import { _decorator, clamp01, Collider2D, Color, Component, Contact2DType, director, find, instantiate, IPhysics2DContact, Label, Node, Sprite, Tween, tween, v3, Vec3 } from 'cc'; import ZClips from 'db://assets/mx/components/ZClips'; import { SuDace } from './SuDace'; import { SuDaceBullet } from './SuDaceBullet'; import { SuDacePoint } from './SuDacePoint'; import { SuDaceBox } from './SuDaceBox'; /** 避免与 SuDaceMap 循环引用,仅声明格子查询 */ interface ISuDaceGridMap { getGridCellAtWorld(worldPos: Readonly): Node | null; getSuDacePointAtWorld(worldPos: Readonly): SuDacePoint | null; } const { ccclass, property } = _decorator; /** 移动方向(与 onMoveStatusChange / 动作表现一致,供摇杆等输入量化后复用) */ export enum MoveDirection { NONE = 0, RIGHT = 1, LEFT = -1, UP = 2, DOWN = -2, } @ccclass('SuDacePlayer') export class SuDacePlayer extends Component { @property({ displayName: "移动速度" }) moveSpeed: number = 300; @property({ displayName: "最大血量" }) maxHp = 100; hp = 100; /**玩家节点 */ skinNode: Node = null; /**身体节点 */ bodyNode: Node = null; /**倒地节点 */ dieNode: Node = null; /**爬梯子 */ upMoveNode: Node = null; /**头部节点 */ headNode: Node = null; /**腿部节点 */ legNode: Node = null; /**胳膊节点 */ armNode: Node = null; /**血条 */ hpBar: Sprite = null; /**子弹模版 */ bulletTemplate: Node = null; /**是否正在爬楼梯 */ isClimbing: boolean = false; /**描述文本 */ lbDesc: Label = null; /**当前所在楼层(由脚下格子 SuDacePoint.floor 刷新) */ currentFloor: number = 0; private _dead = false; _col: Collider2D | null = null; atkAdd = 0; speedAdd = 0; atkIntervalAdd = 0; addAttr(index: number, value: number) { this.lbDesc.node.active = true; switch (index) { case 0: this.atkAdd += value; this.lbDesc.string = `攻击力+${Math.floor(value * 100)}%`; break; case 1: this.speedAdd += value; this.lbDesc.string = `速度+${Math.floor(value * 100)}%`; break; case 2: this.atkIntervalAdd += value; this.lbDesc.string = `攻击间隔+${Math.floor(value * 100)}%`; break; case 3: this.hp += value; if (this.hp > this.maxHp) this.hp = this.maxHp; this.lbDesc.string = `血量+${Math.floor(value * 100)}%`; break; } this.lbDesc.node.opacity = 255; tween(this.lbDesc.node).delay(2).to(1, { opacity: 0 }).call(() => { this.lbDesc.node.active = false; }).start(); } protected onLoad(): void { this.skinNode = find("skin", this.node); this.bodyNode = find("skin/射击", this.node); this.dieNode = find("skin/倒地", this.node); this.upMoveNode = find("skin/爬梯", this.node); this.legNode = find("skin/射击/腿部", this.node); this.headNode = find("skin/射击/头部", this.node); this.armNode = find("skin/射击/胳膊", this.node); this.bulletTemplate = find("bullet", this.armNode); this.hpBar = find("b/bp", this.node).getComponent(Sprite); this.lbDesc = find("lbDesc", this.node).getComponent(Label); this.hp = Math.max(1, this.maxHp); } protected onEnable(): void { this._col = this.node.getComponent(Collider2D); this._col?.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); } protected onDisable(): void { this._col?.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); } takeDamage(amount: number): void { if (this._dead || amount <= 0) return; this.hp -= amount; if (this.hp <= 0) { this.hp = 0; this._dead = true; SuDace.instance?.onPlayerDefeated(); } this.hpBar.fillRange = clamp01(this.hp / this.maxHp); let comp = this.node.getComponentsInChildren(Sprite); comp.forEach(spr => { if (spr.node.name != "bp") spr.color = Color.RED }); this.scheduleOnce(() => { comp.forEach(spr => { spr.color = Color.WHITE }); }, 0.3); } private onBeginContact(self: Collider2D, other: Collider2D, contact: IPhysics2DContact | null) { if (!this.node.active || !other?.node?.active) return; const otherName = other.node.name.toLowerCase(); if (otherName === "box" || otherName.includes("box")) { let comp = other.node.getComponent(SuDaceBox); if (comp) { comp.openBox(this); } } } /** 关卡重开时由 SuDace 调用 */ resetForStage(): void { this._dead = false; this.hp = Math.max(1, this.maxHp); this.hpBar.fillRange = clamp01(this.hp / this.maxHp); this.isClimbing = false; this._inputDir = MoveDirection.NONE; this._lastInputDir = MoveDirection.NONE; this._climbStartGrid = null; this._leftStairStart = false; this.onMoveStatusChange(MoveDirection.NONE); } /** 摇杆四向输入;NONE 表示无输入 */ private _inputDir: MoveDirection = MoveDirection.NONE; /** 上一帧有效位移方向(含楼梯规则),用于边界时打断 tween、松手回待机 */ private _lastInputDir: MoveDirection = MoveDirection.NONE; /** 开始攀爬时所在格子,用于判断是否离开起点 */ private _climbStartGrid: Node | null = null; private readonly _climbStartWorld = new Vec3(); private _leftStairStart = false; /** 离开楼梯起点判定:位移或换格超过该阈值视为已离开起点(像素,世界单位) */ private static readonly _STAIR_LEAVE_EPS = 22; curentAtkInterval = 100; /** * 由 SuDaceJoystick 等输入组件每帧/每次触摸更新;仅改变位移,不改变 onMoveStatusChange 分支语义。 */ setMoveInput(dir: MoveDirection): void { this._inputDir = dir; } protected update(dt: number): void { if (this._dead) return; if (!SuDace.instance) { return; } if (SuDace.instance.gameFailed || SuDace.instance.levelCleared) return; this.curentAtkInterval = 30; if (this.atkIntervalAdd > 0) { this.curentAtkInterval = Math.max(30 - (this.atkIntervalAdd * 30), 10); } let grid = SuDace.instance.map.getGridCellAtWorld(this.node.worldPosition); if (director.getTotalFrames() % this.curentAtkInterval == 0) { this.atk(); SuDace.instance.playAtk(); if (grid) { let p = grid.getComponent(SuDacePoint); if (p && p.isTrap) { let stone = SuDace.instance.map.generateStone(); stone.setWorldPosition(grid.worldPosition.clone().add(v3(0, -50, 0))); stone.angle = 180; } } } this.tickDirectionalInput(dt); } private tickDirectionalInput(dt: number): void { const map = SuDace.instance?.map; const point = map?.getSuDacePointAtWorld(this.node.worldPosition) ?? null; const effective = this.resolveStairMoveDirection(this._inputDir, point); if (effective === MoveDirection.NONE) { if (this._lastInputDir !== MoveDirection.NONE) { if (!this.isClimbing) { this.onMoveStatusChange(MoveDirection.NONE); } } this._lastInputDir = MoveDirection.NONE; if (this.isClimbing) { this.refreshStairLanding(map); } return; } if (!this.isClimbing) { if (effective === MoveDirection.UP && point?.isUpStairs) { this.beginStairClimb(map); } else if (effective === MoveDirection.DOWN && point?.isDownStairs) { this.beginStairClimb(map); } } if (this._lastInputDir === MoveDirection.NONE) { Tween.stopAllByTarget(this.node); } this._lastInputDir = effective; this.stepAxisLockedWorldMove(effective, dt); this.onMoveStatusChange(effective); if (this.isClimbing) { this.refreshStairLanding(map); } } private beginStairClimb(map: ISuDaceGridMap | null | undefined): void { if (!map) return; this.isClimbing = true; this._climbStartGrid = map.getGridCellAtWorld(this.node.worldPosition); this._leftStairStart = false; Vec3.copy(this._climbStartWorld, this.node.worldPosition); } /** * 楼梯规则:未攀爬时仅允许在对应楼梯口起垂直移动;攀爬中禁止水平移动; * 离开起点后踩上带 isUpStairs/isDownStairs 的路点视为到达平台口,结束攀爬。 */ private resolveStairMoveDirection(raw: MoveDirection, point: SuDacePoint | null): MoveDirection { if (this.isClimbing) { if (raw === MoveDirection.LEFT || raw === MoveDirection.RIGHT) { return MoveDirection.NONE; } if (raw === MoveDirection.UP || raw === MoveDirection.DOWN) { return raw; } return MoveDirection.NONE; } if (raw === MoveDirection.LEFT || raw === MoveDirection.RIGHT) { return raw; } if (raw === MoveDirection.UP) { return point?.isUpStairs ? MoveDirection.UP : MoveDirection.NONE; } if (raw === MoveDirection.DOWN) { return point?.isDownStairs ? MoveDirection.DOWN : MoveDirection.NONE; } return MoveDirection.NONE; } private refreshStairLanding(map: ISuDaceGridMap | null | undefined): void { if (!this.isClimbing || !map) return; if (!this._leftStairStart && this._climbStartGrid) { const cell = map.getGridCellAtWorld(this.node.worldPosition); const dist = Vec3.distance(this.node.worldPosition, this._climbStartWorld); if ((cell && cell !== this._climbStartGrid) || dist > SuDacePlayer._STAIR_LEAVE_EPS) { this._leftStairStart = true; } } if (!this._leftStairStart) return; const p = map.getSuDacePointAtWorld(this.node.worldPosition); if (p && (p.isUpStairs || p.isDownStairs)) { this.finishStairClimbAtPoint(p); } } /** 结束爬梯时把角色吸附到梯口路点,避免停在楼板里/悬空 */ private finishStairClimbAtPoint(point: SuDacePoint): void { const z = this.node.worldPosition.z; this.node.setWorldPosition(point.node.worldPosition.x, point.node.worldPosition.y, z); this.isClimbing = false; this._climbStartGrid = null; this._leftStairStart = false; } /** 与 moveTo 一致:左右只动 X,上下只动 Y */ private stepAxisLockedWorldMove(dir: MoveDirection, dt: number): void { const p = this.node.worldPosition; const oldPos = this.node.worldPosition.clone(); const step = this.moveSpeed * dt * (1 + this.speedAdd); 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; } let grid = SuDace.instance.map.getGridCellAtWorld(this.node.worldPosition); if (grid) { let p = grid.getComponent(SuDacePoint); if (p && p.isWall) { this.node.setWorldPosition(oldPos); return; } if (p && p.isCliff) { tween(this.node).to(1, { position: v3(this.node.position.x, -641.689, 0) }).call(() => { this.hp = 0; this._dead = true; SuDace.instance?.onPlayerDefeated(); }).start(); } } } moveTo(wordPos: Vec3, direction: MoveDirection = null) { if (direction == null) { direction = this.getMoveDirection(wordPos); } this.armNode.angle = 0; let x = wordPos.x; let y = wordPos.y; let z = this.node.worldPosition.z; if (direction == MoveDirection.RIGHT || direction == MoveDirection.LEFT) { if (wordPos.y > this.node.worldPosition.y + 200) return; if (wordPos.y < this.node.worldPosition.y) return; y = this.node.worldPosition.y; } else if (direction == MoveDirection.UP || direction == MoveDirection.DOWN) { x = this.node.worldPosition.x; } Tween.stopAllByTarget(this.node); this.onMoveStatusChange(direction); this.targetPos.set(x, y, z); let distance = Vec3.distance(this.node.worldPosition, this.targetPos); let time = distance / this.moveSpeed; tween(this.node).to(time, { worldPosition: this.targetPos }).call(() => { this.onMoveStatusChange(MoveDirection.NONE); }).start(); } private targetPos: Vec3 = new Vec3(); atk() { if (this._curentStatus == MoveDirection.UP || this._curentStatus == MoveDirection.DOWN) { return; } let bullet = SuDace.instance.map.generateBullet(); const muzzlePos = this.bulletTemplate?.worldPosition ?? this.armNode.worldPosition; bullet.setWorldPosition(muzzlePos); const bulletComp = bullet.getComponent(SuDaceBullet); if (bulletComp) { bulletComp.damage = SuDace.instance.weaponAttack * (1 + this.atkAdd); } let direction = new Vec3(); let monster = this.getAtkTarget(); if (monster) { Vec3.subtract(direction, monster.worldPosition.clone().add(v3(0, 50, 0)), bullet.worldPosition); } else { // 无目标时按当前角色朝向发射 direction.set(this.node.scale.x >= 0 ? 1 : -1, 0, 0); } direction.normalize(); // 瞄准角(以 +X 方向为 0°):用于胳膊朝向 const aimAngle = Math.atan2(direction.y, direction.x) * 180 / Math.PI; // SuDaceBullet 内部按 (node.angle + 90°) 计算朝向,这里要减 90° 修正 bullet.angle = aimAngle - 90; // 同步胳膊角度(直接用瞄准角,避免与子弹修正角混用导致朝下) this.syncArmAngleWithAim(aimAngle); } private syncArmAngleWithAim(aimAngle: number): void { if (!this.armNode?.isValid) return; // armNode 处在会左右翻转的父层级下,朝左(scale.x<0)时需要把世界瞄准角转换为局部角 const faceRight = this.node.scale.x >= 0; const localAngle = faceRight ? aimAngle : 180 - aimAngle; this.armNode.angle = this.normalizeDeg(localAngle); } private normalizeDeg(angle: number): number { let a = angle % 360; if (a > 180) a -= 360; if (a < -180) a += 360; return a; } private getMoveDirection(targetPos: Vec3): MoveDirection { let direction = MoveDirection.NONE; if (targetPos.x > this.node.worldPosition.x) { direction = MoveDirection.RIGHT; } else if (targetPos.x < this.node.worldPosition.x) { direction = MoveDirection.LEFT; } return direction; } /**当移动状态发生变化时调用 */ onMoveStatusChange(status: MoveDirection) { if (this._curentStatus === status) return; this._curentStatus = status; this.bodyNode.active = false; this.upMoveNode.active = false; let spe = 0.1 * (1 - this.atkIntervalAdd); if (spe < 0.01) spe = 0.01; if (spe > 0.1) spe = 0.1; this.armNode.getComponent(ZClips).Duration = spe; let moveClips = this.legNode.getComponent(ZClips); moveClips.stop(); switch (status) { case MoveDirection.NONE: this.bodyNode.active = true; moveClips.play(false); break; case MoveDirection.RIGHT: this.bodyNode.active = true; moveClips.play(true); this.node.scale = v3(1, 1, 1); this.hpBar.node.scale = v3(1, 1, 1); this.lbDesc.node.scale = v3(1, 1, 1); break; case MoveDirection.LEFT: this.bodyNode.active = true; moveClips.play(true); this.node.scale = v3(-1, 1, 1); this.hpBar.node.scale = v3(-1, 1, 1); this.lbDesc.node.scale = v3(-1, 1, 1); break; case MoveDirection.UP: this.upMoveNode.active = true; break; case MoveDirection.DOWN: this.upMoveNode.active = true; break; } } private _curentStatus: MoveDirection = MoveDirection.NONE; private getAtkTarget() { return SuDace.instance.getNearestMonster(); } }