Files
Game/src/main/java/uno/mloluyu/characters/SimpleFighter.java
2025-09-27 15:02:52 +08:00

490 lines
17 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package uno.mloluyu.characters;
// 注意:本类使用的是包 uno.mloluyu.characters 下的 Action (IDLE, JUMP, MOVE, ATTACK, DEFEND, HIT, DEAD)
// 避免与 uno.mloluyu.characters.character.Action (ATTACK1/2/3...) 混淆
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;
import uno.mloluyu.util.GameConstants;
/**
* 简化版角色类,仅包含移动、攻击、受击等基础功能。
*/
public class SimpleFighter extends FighterBase {
// 继承: 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; // 朝向(右/左)
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;
// 新增:连续攻击序号(本地,用于避免重复伤害)
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;
// 死亡淡出
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;
public SimpleFighter(String name) {
super(name);
}
public void update(float deltaTime) {
// 处理无敌帧计时
if (invulnerableTimer > 0) {
invulnerableTimer -= deltaTime;
if (invulnerableTimer < 0)
invulnerableTimer = 0;
}
// 处理击退
if (knockbackTimer > 0) {
hitbox.x += knockbackX * deltaTime;
knockbackTimer -= deltaTime;
if (knockbackTimer <= 0) {
knockbackX = 0;
}
}
if (isAttacking) {
if (attackJustStarted) {
attackJustStarted = false; // 第一帧不扣时间,避免可见阶段提前缩短
} else {
attackTimer -= deltaTime;
}
if (attackTimer <= 0f) {
// 进入下一阶段
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;
}
}
} else {
updateAttackbox("light"); // 非攻击中保持一个默认攻击盒(或可隐藏)
}
// 冷却计时
if (globalAttackCDTimer > 0f) {
globalAttackCDTimer -= deltaTime;
if (globalAttackCDTimer < 0f) globalAttackCDTimer = 0f;
}
if (!isGrounded) {
verticalSpeed -= GameConstants.GRAVITY * deltaTime;
hitbox.y += verticalSpeed * deltaTime;
if (hitbox.y <= GameConstants.GROUND_Y) {
hitbox.y = GameConstants.GROUND_Y;
verticalSpeed = 0;
isGrounded = true;
changeAction(Action.IDLE);
}
}
// 死亡淡出计时递减
if (!isAlive() && deathFadeTimer > 0f) {
deathFadeTimer -= deltaTime;
if (deathFadeTimer < 0f)
deathFadeTimer = 0f;
}
}
public void renderSprite(SpriteBatch batch) {
}
public void renderDebug(ShapeRenderer sr) {
sr.setColor(Color.BLUE);
sr.rect(hitbox.x, hitbox.y, hitbox.width, hitbox.height);
if (isAttacking) {
sr.setColor(Color.RED);
sr.rect(attackbox.x, attackbox.y, attackbox.width, attackbox.height);
}
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);
}
public void handleInput(int keycode, boolean isPressed, float duration) {
if (isPressed) {
if (keycode == Input.Keys.LEFT || keycode == Input.Keys.A) {
move(-1, Gdx.graphics.getDeltaTime());
} else if (keycode == Input.Keys.RIGHT || keycode == Input.Keys.D) {
move(1, Gdx.graphics.getDeltaTime());
}
if (keycode == Input.Keys.SPACE || keycode == Input.Keys.UP || keycode == Input.Keys.W) {
jump();
}
if (!isAttacking && !defending && globalAttackCDTimer <= 0f) {
if (keycode == Input.Keys.Z || keycode == Input.Keys.J) {
attack("light");
NetworkManager.getInstance().sendAttack("light", getFacingDir());
} else if (keycode == Input.Keys.X || keycode == Input.Keys.K) {
attack("heavy");
NetworkManager.getInstance().sendAttack("heavy", getFacingDir());
} else if (keycode == Input.Keys.SHIFT_LEFT || keycode == Input.Keys.SHIFT_RIGHT) {
attack("special");
NetworkManager.getInstance().sendAttack("special", getFacingDir());
}
}
// 防御键C / 左CTRL
if (keycode == Input.Keys.C || keycode == Input.Keys.CONTROL_LEFT) {
if (!isAttacking && isAlive()) {
defending = true;
changeAction(Action.DEFEND);
}
}
} else {
if ((keycode == Input.Keys.LEFT || keycode == Input.Keys.RIGHT || keycode == Input.Keys.A
|| keycode == Input.Keys.D) && getCurrentAction() == Action.MOVE) {
changeAction(Action.IDLE);
}
if (keycode == Input.Keys.C || keycode == Input.Keys.CONTROL_LEFT) {
defending = false;
if (currentAction == Action.DEFEND)
changeAction(Action.IDLE);
}
}
}
public Action getCurrentAction() {
return currentAction;
}
public void changeAction(Action newAction) {
this.currentAction = ActionStateGuard.transition(this, newAction);
}
void directSetAction(Action a) {
this.currentAction = a;
}
public void jump() {
if (isGrounded) {
verticalSpeed = GameConstants.JUMP_SPEED;
isGrounded = false;
changeAction(Action.JUMP);
}
}
public void move(float x, float deltaTime) {
// 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);
} else if (isGrounded && !isAttacking && targetSign == 0 && Math.abs(velX) <= 5f) {
velX = 0f; // 近乎停止直接归零
changeAction(Action.IDLE);
}
}
// 供远程同步:根据位置变化量更新朝向(不触发移动动作,只用于攻击盒朝向正确)
public void updateFacingByDelta(float dx) {
if (dx > 0) {
isFacingRight = true;
} else if (dx < 0) {
isFacingRight = false;
}
}
public boolean isFacingRight() {
return isFacingRight;
}
public void setFacingRight(boolean facingRight) {
this.isFacingRight = facingRight;
}
public String getFacingDir() {
return isFacingRight ? "R" : "L";
}
private void updateAttackbox(String attackType) {
float baseOffsetY = 20f;
float width = 80f, height = 80f;
float offsetX;
float offsetY = baseOffsetY;
// 先决定尺寸,再根据朝向计算 offset不再用旧 attackbox.width 避免漂移)
switch (attackType) {
case "heavy":
width = 100f;
height = 100f;
offsetY = 40f;
offsetX = isFacingRight ? hitbox.width : -width; // 重击靠近身体或覆盖前方
break;
case "special":
width = 120f;
height = 60f;
offsetY = 50f;
offsetX = isFacingRight ? hitbox.width + 20f : -width - 20f;
break;
case "light":
default:
// 轻击稍微往前,不再参考旧 attackbox.width
offsetX = isFacingRight ? hitbox.width - 10f : -80f + 10f;
break;
}
attackbox.setSize(width, height);
attackbox.setPosition(hitbox.x + offsetX, hitbox.y + offsetY);
}
public void attack(String attackType) {
isAttacking = true;
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;
attackJustStarted = true;
changeAction(Action.ATTACK);
updateAttackbox(attackType);
lastAttackType = attackType;
attackSequence++; // 本地每次攻击序号自增
lastDamageAppliedSeq = -1; // 重置伤害标记
}
public void takeHit(int damage) {
takeHit(damage, 0);
}
public void takeHit(int damage, int dirSign) {
if (invulnerableTimer > 0 || health <= 0)
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);
}
invulnerableTimer = INVULNERABLE_DURATION;
float baseKb = 600f;
if (wasDefending)
baseKb *= DEFEND_KNOCKBACK_FACTOR;
if (dirSign == 0) {
knockbackX = isFacingRight ? -baseKb : baseKb;
} else {
knockbackX = dirSign * baseKb;
}
knockbackTimer = KNOCKBACK_DURATION;
if (health == 0)
deathFadeTimer = DEATH_FADE_DURATION;
}
public boolean isAlive() {
return health > 0;
}
public boolean isAttacking() {
return isAttacking;
}
public boolean isInvulnerable() {
return invulnerableTimer > 0;
}
public Rectangle getHitbox() {
return hitbox;
}
public Rectangle getAttackbox() {
return attackbox;
}
public int getHealth() {
return health;
}
public int getMaxHealth() {
return MAX_HEALTH;
}
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() {
// 只有 ACTIVE 阶段且本序号未造成过伤害才允许
return isAttacking && attackPhase == AttackPhase.ACTIVE && attackSequence != lastDamageAppliedSeq;
}
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;
}
}
public String getName() {
return name;
}
public void setPosition(float x, float y) {
hitbox.setPosition(x, y);
}
/** 若当前 Y 低于地面则贴到地面。 */
public void alignToGround() {
if (hitbox.y < GameConstants.GROUND_Y) {
hitbox.y = GameConstants.GROUND_Y;
}
}
public float getAttackTimer() {
return attackTimer;
}
public boolean isInActivePhase() {
return isAttacking && attackPhase == AttackPhase.ACTIVE;
}
public boolean isInStartupPhase() {
return isAttacking && attackPhase == AttackPhase.STARTUP;
}
public boolean isInRecoveryPhase() {
return isAttacking && attackPhase == AttackPhase.RECOVERY;
}
// 重生时重置状态
public void resetForRespawn() {
health = MAX_HEALTH;
isAttacking = false;
attackTimer = 0f;
attackJustStarted = false;
changeAction(Action.IDLE);
invulnerableTimer = 0f;
knockbackTimer = 0f;
knockbackX = 0f;
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;
}
}