2025-09-25 14:57:01 +08:00
|
|
|
|
package uno.mloluyu.characters;
|
|
|
|
|
|
|
2025-09-25 18:22:28 +08:00
|
|
|
|
// 注意:本类使用的是包 uno.mloluyu.characters 下的 Action (IDLE, JUMP, MOVE, ATTACK, DEFEND, HIT, DEAD)
|
|
|
|
|
|
// 避免与 uno.mloluyu.characters.character.Action (ATTACK1/2/3...) 混淆
|
|
|
|
|
|
|
2025-09-25 14:57:01 +08:00
|
|
|
|
import com.badlogic.gdx.Gdx;
|
|
|
|
|
|
import com.badlogic.gdx.Input;
|
|
|
|
|
|
import com.badlogic.gdx.graphics.Color;
|
|
|
|
|
|
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
|
|
|
|
|
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
|
|
|
|
|
|
import com.badlogic.gdx.math.Rectangle;
|
2025-09-25 21:24:10 +08:00
|
|
|
|
import uno.mloluyu.network.NetworkManager;
|
2025-09-26 09:31:46 +08:00
|
|
|
|
import uno.mloluyu.util.GameConstants;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 简化版角色类,仅包含移动、攻击、受击等基础功能。
|
|
|
|
|
|
*/
|
2025-09-26 09:31:46 +08:00
|
|
|
|
public class SimpleFighter extends FighterBase {
|
2025-09-25 14:57:01 +08:00
|
|
|
|
|
2025-09-26 09:31:46 +08:00
|
|
|
|
// 继承: name, currentAction, hitbox, attackbox
|
2025-09-25 21:24:10 +08:00
|
|
|
|
private float verticalSpeed = 0f; // 垂直速度(跳跃/下落)
|
|
|
|
|
|
private boolean isGrounded = true; // 是否着地
|
|
|
|
|
|
private Rectangle hitbox = new Rectangle(0, 0, 64, 128); // 碰撞盒
|
|
|
|
|
|
private Rectangle attackbox = new Rectangle(0, 0, 80, 80); // 攻击判定盒
|
|
|
|
|
|
private boolean isFacingRight = true; // 朝向(右/左)
|
2025-09-27 15:02:52 +08:00
|
|
|
|
private float speed = GameConstants.MOVE_SPEED; // 目标最大水平移动速度
|
|
|
|
|
|
private float velX = 0f; // 当前水平速度(加入加速度与减速)
|
|
|
|
|
|
private static final int MAX_HEALTH = 200; // 最大生命值
|
|
|
|
|
|
private int health = MAX_HEALTH; // 当前生命值
|
|
|
|
|
|
private boolean isAttacking = false; // 是否正在攻击(包含 startup+active+recovery 整体)
|
|
|
|
|
|
private boolean attackJustStarted = false; // 本帧是否刚开始(用于跳过首帧计时扣减)
|
|
|
|
|
|
private float attackTimer = 0f; // 当前阶段剩余时间
|
|
|
|
|
|
// 分阶段(帧数可按类型定制):startup -> active -> recovery
|
|
|
|
|
|
|
|
|
|
|
|
private enum AttackPhase {
|
|
|
|
|
|
STARTUP, ACTIVE, RECOVERY
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private AttackPhase attackPhase = AttackPhase.STARTUP;
|
|
|
|
|
|
// 基础时长(可后续移出到配置)
|
|
|
|
|
|
// 降低攻击速度:整体各阶段放大(原值约 *1.6~2)
|
|
|
|
|
|
private static final float LIGHT_STARTUP = 0.10f, LIGHT_ACTIVE = 0.10f, LIGHT_RECOVERY = 0.22f;
|
|
|
|
|
|
private static final float HEAVY_STARTUP = 0.18f, HEAVY_ACTIVE = 0.16f, HEAVY_RECOVERY = 0.36f;
|
|
|
|
|
|
private static final float SPECIAL_STARTUP = 0.20f, SPECIAL_ACTIVE = 0.22f, SPECIAL_RECOVERY = 0.42f;
|
|
|
|
|
|
// 当前 attackType 的阶段时长(缓存便于递进)
|
|
|
|
|
|
private float curStartup, curActive, curRecovery;
|
2025-09-25 22:03:19 +08:00
|
|
|
|
// 新增:连续攻击序号(本地,用于避免重复伤害)
|
|
|
|
|
|
private int attackSequence = 0;
|
|
|
|
|
|
private String lastAttackType = "light"; // 记录最后一次攻击类型,供伤害判定
|
|
|
|
|
|
private int lastDamageAppliedSeq = -1; // 已经对目标造成伤害的序号,避免重复
|
|
|
|
|
|
// 击退 & 无敌
|
|
|
|
|
|
private float knockbackX = 0f;
|
|
|
|
|
|
private float knockbackTimer = 0f;
|
|
|
|
|
|
private float invulnerableTimer = 0f; // 无敌帧时间(被击中后短暂无敌)
|
|
|
|
|
|
private static final float INVULNERABLE_DURATION = 0.3f;
|
|
|
|
|
|
private static final float KNOCKBACK_DURATION = 0.12f;
|
2025-09-27 15:02:52 +08:00
|
|
|
|
// 死亡淡出
|
|
|
|
|
|
private float deathFadeTimer = 0f; // 剩余淡出时间(>0 表示正在淡出)
|
|
|
|
|
|
private static final float DEATH_FADE_DURATION = 1.2f; // 完全消失所需时间
|
|
|
|
|
|
// 防御新增字段
|
|
|
|
|
|
private boolean defending = false; // 是否防御中
|
|
|
|
|
|
private static final float DEFEND_DAMAGE_FACTOR = 0.25f; // 防御减伤比例
|
|
|
|
|
|
private static final float DEFEND_KNOCKBACK_FACTOR = 0.3f; // 防御击退比例
|
|
|
|
|
|
// 新增:攻击全局冷却(收招结束到允许下一次攻击的最短间隔)
|
|
|
|
|
|
private static final float GLOBAL_ATTACK_COOLDOWN = 0.12f;
|
|
|
|
|
|
private float globalAttackCDTimer = 0f;
|
2025-09-25 18:22:28 +08:00
|
|
|
|
|
|
|
|
|
|
public SimpleFighter(String name) {
|
2025-09-26 09:31:46 +08:00
|
|
|
|
super(name);
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void update(float deltaTime) {
|
2025-09-25 22:03:19 +08:00
|
|
|
|
// 处理无敌帧计时
|
|
|
|
|
|
if (invulnerableTimer > 0) {
|
|
|
|
|
|
invulnerableTimer -= deltaTime;
|
|
|
|
|
|
if (invulnerableTimer < 0)
|
|
|
|
|
|
invulnerableTimer = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 处理击退
|
|
|
|
|
|
if (knockbackTimer > 0) {
|
|
|
|
|
|
hitbox.x += knockbackX * deltaTime;
|
|
|
|
|
|
knockbackTimer -= deltaTime;
|
|
|
|
|
|
if (knockbackTimer <= 0) {
|
|
|
|
|
|
knockbackX = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-25 14:57:01 +08:00
|
|
|
|
if (isAttacking) {
|
2025-09-25 18:22:28 +08:00
|
|
|
|
if (attackJustStarted) {
|
2025-09-27 15:02:52 +08:00
|
|
|
|
attackJustStarted = false; // 第一帧不扣时间,避免可见阶段提前缩短
|
2025-09-25 18:22:28 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
attackTimer -= deltaTime;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (attackTimer <= 0f) {
|
2025-09-27 15:02:52 +08:00
|
|
|
|
// 进入下一阶段
|
|
|
|
|
|
switch (attackPhase) {
|
|
|
|
|
|
case STARTUP:
|
|
|
|
|
|
attackPhase = AttackPhase.ACTIVE;
|
|
|
|
|
|
attackTimer = curActive;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case ACTIVE:
|
|
|
|
|
|
attackPhase = AttackPhase.RECOVERY;
|
|
|
|
|
|
attackTimer = curRecovery;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case RECOVERY:
|
|
|
|
|
|
isAttacking = false;
|
|
|
|
|
|
attackTimer = 0f;
|
|
|
|
|
|
attackPhase = AttackPhase.STARTUP;
|
|
|
|
|
|
globalAttackCDTimer = GLOBAL_ATTACK_COOLDOWN; // 开始冷却
|
|
|
|
|
|
if (currentAction == Action.ATTACK)
|
|
|
|
|
|
changeAction(Action.IDLE);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2025-09-25 18:22:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2025-09-27 15:02:52 +08:00
|
|
|
|
updateAttackbox("light"); // 非攻击中保持一个默认攻击盒(或可隐藏)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 冷却计时
|
|
|
|
|
|
if (globalAttackCDTimer > 0f) {
|
|
|
|
|
|
globalAttackCDTimer -= deltaTime;
|
|
|
|
|
|
if (globalAttackCDTimer < 0f) globalAttackCDTimer = 0f;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
2025-09-25 16:50:43 +08:00
|
|
|
|
|
2025-09-25 14:57:01 +08:00
|
|
|
|
if (!isGrounded) {
|
2025-09-26 09:31:46 +08:00
|
|
|
|
verticalSpeed -= GameConstants.GRAVITY * deltaTime;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
hitbox.y += verticalSpeed * deltaTime;
|
2025-09-26 09:31:46 +08:00
|
|
|
|
if (hitbox.y <= GameConstants.GROUND_Y) {
|
|
|
|
|
|
hitbox.y = GameConstants.GROUND_Y;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
verticalSpeed = 0;
|
|
|
|
|
|
isGrounded = true;
|
|
|
|
|
|
changeAction(Action.IDLE);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-27 15:02:52 +08:00
|
|
|
|
// 死亡淡出计时递减
|
|
|
|
|
|
if (!isAlive() && deathFadeTimer > 0f) {
|
|
|
|
|
|
deathFadeTimer -= deltaTime;
|
|
|
|
|
|
if (deathFadeTimer < 0f)
|
|
|
|
|
|
deathFadeTimer = 0f;
|
|
|
|
|
|
}
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-25 18:22:28 +08:00
|
|
|
|
public void renderSprite(SpriteBatch batch) {
|
2025-09-25 21:24:10 +08:00
|
|
|
|
}
|
2025-09-25 14:57:01 +08:00
|
|
|
|
|
2025-09-25 18:22:28 +08:00
|
|
|
|
public void renderDebug(ShapeRenderer sr) {
|
|
|
|
|
|
sr.setColor(Color.BLUE);
|
|
|
|
|
|
sr.rect(hitbox.x, hitbox.y, hitbox.width, hitbox.height);
|
2025-09-25 14:57:01 +08:00
|
|
|
|
if (isAttacking) {
|
2025-09-25 18:22:28 +08:00
|
|
|
|
sr.setColor(Color.RED);
|
|
|
|
|
|
sr.rect(attackbox.x, attackbox.y, attackbox.width, attackbox.height);
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
2025-09-25 18:22:28 +08:00
|
|
|
|
float arrowX = isFacingRight ? hitbox.x + hitbox.width + 5 : hitbox.x - 15;
|
|
|
|
|
|
sr.setColor(Color.YELLOW);
|
|
|
|
|
|
sr.line(arrowX, hitbox.y + hitbox.height * 0.7f, arrowX + (isFacingRight ? 10 : -10),
|
|
|
|
|
|
hitbox.y + hitbox.height * 0.7f);
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-26 09:31:46 +08:00
|
|
|
|
public void handleInput(int keycode, boolean isPressed, float duration) {
|
2025-09-25 14:57:01 +08:00
|
|
|
|
if (isPressed) {
|
|
|
|
|
|
if (keycode == Input.Keys.LEFT || keycode == Input.Keys.A) {
|
2025-09-25 16:50:43 +08:00
|
|
|
|
move(-1, Gdx.graphics.getDeltaTime());
|
2025-09-25 14:57:01 +08:00
|
|
|
|
} else if (keycode == Input.Keys.RIGHT || keycode == Input.Keys.D) {
|
2025-09-25 16:50:43 +08:00
|
|
|
|
move(1, Gdx.graphics.getDeltaTime());
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
2025-09-25 18:22:28 +08:00
|
|
|
|
if (keycode == Input.Keys.SPACE || keycode == Input.Keys.UP || keycode == Input.Keys.W) {
|
|
|
|
|
|
jump();
|
|
|
|
|
|
}
|
2025-09-27 15:02:52 +08:00
|
|
|
|
if (!isAttacking && !defending && globalAttackCDTimer <= 0f) {
|
2025-09-25 16:50:43 +08:00
|
|
|
|
if (keycode == Input.Keys.Z || keycode == Input.Keys.J) {
|
|
|
|
|
|
attack("light");
|
2025-09-25 22:21:26 +08:00
|
|
|
|
NetworkManager.getInstance().sendAttack("light", getFacingDir());
|
2025-09-25 16:50:43 +08:00
|
|
|
|
} else if (keycode == Input.Keys.X || keycode == Input.Keys.K) {
|
|
|
|
|
|
attack("heavy");
|
2025-09-25 22:21:26 +08:00
|
|
|
|
NetworkManager.getInstance().sendAttack("heavy", getFacingDir());
|
2025-09-25 16:50:43 +08:00
|
|
|
|
} else if (keycode == Input.Keys.SHIFT_LEFT || keycode == Input.Keys.SHIFT_RIGHT) {
|
|
|
|
|
|
attack("special");
|
2025-09-25 22:21:26 +08:00
|
|
|
|
NetworkManager.getInstance().sendAttack("special", getFacingDir());
|
2025-09-25 16:50:43 +08:00
|
|
|
|
}
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
2025-09-27 15:02:52 +08:00
|
|
|
|
// 防御键(C / 左CTRL)
|
|
|
|
|
|
if (keycode == Input.Keys.C || keycode == Input.Keys.CONTROL_LEFT) {
|
|
|
|
|
|
if (!isAttacking && isAlive()) {
|
|
|
|
|
|
defending = true;
|
|
|
|
|
|
changeAction(Action.DEFEND);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-25 14:57:01 +08:00
|
|
|
|
} else {
|
2025-09-25 16:50:43 +08:00
|
|
|
|
if ((keycode == Input.Keys.LEFT || keycode == Input.Keys.RIGHT || keycode == Input.Keys.A
|
2025-09-25 21:24:10 +08:00
|
|
|
|
|| keycode == Input.Keys.D) && getCurrentAction() == Action.MOVE) {
|
2025-09-25 14:57:01 +08:00
|
|
|
|
changeAction(Action.IDLE);
|
|
|
|
|
|
}
|
2025-09-27 15:02:52 +08:00
|
|
|
|
if (keycode == Input.Keys.C || keycode == Input.Keys.CONTROL_LEFT) {
|
|
|
|
|
|
defending = false;
|
|
|
|
|
|
if (currentAction == Action.DEFEND)
|
|
|
|
|
|
changeAction(Action.IDLE);
|
|
|
|
|
|
}
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Action getCurrentAction() {
|
2025-09-25 21:24:10 +08:00
|
|
|
|
return currentAction;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void changeAction(Action newAction) {
|
2025-09-26 09:31:46 +08:00
|
|
|
|
this.currentAction = ActionStateGuard.transition(this, newAction);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void directSetAction(Action a) {
|
|
|
|
|
|
this.currentAction = a;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void jump() {
|
|
|
|
|
|
if (isGrounded) {
|
2025-09-26 09:31:46 +08:00
|
|
|
|
verticalSpeed = GameConstants.JUMP_SPEED;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
isGrounded = false;
|
|
|
|
|
|
changeAction(Action.JUMP);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void move(float x, float deltaTime) {
|
2025-09-27 15:02:52 +08:00
|
|
|
|
// x = -1,0,1 方向输入
|
|
|
|
|
|
float targetSign = x;
|
|
|
|
|
|
float accel = GameConstants.MOVE_ACCEL;
|
|
|
|
|
|
if (!isGrounded) {
|
|
|
|
|
|
accel *= GameConstants.AIR_ACCEL_FACTOR; // 空中降低加速度便于区分地面/空中控制
|
|
|
|
|
|
}
|
|
|
|
|
|
if (targetSign != 0) {
|
|
|
|
|
|
isFacingRight = targetSign > 0;
|
|
|
|
|
|
// 向目标速度加速
|
|
|
|
|
|
float targetVel = targetSign * speed;
|
|
|
|
|
|
if (velX < targetVel) {
|
|
|
|
|
|
velX = Math.min(targetVel, velX + accel * deltaTime);
|
|
|
|
|
|
} else if (velX > targetVel) {
|
|
|
|
|
|
velX = Math.max(targetVel, velX - accel * deltaTime);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 无输入:减速(朝 0 逼近)
|
|
|
|
|
|
float decel = GameConstants.MOVE_DECEL * deltaTime;
|
|
|
|
|
|
if (velX > 0) {
|
|
|
|
|
|
velX = Math.max(0, velX - decel);
|
|
|
|
|
|
} else if (velX < 0) {
|
|
|
|
|
|
velX = Math.min(0, velX + decel);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 应用位移
|
|
|
|
|
|
hitbox.x += velX * deltaTime;
|
|
|
|
|
|
// 状态切换
|
|
|
|
|
|
if (Math.abs(velX) > 5f && isGrounded && !isAttacking) {
|
2025-09-25 21:24:10 +08:00
|
|
|
|
changeAction(Action.MOVE);
|
2025-09-27 15:02:52 +08:00
|
|
|
|
} else if (isGrounded && !isAttacking && targetSign == 0 && Math.abs(velX) <= 5f) {
|
|
|
|
|
|
velX = 0f; // 近乎停止直接归零
|
2025-09-25 21:24:10 +08:00
|
|
|
|
changeAction(Action.IDLE);
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-25 22:03:19 +08:00
|
|
|
|
// 供远程同步:根据位置变化量更新朝向(不触发移动动作,只用于攻击盒朝向正确)
|
|
|
|
|
|
public void updateFacingByDelta(float dx) {
|
|
|
|
|
|
if (dx > 0) {
|
|
|
|
|
|
isFacingRight = true;
|
|
|
|
|
|
} else if (dx < 0) {
|
|
|
|
|
|
isFacingRight = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public boolean isFacingRight() {
|
|
|
|
|
|
return isFacingRight;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-25 22:21:26 +08:00
|
|
|
|
public void setFacingRight(boolean facingRight) {
|
|
|
|
|
|
this.isFacingRight = facingRight;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public String getFacingDir() {
|
|
|
|
|
|
return isFacingRight ? "R" : "L";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-25 16:50:43 +08:00
|
|
|
|
private void updateAttackbox(String attackType) {
|
2025-09-26 09:31:46 +08:00
|
|
|
|
float baseOffsetY = 20f;
|
|
|
|
|
|
float width = 80f, height = 80f;
|
|
|
|
|
|
float offsetX;
|
|
|
|
|
|
float offsetY = baseOffsetY;
|
|
|
|
|
|
// 先决定尺寸,再根据朝向计算 offset(不再用旧 attackbox.width 避免漂移)
|
2025-09-25 16:50:43 +08:00
|
|
|
|
switch (attackType) {
|
|
|
|
|
|
case "heavy":
|
2025-09-26 09:31:46 +08:00
|
|
|
|
width = 100f;
|
|
|
|
|
|
height = 100f;
|
|
|
|
|
|
offsetY = 40f;
|
|
|
|
|
|
offsetX = isFacingRight ? hitbox.width : -width; // 重击靠近身体或覆盖前方
|
2025-09-25 16:50:43 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case "special":
|
2025-09-26 09:31:46 +08:00
|
|
|
|
width = 120f;
|
|
|
|
|
|
height = 60f;
|
|
|
|
|
|
offsetY = 50f;
|
|
|
|
|
|
offsetX = isFacingRight ? hitbox.width + 20f : -width - 20f;
|
2025-09-25 16:50:43 +08:00
|
|
|
|
break;
|
2025-09-26 09:31:46 +08:00
|
|
|
|
case "light":
|
2025-09-25 16:50:43 +08:00
|
|
|
|
default:
|
2025-09-26 09:31:46 +08:00
|
|
|
|
// 轻击稍微往前,不再参考旧 attackbox.width
|
|
|
|
|
|
offsetX = isFacingRight ? hitbox.width - 10f : -80f + 10f;
|
|
|
|
|
|
break;
|
2025-09-25 16:50:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
attackbox.setSize(width, height);
|
2025-09-26 09:31:46 +08:00
|
|
|
|
attackbox.setPosition(hitbox.x + offsetX, hitbox.y + offsetY);
|
2025-09-25 16:50:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-25 14:57:01 +08:00
|
|
|
|
public void attack(String attackType) {
|
2025-09-25 16:50:43 +08:00
|
|
|
|
isAttacking = true;
|
2025-09-27 15:02:52 +08:00
|
|
|
|
attackPhase = AttackPhase.STARTUP;
|
|
|
|
|
|
switch (attackType) {
|
|
|
|
|
|
case "heavy":
|
|
|
|
|
|
curStartup = HEAVY_STARTUP;
|
|
|
|
|
|
curActive = HEAVY_ACTIVE;
|
|
|
|
|
|
curRecovery = HEAVY_RECOVERY;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "special":
|
|
|
|
|
|
curStartup = SPECIAL_STARTUP;
|
|
|
|
|
|
curActive = SPECIAL_ACTIVE;
|
|
|
|
|
|
curRecovery = SPECIAL_RECOVERY;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "light":
|
|
|
|
|
|
default:
|
|
|
|
|
|
curStartup = LIGHT_STARTUP;
|
|
|
|
|
|
curActive = LIGHT_ACTIVE;
|
|
|
|
|
|
curRecovery = LIGHT_RECOVERY;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
attackTimer = curStartup;
|
2025-09-25 18:22:28 +08:00
|
|
|
|
attackJustStarted = true;
|
2025-09-25 16:50:43 +08:00
|
|
|
|
changeAction(Action.ATTACK);
|
2025-09-25 18:22:28 +08:00
|
|
|
|
updateAttackbox(attackType);
|
2025-09-25 22:03:19 +08:00
|
|
|
|
lastAttackType = attackType;
|
2025-09-27 15:02:52 +08:00
|
|
|
|
attackSequence++; // 本地每次攻击序号自增
|
|
|
|
|
|
lastDamageAppliedSeq = -1; // 重置伤害标记
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void takeHit(int damage) {
|
2025-09-25 22:21:26 +08:00
|
|
|
|
takeHit(damage, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void takeHit(int damage, int dirSign) {
|
2025-09-25 22:03:19 +08:00
|
|
|
|
if (invulnerableTimer > 0 || health <= 0)
|
2025-09-27 15:02:52 +08:00
|
|
|
|
return;
|
|
|
|
|
|
int finalDamage = damage;
|
|
|
|
|
|
boolean wasDefending = defending && currentAction == Action.DEFEND;
|
|
|
|
|
|
if (wasDefending) {
|
|
|
|
|
|
finalDamage = Math.max(1, Math.round(damage * DEFEND_DAMAGE_FACTOR));
|
|
|
|
|
|
}
|
|
|
|
|
|
health = Math.max(0, health - finalDamage);
|
|
|
|
|
|
if (!(wasDefending && health > 0)) {
|
|
|
|
|
|
changeAction(health > 0 ? Action.HIT : Action.DEAD);
|
|
|
|
|
|
}
|
2025-09-25 22:03:19 +08:00
|
|
|
|
invulnerableTimer = INVULNERABLE_DURATION;
|
2025-09-27 15:02:52 +08:00
|
|
|
|
float baseKb = 600f;
|
|
|
|
|
|
if (wasDefending)
|
|
|
|
|
|
baseKb *= DEFEND_KNOCKBACK_FACTOR;
|
|
|
|
|
|
if (dirSign == 0) {
|
|
|
|
|
|
knockbackX = isFacingRight ? -baseKb : baseKb;
|
2025-09-25 22:21:26 +08:00
|
|
|
|
} else {
|
2025-09-27 15:02:52 +08:00
|
|
|
|
knockbackX = dirSign * baseKb;
|
2025-09-25 22:21:26 +08:00
|
|
|
|
}
|
2025-09-25 22:03:19 +08:00
|
|
|
|
knockbackTimer = KNOCKBACK_DURATION;
|
2025-09-27 15:02:52 +08:00
|
|
|
|
if (health == 0)
|
|
|
|
|
|
deathFadeTimer = DEATH_FADE_DURATION;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public boolean isAlive() {
|
2025-09-25 21:24:10 +08:00
|
|
|
|
return health > 0;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public boolean isAttacking() {
|
2025-09-25 21:24:10 +08:00
|
|
|
|
return isAttacking;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-25 22:03:19 +08:00
|
|
|
|
public boolean isInvulnerable() {
|
|
|
|
|
|
return invulnerableTimer > 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-25 14:57:01 +08:00
|
|
|
|
public Rectangle getHitbox() {
|
2025-09-25 21:24:10 +08:00
|
|
|
|
return hitbox;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Rectangle getAttackbox() {
|
2025-09-25 21:24:10 +08:00
|
|
|
|
return attackbox;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int getHealth() {
|
2025-09-25 21:24:10 +08:00
|
|
|
|
return health;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-27 15:02:52 +08:00
|
|
|
|
public int getMaxHealth() {
|
|
|
|
|
|
return MAX_HEALTH;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-25 22:03:19 +08:00
|
|
|
|
public int getAttackSequence() {
|
|
|
|
|
|
return attackSequence;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public String getLastAttackType() {
|
|
|
|
|
|
return lastAttackType;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int getLastDamageAppliedSeq() {
|
|
|
|
|
|
return lastDamageAppliedSeq;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void setLastDamageAppliedSeq(int seq) {
|
|
|
|
|
|
this.lastDamageAppliedSeq = seq;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public boolean canDealDamage() {
|
2025-09-27 15:02:52 +08:00
|
|
|
|
// 只有 ACTIVE 阶段且本序号未造成过伤害才允许
|
|
|
|
|
|
return isAttacking && attackPhase == AttackPhase.ACTIVE && attackSequence != lastDamageAppliedSeq;
|
2025-09-25 22:03:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void markDamageApplied() {
|
|
|
|
|
|
lastDamageAppliedSeq = attackSequence;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据攻击类型返回伤害数值
|
|
|
|
|
|
public int getDamageForAttack(String type) {
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
|
case "heavy":
|
|
|
|
|
|
return 20;
|
|
|
|
|
|
case "special":
|
|
|
|
|
|
return 30;
|
|
|
|
|
|
case "light":
|
|
|
|
|
|
default:
|
|
|
|
|
|
return 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-25 14:57:01 +08:00
|
|
|
|
public String getName() {
|
2025-09-25 21:24:10 +08:00
|
|
|
|
return name;
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void setPosition(float x, float y) {
|
2025-09-25 21:24:10 +08:00
|
|
|
|
hitbox.setPosition(x, y);
|
2025-09-25 18:22:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-26 09:31:46 +08:00
|
|
|
|
/** 若当前 Y 低于地面则贴到地面。 */
|
|
|
|
|
|
public void alignToGround() {
|
|
|
|
|
|
if (hitbox.y < GameConstants.GROUND_Y) {
|
|
|
|
|
|
hitbox.y = GameConstants.GROUND_Y;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-25 18:22:28 +08:00
|
|
|
|
public float getAttackTimer() {
|
|
|
|
|
|
return attackTimer;
|
|
|
|
|
|
}
|
2025-09-25 22:21:26 +08:00
|
|
|
|
|
2025-09-27 15:02:52 +08:00
|
|
|
|
public boolean isInActivePhase() {
|
|
|
|
|
|
return isAttacking && attackPhase == AttackPhase.ACTIVE;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public boolean isInStartupPhase() {
|
|
|
|
|
|
return isAttacking && attackPhase == AttackPhase.STARTUP;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public boolean isInRecoveryPhase() {
|
|
|
|
|
|
return isAttacking && attackPhase == AttackPhase.RECOVERY;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-25 22:21:26 +08:00
|
|
|
|
// 重生时重置状态
|
|
|
|
|
|
public void resetForRespawn() {
|
2025-09-27 15:02:52 +08:00
|
|
|
|
health = MAX_HEALTH;
|
2025-09-25 22:21:26 +08:00
|
|
|
|
isAttacking = false;
|
|
|
|
|
|
attackTimer = 0f;
|
|
|
|
|
|
attackJustStarted = false;
|
|
|
|
|
|
changeAction(Action.IDLE);
|
|
|
|
|
|
invulnerableTimer = 0f;
|
|
|
|
|
|
knockbackTimer = 0f;
|
|
|
|
|
|
knockbackX = 0f;
|
2025-09-27 15:02:52 +08:00
|
|
|
|
deathFadeTimer = 0f; // 重置淡出
|
|
|
|
|
|
defending = false;
|
|
|
|
|
|
velX = 0f;
|
|
|
|
|
|
globalAttackCDTimer = 0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 获取当前用于渲染的死亡淡出透明度(1=不透明,0=已完全淡出)。 */
|
|
|
|
|
|
public float getRenderAlpha() {
|
|
|
|
|
|
if (health > 0)
|
|
|
|
|
|
return 1f;
|
|
|
|
|
|
return deathFadeTimer / DEATH_FADE_DURATION; // 线性
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public boolean isDefending() {
|
|
|
|
|
|
return defending && currentAction == Action.DEFEND;
|
2025-09-25 22:21:26 +08:00
|
|
|
|
}
|
2025-09-25 14:57:01 +08:00
|
|
|
|
}
|