diff --git a/gamefi/app/components/BattleScene.ts b/gamefi/app/components/BattleScene.ts index 93d82c1..9c37a1c 100644 --- a/gamefi/app/components/BattleScene.ts +++ b/gamefi/app/components/BattleScene.ts @@ -1,6 +1,9 @@ import Phaser from 'phaser'; -import { PlayerController } from './controllers/PlayerController'; -import { loadPlayerSpriteSheets, createPlayerAnimations } from './assets/PlayerSpriteLoader'; +import { PlayerController } from './controllers/Player/PlayerController'; +import { loadPlayerSpriteSheets, createPlayerAnimations } from './assets/HeroSpriteLoader'; +import { loadVagabondMaterials , createVagabondAnimations } from './assets/VagabondSpriteLoader'; +import { AIController } from './controllers/AI/AIController'; +import { CharState } from './controllers/CharState'; // 如果你仍有需要 hp 字段,可定义 interface SpriteWithHP extends Phaser.Physics.Arcade.Sprite { @@ -9,7 +12,7 @@ interface SpriteWithHP extends Phaser.Physics.Arcade.Sprite { export class BattleScene extends Phaser.Scene { private playerCtrl!: PlayerController; - private monsterCtrl!: PlayerController; + private monsterCtrl!: AIController; private playerSprite!: SpriteWithHP; private monsterSprite!: SpriteWithHP; @@ -28,7 +31,7 @@ export class BattleScene extends Phaser.Scene { // 分别加载“player”资源 & “monster”资源 loadPlayerSpriteSheets(this, 'assets/hero', 'hero', { width: 64, height: 44 }); - // loadPlayerSpriteSheets(this, 'assets/monster', 'monster', { width: 128, height: 128 }); + // loadVagabondMaterials(this, 'assets/vagabond', 'AI', { width: 64, height: 64 }); } public create(): void { @@ -41,16 +44,17 @@ export class BattleScene extends Phaser.Scene { // 创建动画 (player / monster) createPlayerAnimations(this, 'hero'); - // createPlayerAnimations(this, 'monster'); + // createVagabondAnimations(this, 'AI'); // ============ 创建玩家 ============ - // 1) 用 PlayerController, 传入初始纹理 (如 'player_idle') + // 1) 用 PlayerController, 传入初始纹理 this.playerCtrl = new PlayerController(this, 100, 300, 'hero_idle'); this.playerSprite = this.playerCtrl.sprite as SpriteWithHP; this.playerSprite.setScale(1.5); this.playerSprite.hp = 100; this.playerSprite.setCollideWorldBounds(true); this.physics.add.collider(this.playerSprite, ground); + (window as any).playerSprite = this.playerSprite; // HP 文本 this.playerHpText = this.add.text(20, 20, `PLAYER HP: ${this.playerSprite.hp}`, { fontSize: '16px', @@ -58,15 +62,22 @@ export class BattleScene extends Phaser.Scene { }); // ============ 创建怪物 ============ - // 2) 再来一个 PlayerController / 或 AIController - // 并给它用“monster_idle”作为纹理 - // this.monsterCtrl = new PlayerController(this, 600, 300, 'monster_idle'); + // ============ 创建对手(怪物/AI) ============ + // this.monsterCtrl = new AIController(this, 600, 300, 'AI_idle', { + // hp: 100, + // scaleFactor: 1.5, + // debug: true, + // // 可传入 AI 专用参数,如 dashAttackInitialSpeed、dashAttackDeceleration 等 + // dashAttackInitialSpeed: 700, + // dashAttackDeceleration: 0.95, + // animPrefix: 'AI', + // }); // this.monsterSprite = this.monsterCtrl.sprite as SpriteWithHP; // this.monsterSprite.hp = 100; + // this.monsterSprite.setScale(1.5); // this.monsterSprite.setCollideWorldBounds(true); // this.physics.add.collider(this.monsterSprite, ground); - // // HP 文本 // this.monsterHpText = this.add.text(600, 20, `MONSTER HP: ${this.monsterSprite.hp}`, { // fontSize: '16px', // color: '#ffffff' @@ -74,7 +85,7 @@ export class BattleScene extends Phaser.Scene { // 播放 Idle 动画 this.playerSprite.play('hero-idle'); - // this.monsterSprite.play('monster-idle'); + // this.monsterSprite.play('AI-idle'); } public update(time: number, delta: number): void { @@ -88,17 +99,28 @@ export class BattleScene extends Phaser.Scene { // this.monsterHpText.setText(`MONSTER HP: ${this.monsterSprite.hp}`); // 如果你需要碰撞检测(攻击命中等),可以在这里 - // this.checkAttackCollision(time); + this.checkAttackCollision(); } - // 示例:如果要做攻击命中检测,就这么写 - /* - private checkAttackCollision(time: number) { - // e.g. use distance or overlap to see if monster is hit - // If (playerCtrl is attacking && distance < 50) => monsterSprite.hp -= 10 - // ... + + + private checkAttackCollision(): void { + const attackStates = [CharState.Attack1, CharState.Attack2, CharState.Attack3, CharState.DashAttack]; + if (attackStates.includes(this.playerCtrl.state)) { + if (this.physics.overlap(this.playerSprite, this.monsterSprite)) { + // 如果怪物处于 Blocking 状态,则触发弹反(仅对瞬间防御有效) + if (this.monsterCtrl.state === CharState.Blocking) { + this.monsterCtrl.onBlocked(this.playerCtrl); + console.log('Block! Player stunned.'); + } else { + // 否则正常受伤 + const hitDirection = this.playerSprite.x < this.monsterSprite.x ? 'right' : 'left'; + this.monsterCtrl.onHit(10, hitDirection); + } + } + } } - */ + } // Phaser 游戏配置 diff --git a/gamefi/app/components/assets/PlayerSpriteLoader.ts b/gamefi/app/components/assets/HeroSpriteLoader.ts similarity index 95% rename from gamefi/app/components/assets/PlayerSpriteLoader.ts rename to gamefi/app/components/assets/HeroSpriteLoader.ts index d867f77..32de9c7 100644 --- a/gamefi/app/components/assets/PlayerSpriteLoader.ts +++ b/gamefi/app/components/assets/HeroSpriteLoader.ts @@ -17,7 +17,7 @@ export function loadPlayerSpriteSheets(scene: Phaser.Scene, const actions = [ 'attack1','attack2','attack3', 'crouch', 'dash', 'dash_attack', 'death', 'fall', 'hurt', - 'idle', 'jump', 'run', 'slide', 'up_to_fall', + 'idle', 'jump', 'run', 'slide', 'up_to_fall', 'block' ]; actions.forEach(action => { @@ -52,6 +52,7 @@ export function createPlayerAnimations(scene: Phaser.Scene, prefix = 'player') { 'jump': { frames: 3, frameRate: 8, repeat: 0 }, 'up_to_fall': { frames: 2, frameRate: 8, repeat: 0 }, 'slide': { frames: 5, frameRate: 10, repeat: 0 }, + 'block': { frames: 4, frameRate: 8, repeat: 0 }, }; Object.entries(animations).forEach(([key, { frames, frameRate, repeat }]) => { diff --git a/gamefi/app/components/assets/VagabondSpriteLoader.ts b/gamefi/app/components/assets/VagabondSpriteLoader.ts new file mode 100644 index 0000000..5ecef92 --- /dev/null +++ b/gamefi/app/components/assets/VagabondSpriteLoader.ts @@ -0,0 +1,47 @@ +// AILoader.ts +import Phaser from 'phaser'; +const isAttack = (action:string) => action.includes('attack'); +export function loadVagabondMaterials(scene: Phaser.Scene, folder: string, prefix: string, { + width = 64, + height = 64, +}) { + const actions = [ + 'attack1', 'attack2', 'block', 'dash', 'death', 'hurt', + 'idle', 'jump', 'jump-attack', 'jump-flip', 'run','attack3','dash_attack' + ]; + + actions.forEach(action => { + scene.load.spritesheet(`${prefix}_${action}`, `${folder}/${action}/${action}.png`, { + frameWidth:isAttack(action) ? width * 2 : width, + frameHeight: height, + }); + }); +} + + +export function createVagabondAnimations(scene: Phaser.Scene, prefix: string) { + const anims = { + 'idle': { frames: 6, frameRate: 6, repeat: -1 }, + 'run': { frames: 8, frameRate: 10, repeat: -1 }, + 'attack1': { frames: 6, frameRate: 12, repeat: 0 }, + 'attack2': { frames: 6, frameRate: 12, repeat: 0 }, + 'block': { frames: 4, frameRate: 8, repeat: 0 }, + 'dash': { frames: 5, frameRate: 10, repeat: 0 }, + 'death': { frames: 7, frameRate: 8, repeat: 0 }, + 'hurt': { frames: 4, frameRate: 8, repeat: 0 }, + 'jump': { frames: 3, frameRate: 8, repeat: 0 }, + 'jump-attack': { frames: 6, frameRate: 12, repeat: 0 }, + 'jump-flip': { frames: 6, frameRate: 12, repeat: 0 }, + 'attack3': { frames: 13, frameRate: 12, repeat: 0 }, + 'dash_attack': { frames: 10, frameRate: 12, repeat: 0 }, + }; + + Object.entries(anims).forEach(([key, { frames, frameRate, repeat }]) => { + scene.anims.create({ + key: `${prefix}-${key}`, + frames: scene.anims.generateFrameNumbers(`${prefix}_${key}`, { start: 0, end: frames - 1 }), + frameRate, + repeat + }); + }); +} diff --git a/gamefi/app/components/controllers/AI/AIController.ts b/gamefi/app/components/controllers/AI/AIController.ts new file mode 100644 index 0000000..15b27f4 --- /dev/null +++ b/gamefi/app/components/controllers/AI/AIController.ts @@ -0,0 +1,194 @@ +// AIController.ts +import Phaser from 'phaser'; +import { BaseController } from '../BaseController'; +import { AIInput } from './AIInput'; +import { CharState } from '../CharState'; +import { BehaviorManager } from './BehaviorManager'; + +export class AIController extends BaseController { + // AI 专用:虚拟输入 + public aiInput: AIInput = { left: false, right: false, up: false, attack: false, slide: false }; + // AI 难度级别(1: 低,2: 中,3: 高) + public difficultyLevel: number = 1; + + // 跳跃冷却(避免连续跳跃) + public lastJumpTime: number = 0; + public jumpCooldown: number = 2500; // 毫秒 + + // 决策更新间隔与反应延迟 + private lastDecisionTime: number = 0; + private decisionInterval: number = 500; // 每 150ms 更新一次决策 + private reactionDelay: number = 0; // 随机反应延迟(毫秒) + private lastReactionTime: number = 0; + + // 动作锁定,确保一旦决策执行后在一段时间内不更改 + private actionLockUntil: number = 0; + private defaultActionLock: number = 300; // 300ms 内不再更新动作 + + constructor( + scene: Phaser.Scene, + x: number, + y: number, + texture: string, + config?: any + ) { + super(scene, x, y, texture, config); + this.difficultyLevel = this.calculateDifficulty(); + } + + private calculateDifficulty(): number { + const winCount = window['playerWinCount'] || 0; + if (winCount < 3) return 1; + if (winCount < 6) return 2; + return 3; + } + + /** + * 更新 AI 决策,生成虚拟输入(AIInput) + * 采用 Utility 模型,根据双方距离、玩家行为数据、AI 难度及历史决策惯性, + * 在每个决策周期内更新一次输入,并锁定该决策一段时间,防止频繁切换。 + */ + public updateAIDecision(time: number): void { + // 如果当前处于动作锁定期,则不更新决策 + if (time < this.actionLockUntil) { + return; + } + // 只在决策间隔到期后更新 + if (time - this.lastDecisionTime < this.decisionInterval) { + return; + } + this.lastDecisionTime = time; + + // 模拟反应延迟 + if (time - this.lastReactionTime < this.reactionDelay) { + return; + } + // 每次更新决策时,设置一个随机反应延迟(50-150ms) + this.reactionDelay = 50 + Math.random() * 100; + this.lastReactionTime = time; + + // 获取玩家精灵(全局存储,或从 GameManager 中获取) + const playerSprite: Phaser.Physics.Arcade.Sprite = window['playerSprite']; + let distance = 300; + if (playerSprite) { + distance = Math.abs(this.sprite.x - playerSprite.x); + } + + // 获取玩家行为统计数据 + const profile = BehaviorManager.getInstance().profile; + + // 计算各动作基础效用(示例逻辑) + let attackUtility = 0; + let defendUtility = 0; + let jumpUtility = 0; + let dashUtility = 0; + let chaseUtility = 0; + + if (distance < 100) { + attackUtility = 1.0; + defendUtility = 0.5; + jumpUtility = 0.2; + dashUtility = 0.3; + chaseUtility = 0.1; + } else if (distance < 300) { + attackUtility = 0.7; + defendUtility = 0.4; + jumpUtility = 0.3; + dashUtility = 0.4; + chaseUtility = 0.6; + } else { + attackUtility = 0.3; + defendUtility = 0.2; + jumpUtility = 0.4; + dashUtility = 0.5; + chaseUtility = 1.0; + } + + // 根据玩家行为微调:如果玩家频繁使用 dashAttack,则提高防御效用 + if (profile.dashAttackCount / (profile.normalAttackCount + 1) > 0.5) { + defendUtility += 0.3; + } + + // 难度放大:难度越高攻击和追击效用越高 + attackUtility *= this.difficultyLevel; + chaseUtility *= this.difficultyLevel; + + // 加入一定惯性:如果上次决策保持同一移动方向,则增加追击效用 + if (this.aiInput.left || this.aiInput.right) { + chaseUtility += 0.2; + } + + // 加权随机选择动作 + const totalUtility = attackUtility + defendUtility + jumpUtility + dashUtility + chaseUtility; + const rand = Math.random() * totalUtility; + let selectedAction: 'attack' | 'defend' | 'jump' | 'dash' | 'chase' = 'attack'; + if (rand < attackUtility) { + selectedAction = 'attack'; + } else if (rand < attackUtility + defendUtility) { + selectedAction = 'defend'; + } else if (rand < attackUtility + defendUtility + jumpUtility) { + selectedAction = 'jump'; + } else if (rand < attackUtility + defendUtility + jumpUtility + dashUtility) { + selectedAction = 'dash'; + } else { + selectedAction = 'chase'; + } + + // 清空虚拟输入 + this.aiInput = { left: false, right: false, up: false, attack: false, slide: false }; + + // 根据选定的动作设置虚拟输入 + switch (selectedAction) { + case 'attack': + this.aiInput.attack = true; + break; + case 'defend': + // 用 slide 表示防御或闪避 + this.aiInput.slide = true; + break; + case 'jump': + if (time - this.lastJumpTime >= this.jumpCooldown) { + this.aiInput.up = true; + this.lastJumpTime = time; + } + break; + case 'dash': + if (playerSprite) { + this.aiInput.left = this.sprite.x > playerSprite.x; + this.aiInput.right = this.sprite.x < playerSprite.x; + } else { + this.aiInput.left = Math.random() < 0.5; + this.aiInput.right = !this.aiInput.left; + } + break; + case 'chase': + if (playerSprite) { + this.aiInput.left = this.sprite.x > playerSprite.x; + this.aiInput.right = this.sprite.x < playerSprite.x; + } + break; + } + + // 锁定当前动作一段时间,确保动作连贯 + this.actionLockUntil = time + this.defaultActionLock; + + if (this.debug) { + console.debug('AI selected action:', selectedAction, 'with utilities:', { + attackUtility, + defendUtility, + jumpUtility, + dashUtility, + chaseUtility, + }); + console.debug('AI Input:', this.aiInput); + } + } + + public update(time: number, delta: number): void { + this.updateAIDecision(time); + super.update(time, delta); + this.movement.handleMovementInput(time); + this.jump.handleJumpInput(time); + this.attack.handleAttackInput(); + } +} diff --git a/gamefi/app/components/controllers/AI/AIInput.ts b/gamefi/app/components/controllers/AI/AIInput.ts new file mode 100644 index 0000000..c59cdf9 --- /dev/null +++ b/gamefi/app/components/controllers/AI/AIInput.ts @@ -0,0 +1,9 @@ +// AIInput.ts +export interface AIInput { + left: boolean; + right: boolean; + up: boolean; + attack: boolean; + slide: boolean; + } + \ No newline at end of file diff --git a/gamefi/app/components/controllers/AI/BehaviorManager.ts b/gamefi/app/components/controllers/AI/BehaviorManager.ts new file mode 100644 index 0000000..e7e23ef --- /dev/null +++ b/gamefi/app/components/controllers/AI/BehaviorManager.ts @@ -0,0 +1,41 @@ +// BehaviorManager.ts +import { PlayerBehaviorProfile } from '../Player/PlayerBehaviorProfile'; + +export class BehaviorManager { + private static instance: BehaviorManager; + public profile: PlayerBehaviorProfile; + + private constructor() { + // 初始化玩家行为数据 + this.profile = { + normalAttackCount: 0, + dashAttackCount: 0, + jumpCount: 0, + slideCount: 0 + }; + } + + public static getInstance(): BehaviorManager { + if (!BehaviorManager.instance) { + BehaviorManager.instance = new BehaviorManager(); + } + return BehaviorManager.instance; + } + + // 示例接口:记录一次普通攻击 + public recordNormalAttack(): void { + this.profile.normalAttackCount++; + } + + public recordDashAttack(): void { + this.profile.dashAttackCount++; + } + + public recordJump(): void { + this.profile.jumpCount++; + } + + public recordSlide(): void { + this.profile.slideCount++; + } +} diff --git a/gamefi/app/components/controllers/AIController.ts b/gamefi/app/components/controllers/AIController.ts deleted file mode 100644 index e69de29..0000000 diff --git a/gamefi/app/components/controllers/BaseController.ts b/gamefi/app/components/controllers/BaseController.ts index e69de29..c36ae31 100644 --- a/gamefi/app/components/controllers/BaseController.ts +++ b/gamefi/app/components/controllers/BaseController.ts @@ -0,0 +1,223 @@ +// BaseController.ts +import Phaser from 'phaser'; +import { CharState } from './CharState'; +import { PlayerConfig } from './Player/PlayerController'; +import { PlayerStateManager } from './PlayerStateManager'; +import { PlayerAnimationManager } from './PlayerAnimationManager'; +import { PlayerMovement } from './PlayerMovement'; +import { PlayerJump } from './PlayerJump'; +import { PlayerAttack } from './PlayerAttack'; + +export abstract class BaseController { + public sprite: Phaser.Physics.Arcade.Sprite; + public hp: number; + public isDead: boolean = false; + public isInvincible: boolean = false; + + public state: CharState = CharState.Idle; + public scene: Phaser.Scene; + public animPrefix: string; + public debug: boolean; + + // 速度与物理参数 + public runSpeed: number; + public dashSpeed: number; + public slideSpeed: number; + public jumpVelocity: number; + public dashDuration: number; + public doubleTapThreshold: number; + public slideDuration: number; + public slideCooldown: number; + public slideInvincibleDuration: number; + public dashAttackInitialSpeed: number; + public dashAttackDeceleration: number; + + // 定时器变量 + public dashEndTime: number = 0; + public slideEndTime: number = 0; + public lastSlideTime: number = 0; + + // 跳跃相关 + public doubleJumpUsed: boolean = false; + public coyoteTime: number; + public jumpCutMultiplier: number; + public lastGroundedTime: number = 0; + public runStartTime: number = 0; + public isAirRun: boolean = false; + public airRunEligible: boolean = false; + public airRunEligibleTime: number = 0; + + // 输入相关(将输入属性声明为可选) + public cursors?: Phaser.Types.Input.Keyboard.CursorKeys; + public attackKey?: Phaser.Input.Keyboard.Key; + public blockKey?: Phaser.Input.Keyboard.Key; + public slideKey?: Phaser.Input.Keyboard.Key; + public lastDirection: 'left' | 'right' = 'right'; + public lastTap: { left: number; right: number } = { left: 0, right: 0 }; + + // 攻击连击系统 + public nextAttackRequested: boolean = false; + public attackComboStep: number = 0; + public normalAttackAnimations: string[]; + public dashAttackAnimation: string; + + // 状态与动画管理模块 + public stateManager: PlayerStateManager; + public animationManager: PlayerAnimationManager; + + // 公共子模块:移动、跳跃、攻击 + public movement!: PlayerMovement; + public jump!: PlayerJump; + public attack!: PlayerAttack; + + // 输入缓冲(用于存储命令,在当前动作结束后执行) + public bufferedAction: (() => void) | null = null; + + public blockStartTime: number = 0; + public continuousBlock: boolean = false; + + constructor( + scene: Phaser.Scene, + x: number, + y: number, + texture: string, + config?: PlayerConfig + ) { + this.scene = scene; + this.animPrefix = config?.animPrefix ?? 'hero'; + this.hp = config?.hp ?? 100; + this.debug = config?.debug ?? false; + + // 初始化物理参数 + this.runSpeed = config?.runSpeed ?? 150; + this.dashSpeed = config?.dashSpeed ?? 120; + this.slideSpeed = config?.slideSpeed ?? 300; + this.jumpVelocity = config?.jumpVelocity ?? -500; + this.dashDuration = config?.dashDuration ?? 250; + this.doubleTapThreshold = config?.doubleTapThreshold ?? 300; + this.slideDuration = config?.slideDuration ?? 500; + this.slideCooldown = config?.slideCooldown ?? 2000; + this.slideInvincibleDuration = config?.slideInvincibleDuration ?? 300; + this.coyoteTime = config?.coyoteTime ?? 150; + this.jumpCutMultiplier = config?.jumpCutMultiplier ?? 0.9; + this.dashAttackInitialSpeed = config?.dashAttackInitialSpeed ?? 700; + this.dashAttackDeceleration = config?.dashAttackDeceleration ?? 0.95; + + this.lastGroundedTime = 0; + + // 创建精灵并设置属性 + this.sprite = scene.physics.add.sprite(x, y, texture); + this.sprite.setCollideWorldBounds(true); + const scaleFactor = config?.scaleFactor ?? 2; + this.sprite.setScale(scaleFactor); + + // 设置攻击动画键 + this.normalAttackAnimations = [ + `${this.animPrefix}-attack1`, + `${this.animPrefix}-attack2`, + `${this.animPrefix}-attack3` + ]; + this.dashAttackAnimation = `${this.animPrefix}-dash_attack`; + + // 初始化状态与动画管理模块 + this.animationManager = new PlayerAnimationManager(this); + this.stateManager = new PlayerStateManager(this, this.animationManager); + this.movement = new PlayerMovement(this); + this.jump = new PlayerJump(this); + this.attack = new PlayerAttack(this); + } + + public update(time: number, delta: number): void { + if (this.bufferedAction) { + const action = this.bufferedAction; + this.bufferedAction = null; + action(); + } + } + + public playAnimationIfNotPlaying(animKey: string): void { + if (this.sprite.anims.currentAnim && this.sprite.anims.currentAnim.key === animKey) return; + this.sprite.play(animKey, true); + if (this.debug) { + console.debug(`Playing animation: ${animKey}`); + } + } + + + + public isJumping(): boolean { + return ( + this.state === CharState.Jump || + this.state === CharState.UpToFall || + this.state === CharState.Fall || + this.state === CharState.AirRun + ); + } + public isAttacking(): boolean { + return ( + this.state === CharState.Attack1 || + this.state === CharState.Attack2 || + this.state === CharState.Attack3 || + this.state === CharState.DashAttack + ); + } + + /** + * 当角色受到攻击时调用,执行受击反馈 + * @param damage 伤害值 + * @param hitDirection 击退方向('left' 或 'right') + */ + public onHit(damage: number, hitDirection: 'left' | 'right'): void { + // 如果已经在受击状态,则不重复扣血 + if (this.state === CharState.Hurt) return; + + this.hp -= damage; + (this.sprite as any).hp = Math.floor(this.hp); + + if (this.hp <= 0) { + this.die(); + return; + } + + this.stateManager.changeState(CharState.Hurt); + this.playAnimationIfNotPlaying(`${this.animPrefix}-hurt`); + + const knockbackSpeed = 200; + if (hitDirection === 'left') { + this.sprite.setVelocityX(-knockbackSpeed); + } else { + this.sprite.setVelocityX(knockbackSpeed); + } + + this.sprite.setTint(0xff0000); + this.scene.time.delayedCall(200, () => { + this.sprite.clearTint(); + }); + } + + + /** + * 当血量耗尽时调用,处理角色死亡 + */ + public die(): void { + this.stateManager.changeState(CharState.Death); + this.playAnimationIfNotPlaying(`${this.animPrefix}-death`); + // 禁用碰撞、输入、动作更新等 + this.isDead = true; + this.sprite.setVelocity(0); + // 这里可以进一步处理死亡后的逻辑(例如重启关卡) + } + + public onBlocked(attacker: BaseController): void { + // 只有瞬间防御(连续防御不触发弹反) + if (!this.continuousBlock) { + attacker.stateManager.changeState(CharState.Stunned); + attacker.sprite.setVelocity(0); + this.scene.time.delayedCall(1000, () => { + if (attacker.sprite.body!.blocked.down) { + attacker.stateManager.changeState(CharState.Idle); + } + }); + } + } +} diff --git a/gamefi/app/components/controllers/CharState.ts b/gamefi/app/components/controllers/CharState.ts index f7d3c81..ccf12b0 100644 --- a/gamefi/app/components/controllers/CharState.ts +++ b/gamefi/app/components/controllers/CharState.ts @@ -5,12 +5,24 @@ export enum CharState { Jump = 'Jump', UpToFall = 'UpToFall', Fall = 'Fall', + Hurt = 'Hurt', + Death = 'Death', Attack1 = 'Attack1', Attack2 = 'Attack2', Attack3 = 'Attack3', DashAttack = 'DashAttack', - Blocking = 'Blocking', + Blocking = 'Block', Slide = 'Slide', Stunned = 'Stunned', AirRun = 'AirRun', +} + + +export enum AIState { + Idle = 'Idle', + Run = 'Run', + Attack1 = 'Attack1', + Attack2 = 'Attack2', + JumpAttack = 'JumpAttack', + JumpFlip = 'JumpFlip', } \ No newline at end of file diff --git a/gamefi/app/components/controllers/Player/PlayerBehaviorProfile.ts b/gamefi/app/components/controllers/Player/PlayerBehaviorProfile.ts new file mode 100644 index 0000000..818422c --- /dev/null +++ b/gamefi/app/components/controllers/Player/PlayerBehaviorProfile.ts @@ -0,0 +1,8 @@ +// PlayerBehaviorProfile.ts +export interface PlayerBehaviorProfile { + normalAttackCount: number; + dashAttackCount: number; + jumpCount: number; + slideCount: number; + } + \ No newline at end of file diff --git a/gamefi/app/components/controllers/PlayerController.ts b/gamefi/app/components/controllers/Player/PlayerController.ts similarity index 90% rename from gamefi/app/components/controllers/PlayerController.ts rename to gamefi/app/components/controllers/Player/PlayerController.ts index 369b269..41ec18c 100644 --- a/gamefi/app/components/controllers/PlayerController.ts +++ b/gamefi/app/components/controllers/Player/PlayerController.ts @@ -1,11 +1,11 @@ import Phaser from 'phaser'; -import { PlayerMovement } from './PlayerMovement'; -import { PlayerJump } from './PlayerJump'; -import { PlayerAttack } from './PlayerAttack'; -import { PlayerStateManager } from "./PlayerStateManager"; -import { PlayerAnimationManager } from "./PlayerAnimationManager"; -import { CharState } from './CharState'; - +import { PlayerMovement } from '../PlayerMovement'; +import { PlayerJump } from '../PlayerJump'; +import { PlayerAttack } from '../PlayerAttack'; +import { PlayerStateManager } from "../PlayerStateManager"; +import { PlayerAnimationManager } from "../PlayerAnimationManager"; +import { CharState } from '../CharState'; +import { BaseController } from '../BaseController'; export interface PlayerConfig { runSpeed?: number; // 普通移动速度(Run状态) @@ -28,8 +28,7 @@ export interface PlayerConfig { dashAttackDeceleration?: number; // DashAttack 衰减系数(例如 0.95) } -export class PlayerController { - public sprite: Phaser.Physics.Arcade.Sprite; +export class PlayerController extends BaseController { public hp: number; public isDead: boolean = false; /** 在 Slide 等无敌期间为 true */ @@ -86,7 +85,7 @@ export class PlayerController { public dashAttackInitialSpeed: number; public dashAttackDeceleration: number; - private dashAttackAnimation: string; + public dashAttackAnimation: string; // 子模块 public movement: PlayerMovement; @@ -104,6 +103,7 @@ export class PlayerController { texture: string, config?: PlayerConfig ) { + super(scene, x, y, texture, config); this.scene = scene; this.animPrefix = config?.animPrefix ?? 'hero'; this.hp = config?.hp ?? 100; @@ -134,18 +134,14 @@ export class PlayerController { this.dashAttackDeceleration = config?.dashAttackDeceleration ?? 0.95; - // 创建精灵,并设置边界碰撞和缩放 - this.sprite = scene.physics.add.sprite(x, y, texture); - this.sprite.setCollideWorldBounds(true); - const scaleFactor = config?.scaleFactor ?? 2; - this.sprite.setScale(scaleFactor); + // 注册输入 this.cursors = scene.input.keyboard!.createCursorKeys(); this.attackKey = scene.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.F); this.blockKey = scene.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.D); this.slideKey = scene.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT); - + // 设置攻击动画(连击)和 dash 攻击动画 this.normalAttackAnimations = [ `${this.animPrefix}-attack1`, @@ -162,10 +158,27 @@ export class PlayerController { // 初始化状态与动画管理模块 this.animationManager = new PlayerAnimationManager(this); this.stateManager = new PlayerStateManager(this, this.animationManager); - + this.setupAnimationEvents(); } + private handleBlockInput(time: number): void { + // 如果刚按下防御键,则进入 Blocking 状态,并记录开始时间 + if (Phaser.Input.Keyboard.JustDown(this.blockKey) ) { + this.stateManager.changeState(CharState.Blocking); + this.blockStartTime = time; + this.continuousBlock = false; // 瞬间防御 + } + // 如果防御键一直按下,则标记为持续防御 + if (this.blockKey.isDown) { + this.continuousBlock = true; + } + // 如果防御键已释放且已进入 Blocking 状态,并且已超过 1 秒,则退出 Blocking 状态 + if (this.state === CharState.Blocking && !this.blockKey.isDown && time - this.blockStartTime >= 500) { + this.stateManager.changeState(CharState.Idle); + } + } + /** * 监听动画完成事件,根据动画完成时机处理攻击连击等状态切换 */ @@ -219,11 +232,12 @@ export class PlayerController { else if (anim.key === `${this.animPrefix}-slide` && this.state === CharState.Slide) { this.stateManager.changeState(CharState.Idle); } + // 对于 up_to_fall,交由 update() 检测动画进度转换到 Fall 状态 if (this.bufferedAction) { const action = this.bufferedAction; this.bufferedAction = null; action(); - } + } } ); } @@ -310,6 +324,7 @@ export class PlayerController { this.jump.handleJumpInput(time); this.attack.handleAttackInput(); this.handleSlideInput(time); + this.handleBlockInput(time); } /** diff --git a/gamefi/app/components/controllers/PlayerAnimationManager.ts b/gamefi/app/components/controllers/PlayerAnimationManager.ts index ad8d384..ee9b558 100644 --- a/gamefi/app/components/controllers/PlayerAnimationManager.ts +++ b/gamefi/app/components/controllers/PlayerAnimationManager.ts @@ -1,11 +1,11 @@ // PlayerAnimationManager.ts -import { PlayerController } from "./PlayerController"; +import { PlayerController } from "./Player/PlayerController"; import { CharState } from "./CharState"; - +import { BaseController } from "./BaseController"; export class PlayerAnimationManager { - private controller: PlayerController; + private controller: PlayerController | BaseController; - constructor(controller: PlayerController) { + constructor(controller: PlayerController | BaseController) { this.controller = controller; } /** @@ -55,7 +55,7 @@ export class PlayerAnimationManager { this.controller.playAnimationIfNotPlaying(`${this.controller.animPrefix}-slide`); break; case CharState.Blocking: - this.controller.playAnimationIfNotPlaying(`${this.controller.animPrefix}-crouch`); + this.controller.playAnimationIfNotPlaying(`${this.controller.animPrefix}-block`); break; case CharState.Stunned: this.controller.playAnimationIfNotPlaying(`${this.controller.animPrefix}-hurt`); diff --git a/gamefi/app/components/controllers/PlayerAttack.ts b/gamefi/app/components/controllers/PlayerAttack.ts index 725638f..8940e20 100644 --- a/gamefi/app/components/controllers/PlayerAttack.ts +++ b/gamefi/app/components/controllers/PlayerAttack.ts @@ -1,7 +1,8 @@ // PlayerAttack.ts import Phaser from 'phaser'; -import { PlayerController } from './PlayerController'; +import { PlayerController } from './Player/PlayerController'; import { CharState } from './CharState'; +import { BaseController } from './BaseController'; /** * handle player attack input @@ -11,15 +12,22 @@ import { CharState } from './CharState'; */ export class PlayerAttack { - private controller: PlayerController; + private controller: PlayerController | BaseController; private attackBufferThreshold: number = 0.5; - constructor(controller: PlayerController) { + constructor(controller: BaseController) { this.controller = controller; } - + private getAttackInput(): boolean { + if ((this.controller as any).aiInput !== undefined) { + return (this.controller as any).aiInput.attack; + } + // 现在 BaseController 定义了 attackKey(可选),我们使用断言来确保它存在 + return Phaser.Input.Keyboard.JustDown(this.controller.attackKey!); + } + public handleAttackInput(): void { - if (Phaser.Input.Keyboard.JustDown(this.controller.attackKey)) { + if (this.getAttackInput()) { // if in the state of Run or Dash, dash attack will be triggered if ( diff --git a/gamefi/app/components/controllers/PlayerJump.ts b/gamefi/app/components/controllers/PlayerJump.ts index ec1c540..9833b1d 100644 --- a/gamefi/app/components/controllers/PlayerJump.ts +++ b/gamefi/app/components/controllers/PlayerJump.ts @@ -1,7 +1,8 @@ // PlayerJump.ts import Phaser from 'phaser'; -import { PlayerController } from './PlayerController'; +import { PlayerController } from './Player/PlayerController'; import { CharState } from './CharState'; +import { BaseController } from './BaseController'; /** * Handle player jump input (including jump and double jump), @@ -9,15 +10,24 @@ import { CharState } from './CharState'; * then jumping will trigger an AirRun state. */ export class PlayerJump { - private controller: PlayerController; + private controller: PlayerController |BaseController; - constructor(controller: PlayerController) { + constructor(controller: PlayerController | BaseController) { this.controller = controller; } + private getJumpInput(): boolean { + // 如果存在 aiInput,则直接返回 aiInput.up(假设 AI 在需要触发跳跃时只给 true 一帧) + if ((this.controller as any).aiInput !== undefined) { + return (this.controller as any).aiInput.up; + } + return Phaser.Input.Keyboard.JustDown(this.controller.cursors!.up!); + } + + public handleJumpInput(time: number): void { // Detect jump key press - if (Phaser.Input.Keyboard.JustDown(this.controller.cursors.up!)) { + if (this.getJumpInput()) { // If on the ground or within coyote time (jump buffer) if ( this.controller.sprite.body!.blocked.down || @@ -56,13 +66,18 @@ export class PlayerJump { } this.controller.sprite.setVelocityY(this.controller.jumpVelocity); } + + } // Flexible jump height: if upward velocity and jump key is released, // reduce upward velocity so that jump height depends on how long the key is held + const jumpHeld = (this.controller as any).aiInput + ? (this.controller as any).aiInput.up + : this.controller.cursors?.up?.isDown; if ( this.controller.sprite.body!.velocity.y < 0 && - !this.controller.cursors.up!.isDown + !jumpHeld ) { this.controller.sprite.setVelocityY( this.controller.sprite.body!.velocity.y * this.controller.jumpCutMultiplier diff --git a/gamefi/app/components/controllers/PlayerMovement.ts b/gamefi/app/components/controllers/PlayerMovement.ts index 4a8eb41..8bfe103 100644 --- a/gamefi/app/components/controllers/PlayerMovement.ts +++ b/gamefi/app/components/controllers/PlayerMovement.ts @@ -1,7 +1,8 @@ // PlayerMovement.ts import Phaser from 'phaser'; -import { PlayerController } from './PlayerController'; +import { PlayerController } from './Player/PlayerController'; import { CharState } from './CharState'; +import { BaseController } from './BaseController'; /** * Handle player movement input. @@ -9,9 +10,9 @@ import { CharState } from './CharState'; * transition into Dash state as a running-to-dash transition. */ export class PlayerMovement { - private controller: PlayerController; + private controller: PlayerController | BaseController; - constructor(controller: PlayerController) { + constructor(controller: PlayerController | BaseController) { this.controller = controller; } @@ -20,9 +21,11 @@ export class PlayerMovement { let moving = false; // 判断是否在空中 const isAirborne = !this.controller.sprite.body!.blocked.down; - + const input = (this.controller as any).aiInput || this.controller.cursors; + const leftPressed = typeof input.left === 'boolean' ? input.left : input.left?.isDown; + const rightPressed = typeof input.right === 'boolean' ? input.right : input.right?.isDown; // 检测左右方向键 - if (this.controller.cursors.left?.isDown) { + if (leftPressed) { velocityX = -this.controller.runSpeed; moving = true; this.controller.sprite.setFlipX(true); @@ -31,7 +34,7 @@ export class PlayerMovement { this.controller.stateManager.changeState(CharState.Run); } this.controller.lastDirection = 'left'; - } else if (this.controller.cursors.right?.isDown) { + } else if (rightPressed) { velocityX = this.controller.runSpeed; moving = true; this.controller.sprite.setFlipX(false); diff --git a/gamefi/app/components/controllers/PlayerStateManager.ts b/gamefi/app/components/controllers/PlayerStateManager.ts index d5982c3..f4c9082 100644 --- a/gamefi/app/components/controllers/PlayerStateManager.ts +++ b/gamefi/app/components/controllers/PlayerStateManager.ts @@ -1,13 +1,14 @@ // PlayerStateManager.ts -import { PlayerController } from "./PlayerController"; +import { PlayerController } from "./Player/PlayerController"; import { PlayerAnimationManager } from "./PlayerAnimationManager"; import { CharState } from "./CharState"; +import {BaseController } from "./BaseController"; export class PlayerStateManager { - private controller: PlayerController; + private controller: PlayerController | BaseController; private animationManager: PlayerAnimationManager; - constructor(controller: PlayerController, animationManager: PlayerAnimationManager) { + constructor(controller: PlayerController | BaseController, animationManager: PlayerAnimationManager) { this.controller = controller; this.animationManager = animationManager; } diff --git a/gamefi/app/global.d.ts b/gamefi/app/global.d.ts new file mode 100644 index 0000000..2c7c0a0 --- /dev/null +++ b/gamefi/app/global.d.ts @@ -0,0 +1,15 @@ +interface Ethereum { + request: (args: any) => Promise; + on: (event: string, handler: (...args: any[]) => void) => void; +} + +interface solana { + connect: () => Promise; +} + +interface Window { + ethereum?: Ethereum; + solana?: solana; + playerWinCount?: number; +} + diff --git a/gamefi/global.d.ts b/gamefi/global.d.ts index 22bebb6..35994fb 100644 --- a/gamefi/global.d.ts +++ b/gamefi/global.d.ts @@ -1,8 +1,4 @@ -interface Ethereum { - request: (args: any) => Promise; - on: (event: string, handler: (...args: any[]) => void) => void; - } interface Window { - ethereum?: Ethereum; + playerSprite?: any; } \ No newline at end of file diff --git a/gamefi/public/assets/hero/block/block.png b/gamefi/public/assets/hero/block/block.png new file mode 100644 index 0000000..4108cba Binary files /dev/null and b/gamefi/public/assets/hero/block/block.png differ diff --git a/gamefi/public/assets/monster/Idle.png b/gamefi/public/assets/monster/Idle.png deleted file mode 100644 index cf29584..0000000 Binary files a/gamefi/public/assets/monster/Idle.png and /dev/null differ diff --git a/gamefi/public/assets/monster/attack1.png b/gamefi/public/assets/monster/attack1.png deleted file mode 100644 index 752d374..0000000 Binary files a/gamefi/public/assets/monster/attack1.png and /dev/null differ diff --git a/gamefi/public/assets/monster/attack2.png b/gamefi/public/assets/monster/attack2.png deleted file mode 100644 index d3ac9e4..0000000 Binary files a/gamefi/public/assets/monster/attack2.png and /dev/null differ diff --git a/gamefi/public/assets/monster/attack3.png b/gamefi/public/assets/monster/attack3.png deleted file mode 100644 index 3a3ac0e..0000000 Binary files a/gamefi/public/assets/monster/attack3.png and /dev/null differ diff --git a/gamefi/public/assets/monster/death.png b/gamefi/public/assets/monster/death.png deleted file mode 100644 index 57fbccc..0000000 Binary files a/gamefi/public/assets/monster/death.png and /dev/null differ diff --git a/gamefi/public/assets/monster/defend.png b/gamefi/public/assets/monster/defend.png deleted file mode 100644 index 350fc81..0000000 Binary files a/gamefi/public/assets/monster/defend.png and /dev/null differ diff --git a/gamefi/public/assets/monster/hurt.png b/gamefi/public/assets/monster/hurt.png deleted file mode 100644 index 02df885..0000000 Binary files a/gamefi/public/assets/monster/hurt.png and /dev/null differ diff --git a/gamefi/public/assets/monster/jump.png b/gamefi/public/assets/monster/jump.png deleted file mode 100644 index 0cb8505..0000000 Binary files a/gamefi/public/assets/monster/jump.png and /dev/null differ diff --git a/gamefi/public/assets/monster/run.png b/gamefi/public/assets/monster/run.png deleted file mode 100644 index 1a6bc91..0000000 Binary files a/gamefi/public/assets/monster/run.png and /dev/null differ diff --git a/gamefi/public/assets/monster/walk.png b/gamefi/public/assets/monster/walk.png deleted file mode 100644 index 3f8246e..0000000 Binary files a/gamefi/public/assets/monster/walk.png and /dev/null differ diff --git a/gamefi/public/assets/player/attack1.png b/gamefi/public/assets/player/attack1.png deleted file mode 100644 index 3c0b70d..0000000 Binary files a/gamefi/public/assets/player/attack1.png and /dev/null differ diff --git a/gamefi/public/assets/player/attack2.png b/gamefi/public/assets/player/attack2.png deleted file mode 100644 index 8ba3013..0000000 Binary files a/gamefi/public/assets/player/attack2.png and /dev/null differ diff --git a/gamefi/public/assets/player/attack3.png b/gamefi/public/assets/player/attack3.png deleted file mode 100644 index 5140167..0000000 Binary files a/gamefi/public/assets/player/attack3.png and /dev/null differ diff --git a/gamefi/public/assets/player/death.png b/gamefi/public/assets/player/death.png deleted file mode 100644 index d40b2bb..0000000 Binary files a/gamefi/public/assets/player/death.png and /dev/null differ diff --git a/gamefi/public/assets/player/defend.png b/gamefi/public/assets/player/defend.png deleted file mode 100644 index ae99192..0000000 Binary files a/gamefi/public/assets/player/defend.png and /dev/null differ diff --git a/gamefi/public/assets/player/hurt.png b/gamefi/public/assets/player/hurt.png deleted file mode 100644 index 8ca8578..0000000 Binary files a/gamefi/public/assets/player/hurt.png and /dev/null differ diff --git a/gamefi/public/assets/player/idle.png b/gamefi/public/assets/player/idle.png deleted file mode 100644 index f00a294..0000000 Binary files a/gamefi/public/assets/player/idle.png and /dev/null differ diff --git a/gamefi/public/assets/player/jump.png b/gamefi/public/assets/player/jump.png deleted file mode 100644 index aa0c02e..0000000 Binary files a/gamefi/public/assets/player/jump.png and /dev/null differ diff --git a/gamefi/public/assets/player/run.png b/gamefi/public/assets/player/run.png deleted file mode 100644 index 3b2fc4e..0000000 Binary files a/gamefi/public/assets/player/run.png and /dev/null differ diff --git a/gamefi/public/assets/player/walk.png b/gamefi/public/assets/player/walk.png deleted file mode 100644 index 1d9aff1..0000000 Binary files a/gamefi/public/assets/player/walk.png and /dev/null differ diff --git a/gamefi/public/assets/vagabond/attack1/attack1.png b/gamefi/public/assets/vagabond/attack1/attack1.png new file mode 100644 index 0000000..067b31f Binary files /dev/null and b/gamefi/public/assets/vagabond/attack1/attack1.png differ diff --git a/gamefi/public/assets/vagabond/attack2/attack2.png b/gamefi/public/assets/vagabond/attack2/attack2.png new file mode 100644 index 0000000..f14d9a7 Binary files /dev/null and b/gamefi/public/assets/vagabond/attack2/attack2.png differ diff --git a/gamefi/public/assets/vagabond/attack3/attack3.png b/gamefi/public/assets/vagabond/attack3/attack3.png new file mode 100644 index 0000000..a7b7299 Binary files /dev/null and b/gamefi/public/assets/vagabond/attack3/attack3.png differ diff --git a/gamefi/public/assets/vagabond/block/block.png b/gamefi/public/assets/vagabond/block/block.png new file mode 100644 index 0000000..d0331c8 Binary files /dev/null and b/gamefi/public/assets/vagabond/block/block.png differ diff --git a/gamefi/public/assets/vagabond/dash/dash.png b/gamefi/public/assets/vagabond/dash/dash.png new file mode 100644 index 0000000..f757368 Binary files /dev/null and b/gamefi/public/assets/vagabond/dash/dash.png differ diff --git a/gamefi/public/assets/vagabond/dash_attack/dash_attack.png b/gamefi/public/assets/vagabond/dash_attack/dash_attack.png new file mode 100644 index 0000000..067b31f Binary files /dev/null and b/gamefi/public/assets/vagabond/dash_attack/dash_attack.png differ diff --git a/gamefi/public/assets/vagabond/death/death.png b/gamefi/public/assets/vagabond/death/death.png new file mode 100644 index 0000000..dd54ca1 Binary files /dev/null and b/gamefi/public/assets/vagabond/death/death.png differ diff --git a/gamefi/public/assets/vagabond/hurt/hurt.png b/gamefi/public/assets/vagabond/hurt/hurt.png new file mode 100644 index 0000000..9e99c24 Binary files /dev/null and b/gamefi/public/assets/vagabond/hurt/hurt.png differ diff --git a/gamefi/public/assets/vagabond/idle/idle.png b/gamefi/public/assets/vagabond/idle/idle.png new file mode 100644 index 0000000..76f03e2 Binary files /dev/null and b/gamefi/public/assets/vagabond/idle/idle.png differ diff --git a/gamefi/public/assets/vagabond/jump-attack/jump-attack.png b/gamefi/public/assets/vagabond/jump-attack/jump-attack.png new file mode 100644 index 0000000..a7b7299 Binary files /dev/null and b/gamefi/public/assets/vagabond/jump-attack/jump-attack.png differ diff --git a/gamefi/public/assets/vagabond/jump-flip/jump-flip.png b/gamefi/public/assets/vagabond/jump-flip/jump-flip.png new file mode 100644 index 0000000..7daecda Binary files /dev/null and b/gamefi/public/assets/vagabond/jump-flip/jump-flip.png differ diff --git a/gamefi/public/assets/vagabond/jump/jump.png b/gamefi/public/assets/vagabond/jump/jump.png new file mode 100644 index 0000000..3d20a66 Binary files /dev/null and b/gamefi/public/assets/vagabond/jump/jump.png differ diff --git a/gamefi/public/assets/vagabond/run/run.png b/gamefi/public/assets/vagabond/run/run.png new file mode 100644 index 0000000..387d234 Binary files /dev/null and b/gamefi/public/assets/vagabond/run/run.png differ