Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 41 additions & 19 deletions gamefi/app/components/BattleScene.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
Expand All @@ -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 {
Expand All @@ -41,40 +44,48 @@ 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',
color: '#ffffff'
});

// ============ 创建怪物 ============
// 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'
// });

// 播放 Idle 动画
this.playerSprite.play('hero-idle');
// this.monsterSprite.play('monster-idle');
// this.monsterSprite.play('AI-idle');
}

public update(time: number, delta: number): void {
Expand All @@ -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 游戏配置
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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 }]) => {
Expand Down
47 changes: 47 additions & 0 deletions gamefi/app/components/assets/VagabondSpriteLoader.ts
Original file line number Diff line number Diff line change
@@ -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
});
});
}
194 changes: 194 additions & 0 deletions gamefi/app/components/controllers/AI/AIController.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
9 changes: 9 additions & 0 deletions gamefi/app/components/controllers/AI/AIInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// AIInput.ts
export interface AIInput {
left: boolean;
right: boolean;
up: boolean;
attack: boolean;
slide: boolean;
}

Loading