消消方块阵换皮表情
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

345 lines
13 KiB

1 week ago
import { _decorator, Component, sp, Tween, v3, Vec3, Node, tween, Color } from 'cc';
import { SuDace } from './SuDace';
import { MoveDirection, SuDacePlayer } from './SuDacePlayer';
import { SuDacePoint } from './SuDacePoint';
const { ccclass, property } = _decorator;
/**
* 线 SuDacePlayer.tickDirectionalInput stepAxisLockedWorldMove + onMoveStatusChange
* setMoveInput
*/
@ccclass('SuDaceEnemy')
export class SuDaceEnemy extends Component {
@property
moveSpeed = 100;
@property
maxHp = 20;
@property
hp = 20;
@property
level = 1;
@property
attackDamage = 1;
@property
attackInterval = 0.6;
@property
attackYTolerance = 80;
/** 由脚下路点 SuDacePoint.floor 得到(平台可走处需有带 floor 的路点或占位) */
currentFloor: number = 0;
anim: sp.Skeleton = null;
/**正在攻击 */
isAttacking = false;
/**正在爬楼梯 */
isClimbing = false;
/** 本帧希望移动的方向(对应玩家 _inputDir;默认向左,可由外部或后续 AI 改写) */
private _moveIntent: MoveDirection = MoveDirection.LEFT;
/** 上一帧有效位移方向(与玩家 _lastInputDir 一致) */
private _lastInputDir: MoveDirection = MoveDirection.NONE;
/** 须从 NONE 开始,否则首帧 onMoveStatusChange(LEFT) 会因「已是 LEFT」被整段跳过 */
private _curentStatus: MoveDirection = MoveDirection.NONE;
private _player: SuDacePlayer = null;
private _attackAcc = 0;
protected onLoad(): void {
this.anim = this.node.getComponentInChildren(sp.Skeleton);
}
init(level: number, hp: number, moveSpeed: number): void {
this.level = Math.max(1, level);
this.maxHp = Math.max(1, Math.floor(hp));
this.hp = this.maxHp;
this.moveSpeed = Math.max(10, moveSpeed);
this._moveIntent = MoveDirection.LEFT;
this._lastInputDir = MoveDirection.NONE;
this._curentStatus = MoveDirection.NONE;
this._player = SuDace.instance?.map.player;
this._attackAcc = 0;
this.isAttacking = false;
this.isClimbing = false;
}
update(dt: number): void {
const su = SuDace.instance;
if (!su?.map) return;
if (!this._player?.node?.isValid) {
this._player = su.map.player;
}
if (!this._player?.node?.isValid) {
this._moveIntent = MoveDirection.NONE;
this.tickMoveByIntent(dt);
return;
}
let distance = Vec3.distance(this.node.worldPosition, this._player.node.worldPosition);
const yDiff = Math.abs(this.node.worldPosition.y - this._player.node.worldPosition.y);
// 梯口吸附后两者 y 可能存在几十像素偏差;容差过小会出现“只打一下就脱战”
this.isAttacking = distance < 100 && yDiff <= Math.max(0, this.attackYTolerance);
if (this.isAttacking) {
this._moveIntent = MoveDirection.NONE;
this._attackAcc += dt;
if (this._attackAcc >= Math.max(0.05, this.attackInterval)) {
this._attackAcc -= Math.max(0.05, this.attackInterval);
this._player.takeDamage(this.attackDamage);
this.playAttackSpine();
}
} else {
this._attackAcc = 0;
let playerY = this._player.node.worldPosition.y;
let playerX = this._player.node.worldPosition.x;
let enemyY = this.node.worldPosition.y;
let enemyX = this.node.worldPosition.x;
// worldPosition 是浮点数,使用容差判断同一高度,避免因精度抖动导致一直不相等
if (Math.abs(enemyY - playerY) <= 2) {
this.isClimbing = false;
if (enemyX > playerX) {
this._moveIntent = MoveDirection.LEFT;
} else {
this._moveIntent = MoveDirection.RIGHT;
}
} else {
if (!this.isClimbing) {
const needUp = enemyY < playerY;
const grid = this.findNearestStairEntrance(
needUp,
enemyX,
enemyY,
su.map.gridNode?.children ?? []
);
if (grid) {
let disX = Math.abs(enemyX - grid.worldPosition.x);
let disY = Math.abs(enemyY - grid.worldPosition.y);
if (disX < 50 && disY < 50) {
this.isClimbing = true;
}
if (enemyX > grid.worldPosition.x) {
this._moveIntent = MoveDirection.LEFT;
} else {
this._moveIntent = MoveDirection.RIGHT;
}
} else {
// 没找到梯口时继续巡逻,避免原地卡死
this._moveIntent = MoveDirection.LEFT;
}
} else {
if (Math.abs(enemyY - playerY) <= 2) {
// 高度已接近时,也把位置吸附到目标梯口,避免结束爬梯后还偏离梯口
const arriveByUp = enemyY < playerY;
const nearestExitStair = this.findNearestStairEntrance(
!arriveByUp,
enemyX,
enemyY,
su.map.gridNode?.children ?? []
);
this.finishClimbAtStair(nearestExitStair, playerX);
} else if (enemyY < playerY) {
this._moveIntent = MoveDirection.UP;
// 如果到达最近下梯口,则终止爬楼梯
const nearestDownStair = this.findNearestStairEntrance(
false,
enemyX,
enemyY,
su.map.gridNode?.children ?? []
);
if (this.isReachedStair(nearestDownStair, enemyX, enemyY)) {
this.finishClimbAtStair(nearestDownStair, playerX);
}
} else {
this._moveIntent = MoveDirection.DOWN;
// 如果到达最近上梯口,则终止爬楼梯
const nearestUpStair = this.findNearestStairEntrance(
true,
enemyX,
enemyY,
su.map.gridNode?.children ?? []
);
if (this.isReachedStair(nearestUpStair, enemyX, enemyY)) {
this.finishClimbAtStair(nearestUpStair, playerX);
}
}
}
}
}
this.tickMoveByIntent(dt);
}
/**
* /退
*/
private findNearestStairEntrance(needUp: boolean, enemyX: number, enemyY: number, cells: Node[]): Node | null {
let bestNear: Node | null = null;
let bestNearScore = Number.POSITIVE_INFINITY;
let bestAny: Node | null = null;
let bestAnyScore = Number.POSITIVE_INFINITY;
const sameBand = 90;
for (let i = 0; i < cells.length; i++) {
const g = cells[i];
const point = g.getComponent(SuDacePoint);
if (!point) continue;
if (needUp && !point.isUpStairs) continue;
if (!needUp && !point.isDownStairs) continue;
const dx = Math.abs(g.worldPosition.x - enemyX);
const dy = Math.abs(g.worldPosition.y - enemyY);
const score = dx + dy * 0.35;
if (score < bestAnyScore) {
bestAnyScore = score;
bestAny = g;
}
if (dy <= sameBand && score < bestNearScore) {
bestNearScore = score;
bestNear = g;
}
}
return bestNear ?? bestAny;
}
private isReachedStair(stair: Node | null, enemyX: number, enemyY: number): boolean {
if (!stair) return false;
const reachX = 50;
const reachY = 50;
const disX = Math.abs(enemyX - stair.worldPosition.x);
const disY = Math.abs(enemyY - stair.worldPosition.y);
return disX <= reachX && disY <= reachY;
}
private finishClimbAtStair(stair: Node | null, playerX: number): void {
if (stair?.isValid) {
const z = this.node.worldPosition.z;
this.node.setWorldPosition(stair.worldPosition.x, stair.worldPosition.y, z);
}
this.isClimbing = false;
this._moveIntent = this.node.worldPosition.x > playerX ? MoveDirection.LEFT : MoveDirection.RIGHT;
}
/**
* SuDacePlayer.tickDirectionalInput isUpStairs /
* resolveStairMoveDirection + isClimbing
*/
private tickMoveByIntent(dt: number): void {
const effective = this._moveIntent;
if (effective === MoveDirection.NONE) {
if (this._lastInputDir !== MoveDirection.NONE) {
this.onMoveStatusChange(MoveDirection.NONE);
}
this._lastInputDir = MoveDirection.NONE;
return;
}
if (this._lastInputDir === MoveDirection.NONE) {
Tween.stopAllByTarget(this.node);
}
this._lastInputDir = effective;
this.stepAxisLockedWorldMove(effective, dt);
this.onMoveStatusChange(effective);
}
/** 与 SuDacePlayer.stepAxisLockedWorldMove 一致 */
private stepAxisLockedWorldMove(dir: MoveDirection, dt: number): void {
const p = this.node.worldPosition;
const step = this.moveSpeed * dt;
switch (dir) {
case MoveDirection.RIGHT:
this.node.setWorldPosition(p.x + step, p.y, p.z);
break;
case MoveDirection.LEFT:
this.node.setWorldPosition(p.x - step, p.y, p.z);
break;
case MoveDirection.UP:
this.node.setWorldPosition(p.x, p.y + step, p.z);
break;
case MoveDirection.DOWN:
this.node.setWorldPosition(p.x, p.y - step, p.z);
break;
default:
break;
}
}
/** 与 SuDacePlayer.onMoveStatusChange 一致(ZClips / 节点显隐 / scale) */
onMoveStatusChange(status: MoveDirection): void {
if (this._curentStatus === status) return;
this._curentStatus = status;
if (this._curentStatus !== MoveDirection.NONE) {
this.playWalkSpine();
}
switch (status) {
case MoveDirection.NONE:
break;
case MoveDirection.RIGHT:
this.node.scale = v3(-1, 1, 1);
break;
case MoveDirection.LEFT:
this.node.scale = v3(1, 1, 1);
break;
case MoveDirection.UP:
break;
case MoveDirection.DOWN:
break;
}
}
hit(damage: number): void {
this.hp -= Math.max(0, damage);
if (this.hp <= 0) {
this.die();
}
SuDace.instance.playHit();
Tween.stopAllByTarget(this.anim)
tween(this.anim)
.call(() => { this.anim.color = new Color(255, 83, 83, 255) })
.delay(0.35)
.call(() => { this.anim.color = new Color(255, 255, 255, 255) })
.start()
}
die(): void {
SuDace.instance?.recycleMonster(this.node);
}
/**播放走路动画 */
playWalkSpine(callback: Function = null, timeScale = 1, loop = true): void {
this.playSpine('走路', callback, timeScale, loop);
}
/**播放攻击动画 */
playAttackSpine(callback: Function = null, timeScale = 1, loop = false): void {
this.playSpine('攻击', callback, timeScale, loop);
}
/**播放spine动画 */
playSpine(name: string, callback: Function = null, timeScale = 1, loop = false): void {
if (this.anim.animation == name && loop) {
return;
}
this.anim.setCompleteListener(() => {
this.anim.setCompleteListener(null);
if (callback) callback();
});
this.anim.timeScale = timeScale;
this.anim.setAnimation(0, name, loop);
}
}