消消方块阵换皮表情
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.
 
 
 

487 lines
17 KiB

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<Vec3>): Node | null;
getSuDacePointAtWorld(worldPos: Readonly<Vec3>): 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();
}
}