Files
Game/src/main/java/uno/mloluyu/characters/SimpleFighter.java

490 lines
17 KiB
Java
Raw Normal View History

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;
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
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 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
|| 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() {
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) {
changeAction(Action.MOVE);
2025-09-27 15:02:52 +08:00
} else if (isGrounded && !isAttacking && targetSign == 0 && Math.abs(velX) <= 5f) {
velX = 0f; // 近乎停止直接归零
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() {
return health > 0;
2025-09-25 14:57:01 +08:00
}
public boolean isAttacking() {
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() {
return hitbox;
2025-09-25 14:57:01 +08:00
}
public Rectangle getAttackbox() {
return attackbox;
2025-09-25 14:57:01 +08:00
}
public int getHealth() {
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() {
return name;
2025-09-25 14:57:01 +08:00
}
public void setPosition(float x, float y) {
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
}