diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md
deleted file mode 100644
index e69de29..0000000
diff --git a/Game/.gitignore b/Game/.gitignore
deleted file mode 100644
index 8d9590a..0000000
Binary files a/Game/.gitignore and /dev/null differ
diff --git a/Game/README.md b/Game/README.md
deleted file mode 100644
index b9a2ea2..0000000
--- a/Game/README.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# 有关本项目的说明
-
-本项目为AVCEIT2025届软件技术专业软件241班Java实训小组项目
-This project is about Java Project Training 2025, Major.Software Technology
-
-### 本项目目前需要做的
-
-- 实现游戏主界面,添加素材
-- 完成游戏的核心功能,即双人格斗
-- 实现基本的局域网内联机游戏功能
-- *~~打则死路一条~~*
-
-### 目前正在做的
-
-- 编写单个角色并实现其状态 (@wsj)
-- 将游戏素材整理为精灵图 (@wsj)
-- 给游戏进行界面类添加BUG (@mloluyu)
-- 给标题界面类添加BUG (@mloluyu)
-- 寻找一个合适的数据处理类编写方法
-- *~~丰矿地打则~~*
-
-### 参与项目的人
-
-- 武术家 [@wsj](varia.mloluyu.uno/wsj)
-- 大货车 [@mloluyu](varia.mloluyu.uno/mloluyu)
diff --git a/Game/pom.xml b/Game/pom.xml
deleted file mode 100644
index 7266d29..0000000
--- a/Game/pom.xml
+++ /dev/null
@@ -1,114 +0,0 @@
-
-
-
- 4.0.0
-
- uno.mloluyu
-
- game
-
- 1.0-SNAPSHOT
-
- jar
-
-
- 21
- 21
- UTF-8
- 1.12.1
-
-
-
-
-
- com.badlogicgames.gdx
- gdx
- 1.12.1
-
-
-
-
- com.badlogicgames.gdx
- gdx-freetype
- 1.12.1
-
-
-
-
- com.badlogicgames.gdx
- gdx-freetype-platform
- 1.12.1
- natives-desktop
-
-
- com.badlogicgames.gdx
- gdx-backend-lwjgl
- 1.12.1
-
-
- com.badlogicgames.gdx
- gdx-platform
- 1.12.1
- natives-desktop
-
-
- com.badlogicgames.gdx
- gdx
- ${gdx.version}
-
-
- com.badlogicgames.gdx
- gdx-backend-lwjgl3
- ${gdx.version}
-
-
- com.badlogicgames.gdx
- gdx-platform
- ${gdx.version}
- natives-desktop
-
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.13.0
-
-
- 17
- 17
-
-
-
-
-
- org.codehaus.mojo
- exec-maven-plugin
- 3.1.0
-
-
-
- uno.mloluyu.desktop.Launcher
-
-
-
-
-
- org.apache.maven.plugins
- maven-jar-plugin
- 3.3.0
-
-
-
- true
- uno.mloluyu.desktop.Launcher
-
-
-
-
-
-
-
-
-
diff --git a/docs/perf_hotspots.md b/docs/perf_hotspots.md
deleted file mode 100644
index 984dffb..0000000
--- a/docs/perf_hotspots.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# 性能热点初稿
-
-首轮标记:
-
-| 区域 | 说明 | 优先级 | 说明 |
-|------|------|--------|------|
-GameScreen.render | 多次集合遍历 + 远程玩家循环内更新 | 高 | 后续拆分逻辑/渲染阶段 |
-SimpleFighter.update | 物理与攻击状态混杂 | 中 | 拆分到组件式(动作/物理) |
-网络同步(待补) | sendPosition 每帧发送 | 中 | 引入位置压缩/频率限制 |
-清屏/批处理 | 现已静态化 | 已改善 | 继续合并渲染批次 |
-
-后续收集: 帧时间分布 / GC 次数。
diff --git a/pom.xml b/pom.xml
index 4036ec1..52f6a31 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,12 @@
5.10.2
test
+
+
+ com.badlogicgames.gdx
+ gdx-ai
+ 1.8.2
+
diff --git a/src/main/java/uno/mloluyu/characters/SimpleFighter.java b/src/main/java/uno/mloluyu/characters/SimpleFighter.java
index d010d6a..662252a 100644
--- a/src/main/java/uno/mloluyu/characters/SimpleFighter.java
+++ b/src/main/java/uno/mloluyu/characters/SimpleFighter.java
@@ -23,12 +23,27 @@ public class SimpleFighter extends FighterBase {
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 int health = 200; // 生命值
- private boolean isAttacking = false; // 是否正在攻击
- private boolean attackJustStarted = false; // 攻击是否刚开始
- private float attackTimer = 0f; // 攻击计时器
- private static final float ATTACK_DURATION = 0.15f; // 攻击持续时间
+ 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"; // 记录最后一次攻击类型,供伤害判定
@@ -39,6 +54,16 @@ public class SimpleFighter extends FighterBase {
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);
@@ -61,18 +86,39 @@ public class SimpleFighter extends FighterBase {
}
if (isAttacking) {
if (attackJustStarted) {
- attackJustStarted = false;
+ attackJustStarted = false; // 第一帧不扣时间,避免可见阶段提前缩短
} else {
attackTimer -= deltaTime;
}
if (attackTimer <= 0f) {
- isAttacking = false;
- attackTimer = 0f;
- if (currentAction == Action.ATTACK)
- changeAction(Action.IDLE);
+ // 进入下一阶段
+ 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");
+ updateAttackbox("light"); // 非攻击中保持一个默认攻击盒(或可隐藏)
+ }
+
+ // 冷却计时
+ if (globalAttackCDTimer > 0f) {
+ globalAttackCDTimer -= deltaTime;
+ if (globalAttackCDTimer < 0f) globalAttackCDTimer = 0f;
}
if (!isGrounded) {
@@ -85,6 +131,12 @@ public class SimpleFighter extends FighterBase {
changeAction(Action.IDLE);
}
}
+ // 死亡淡出计时递减
+ if (!isAlive() && deathFadeTimer > 0f) {
+ deathFadeTimer -= deltaTime;
+ if (deathFadeTimer < 0f)
+ deathFadeTimer = 0f;
+ }
}
public void renderSprite(SpriteBatch batch) {
@@ -113,7 +165,7 @@ public class SimpleFighter extends FighterBase {
if (keycode == Input.Keys.SPACE || keycode == Input.Keys.UP || keycode == Input.Keys.W) {
jump();
}
- if (!isAttacking) {
+ if (!isAttacking && !defending && globalAttackCDTimer <= 0f) {
if (keycode == Input.Keys.Z || keycode == Input.Keys.J) {
attack("light");
NetworkManager.getInstance().sendAttack("light", getFacingDir());
@@ -125,11 +177,23 @@ public class SimpleFighter extends FighterBase {
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);
+ }
}
}
@@ -154,11 +218,37 @@ public class SimpleFighter extends FighterBase {
}
public void move(float x, float deltaTime) {
- if (x != 0) {
- isFacingRight = x > 0;
- hitbox.x += x * speed * 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) {
+ } else if (isGrounded && !isAttacking && targetSign == 0 && Math.abs(velX) <= 5f) {
+ velX = 0f; // 近乎停止直接归零
changeAction(Action.IDLE);
}
}
@@ -215,13 +305,32 @@ public class SimpleFighter extends FighterBase {
public void attack(String attackType) {
isAttacking = true;
- attackTimer = ATTACK_DURATION;
+ 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; // 新一次攻击重置
+ attackSequence++; // 本地每次攻击序号自增
+ lastDamageAppliedSeq = -1; // 重置伤害标记
}
public void takeHit(int damage) {
@@ -230,17 +339,28 @@ public class SimpleFighter extends FighterBase {
public void takeHit(int damage, int dirSign) {
if (invulnerableTimer > 0 || health <= 0)
- return; // 无敌或已死亡
- health = Math.max(0, health - damage);
- changeAction(health > 0 ? Action.HIT : Action.DEAD);
+ 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;
- // dirSign: -1 表示从右向左击中(目标向左被推), 1 表示从左向右击中(目标向右被推)
- if (dirSign == 0) { // 没有提供方向则沿用基于自身面向的旧逻辑
- knockbackX = isFacingRight ? -600f : 600f;
+ float baseKb = 600f;
+ if (wasDefending)
+ baseKb *= DEFEND_KNOCKBACK_FACTOR;
+ if (dirSign == 0) {
+ knockbackX = isFacingRight ? -baseKb : baseKb;
} else {
- knockbackX = dirSign * 600f;
+ knockbackX = dirSign * baseKb;
}
knockbackTimer = KNOCKBACK_DURATION;
+ if (health == 0)
+ deathFadeTimer = DEATH_FADE_DURATION;
}
public boolean isAlive() {
@@ -267,6 +387,10 @@ public class SimpleFighter extends FighterBase {
return health;
}
+ public int getMaxHealth() {
+ return MAX_HEALTH;
+ }
+
public int getAttackSequence() {
return attackSequence;
}
@@ -284,7 +408,8 @@ public class SimpleFighter extends FighterBase {
}
public boolean canDealDamage() {
- return isAttacking && attackSequence != lastDamageAppliedSeq; // 未对当前序号造成过伤害
+ // 只有 ACTIVE 阶段且本序号未造成过伤害才允许
+ return isAttacking && attackPhase == AttackPhase.ACTIVE && attackSequence != lastDamageAppliedSeq;
}
public void markDamageApplied() {
@@ -323,9 +448,21 @@ public class SimpleFighter extends FighterBase {
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 = 100;
+ health = MAX_HEALTH;
isAttacking = false;
attackTimer = 0f;
attackJustStarted = false;
@@ -333,5 +470,20 @@ public class SimpleFighter extends FighterBase {
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;
}
}
diff --git a/src/main/java/uno/mloluyu/characters/ai/SimpleFighterAI.java b/src/main/java/uno/mloluyu/characters/ai/SimpleFighterAI.java
new file mode 100644
index 0000000..daf409e
--- /dev/null
+++ b/src/main/java/uno/mloluyu/characters/ai/SimpleFighterAI.java
@@ -0,0 +1,158 @@
+package uno.mloluyu.characters.ai;
+
+import com.badlogic.gdx.ai.fsm.DefaultStateMachine;
+import com.badlogic.gdx.ai.fsm.State;
+import com.badlogic.gdx.ai.fsm.StateMachine;
+import com.badlogic.gdx.ai.msg.Telegram;
+import com.badlogic.gdx.math.MathUtils;
+import uno.mloluyu.characters.SimpleFighter;
+
+/**
+ * 一个非常简化的 AI:
+ * - 距离目标远: 向目标靠近
+ * - 距离合适: 停下并有概率攻击
+ * - 偶尔随机跳一下
+ */
+public class SimpleFighterAI {
+ private final SimpleFighter self;
+ private SimpleFighter target; // 追踪的目标(本地玩家)
+ private final StateMachine fsm;
+
+ // 参数(后续可提取到 GameConstants 或配置文件)
+ private float attackCooldown = 0f;
+ private float jumpCooldown = 0f;
+ private static final float ATTACK_COOLDOWN_MIN = 0.6f;
+ private static final float ATTACK_COOLDOWN_MAX = 1.2f;
+ private static final float JUMP_COOLDOWN_MIN = 2.5f;
+ private static final float JUMP_COOLDOWN_MAX = 4.5f;
+ private static final float DESIRED_DISTANCE = 140f; // 停下的理想距离
+ private static final float ATTACK_RANGE = 130f; // 触发攻击的距离
+
+ public SimpleFighterAI(SimpleFighter self, SimpleFighter target) {
+ this.self = self;
+ this.target = target;
+ this.fsm = new DefaultStateMachine<>(this, AIState.IDLE);
+ resetAttackCd();
+ resetJumpCd();
+ }
+
+ private void resetAttackCd() {
+ attackCooldown = MathUtils.random(ATTACK_COOLDOWN_MIN, ATTACK_COOLDOWN_MAX);
+ }
+
+ private void resetJumpCd() {
+ jumpCooldown = MathUtils.random(JUMP_COOLDOWN_MIN, JUMP_COOLDOWN_MAX);
+ }
+
+ public void update(float delta) {
+ attackCooldown -= delta;
+ jumpCooldown -= delta;
+ fsm.update();
+ }
+
+ public void setTarget(SimpleFighter target) {
+ this.target = target;
+ }
+
+ private float horizontalDistance() {
+ if (target == null)
+ return Float.MAX_VALUE;
+ return target.getHitbox().x - self.getHitbox().x;
+ }
+
+ private float absDistance() {
+ return Math.abs(horizontalDistance());
+ }
+
+ enum AIState implements State {
+ IDLE {
+ @Override
+ public void enter(SimpleFighterAI ai) {
+ }
+
+ @Override
+ public void update(SimpleFighterAI ai) {
+ if (ai.target == null || !ai.target.isAlive())
+ return;
+ float dist = ai.absDistance();
+ if (dist > DESIRED_DISTANCE * 1.3f) {
+ ai.fsm.changeState(MOVE);
+ return;
+ }
+ // 在理想距离范围内尝试攻击
+ ai.tryAttack();
+ ai.tryJump();
+ }
+
+ @Override
+ public void exit(SimpleFighterAI ai) {
+ }
+
+ @Override
+ public boolean onMessage(SimpleFighterAI entity, Telegram telegram) {
+ return false;
+ }
+ },
+ MOVE {
+ @Override
+ public void enter(SimpleFighterAI ai) {
+ }
+
+ @Override
+ public void update(SimpleFighterAI ai) {
+ if (ai.target == null || !ai.target.isAlive()) {
+ ai.fsm.changeState(IDLE);
+ return;
+ }
+ float distSigned = ai.horizontalDistance();
+ float distAbs = Math.abs(distSigned);
+ if (distAbs < DESIRED_DISTANCE) {
+ ai.fsm.changeState(IDLE);
+ return;
+ }
+ float dir = Math.signum(distSigned);
+ ai.self.move(dir, com.badlogic.gdx.Gdx.graphics.getDeltaTime());
+ // 攻击机会(接近过程中如果已经够近)
+ if (distAbs < ATTACK_RANGE * 0.9f) {
+ ai.tryAttack();
+ }
+ ai.tryJump();
+ }
+
+ @Override
+ public void exit(SimpleFighterAI ai) {
+ }
+
+ @Override
+ public boolean onMessage(SimpleFighterAI entity, Telegram telegram) {
+ return false;
+ }
+ };
+ }
+
+ private void tryAttack() {
+ if (attackCooldown <= 0f && self.isAlive() && !self.isAttacking()) {
+ float dist = absDistance();
+ if (dist < ATTACK_RANGE) {
+ // 轻重攻击随机
+ String atkType;
+ float r = MathUtils.random();
+ if (r < 0.7f)
+ atkType = "light";
+ else if (r < 0.9f)
+ atkType = "heavy";
+ else
+ atkType = "special";
+ self.attack(atkType);
+ resetAttackCd();
+ }
+ }
+ }
+
+ private void tryJump() {
+ if (jumpCooldown <= 0f && self.isAlive() && MathUtils.random() < 0.15f) {
+ self.jump();
+ resetJumpCd();
+ }
+ }
+}
diff --git a/src/main/java/uno/mloluyu/characters/effects/AttackEffect.java b/src/main/java/uno/mloluyu/characters/effects/AttackEffect.java
new file mode 100644
index 0000000..e815687
--- /dev/null
+++ b/src/main/java/uno/mloluyu/characters/effects/AttackEffect.java
@@ -0,0 +1,44 @@
+package uno.mloluyu.characters.effects;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+import com.badlogic.gdx.math.Rectangle;
+
+/** 简单攻击特效:在攻击盒区域闪现并淡出。 */
+public class AttackEffect {
+ private final Rectangle area = new Rectangle();
+ private float life; // 剩余寿命
+ private final float totalLife;
+ private final Color color = new Color();
+
+ public AttackEffect(Rectangle src, float duration, Color base) {
+ this.area.set(src);
+ this.life = duration;
+ this.totalLife = duration;
+ this.color.set(base);
+ }
+
+ public boolean isAlive() {
+ return life > 0f;
+ }
+
+ public void update(float delta) {
+ life -= delta;
+ }
+
+ public void render(ShapeRenderer sr) {
+ if (!isAlive())
+ return;
+ float alpha = life / totalLife; // 线性淡出
+ sr.setColor(color.r, color.g, color.b, alpha * color.a);
+ sr.rect(area.x, area.y, area.width, area.height);
+ }
+
+ public void renderOutline(ShapeRenderer sr) {
+ if (!isAlive())
+ return;
+ float alpha = life / totalLife;
+ sr.setColor(color.r, color.g, color.b, alpha * 0.6f);
+ sr.rect(area.x, area.y, area.width, area.height);
+ }
+}
diff --git a/src/main/java/uno/mloluyu/characters/effects/HitParticle.java b/src/main/java/uno/mloluyu/characters/effects/HitParticle.java
new file mode 100644
index 0000000..8a5f3e3
--- /dev/null
+++ b/src/main/java/uno/mloluyu/characters/effects/HitParticle.java
@@ -0,0 +1,54 @@
+package uno.mloluyu.characters.effects;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+import com.badlogic.gdx.math.MathUtils;
+
+/** 单个命中粒子:简单方块/短线喷射,带速度、重力与淡出。 */
+public class HitParticle {
+ private float x, y;
+ private float vx, vy;
+ private float life; // 剩余寿命
+ private final float totalLife;
+ private final float size;
+ private final Color color = new Color();
+ private static final float GRAVITY = 900f; // 轻微下坠
+
+ public HitParticle(float x, float y, Color base, float speedMin, float speedMax, float lifeMin, float lifeMax,
+ float sizeMin, float sizeMax) {
+ this.x = x;
+ this.y = y;
+ float ang = MathUtils.random(15f, 165f) * MathUtils.degreesToRadians; // 向上扇形
+ float spd = MathUtils.random(speedMin, speedMax);
+ this.vx = MathUtils.cos(ang) * spd;
+ this.vy = MathUtils.sin(ang) * spd;
+ this.life = MathUtils.random(lifeMin, lifeMax);
+ this.totalLife = life;
+ this.size = MathUtils.random(sizeMin, sizeMax);
+ // 基础色随机稍许扰动
+ float tint = MathUtils.random(0.85f, 1.05f);
+ this.color.set(base.r * tint, base.g * tint, base.b * tint, 1f);
+ }
+
+ public boolean isAlive() {
+ return life > 0f;
+ }
+
+ public void update(float dt) {
+ if (!isAlive())
+ return;
+ life -= dt;
+ // 运动积分
+ x += vx * dt;
+ y += vy * dt;
+ vy -= GRAVITY * dt * 0.35f; // 轻微重力
+ }
+
+ public void render(ShapeRenderer sr) {
+ if (!isAlive())
+ return;
+ float a = (life / totalLife); // 线性淡出
+ sr.setColor(color.r, color.g, color.b, a);
+ sr.rect(x - size / 2f, y - size / 2f, size, size);
+ }
+}
diff --git a/src/main/java/uno/mloluyu/characters/effects/RadialRing.java b/src/main/java/uno/mloluyu/characters/effects/RadialRing.java
new file mode 100644
index 0000000..7fd13ca
--- /dev/null
+++ b/src/main/java/uno/mloluyu/characters/effects/RadialRing.java
@@ -0,0 +1,48 @@
+package uno.mloluyu.characters.effects;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+
+/**
+ * 攻击进入 ACTIVE 瞬间生成的环形扩散效果:半径扩大+透明衰减。
+ */
+public class RadialRing {
+ private float x, y;
+ private float radius;
+ private float maxRadius;
+ private float life;
+ private float maxLife;
+ private Color color;
+
+ public RadialRing(float x, float y, float startRadius, float maxRadius, float life, Color color) {
+ this.x = x; this.y = y; this.radius = startRadius; this.maxRadius = maxRadius; this.life = life; this.maxLife = life; this.color = new Color(color);
+ }
+
+ public void update(float dt) {
+ life -= dt;
+ float t = 1f - life / maxLife; // 0 -> 1
+ radius = (startRadius() + (maxRadius - startRadius()) * t);
+ }
+
+ private float startRadius() { return 6f; }
+
+ public boolean isAlive() { return life > 0f; }
+
+ public void render(ShapeRenderer sr) {
+ float alpha = life / maxLife;
+ sr.setColor(color.r, color.g, color.b, alpha * color.a);
+ // 使用多段线模拟圆环(填充模式下画薄圆)
+ int segments = 26;
+ float lineWidth = 2f;
+ float step = (float)(Math.PI * 2 / segments);
+ for (int i = 0; i < segments; i++) {
+ float a1 = i * step;
+ float a2 = (i + 1) * step;
+ float x1 = x + (float)Math.cos(a1) * radius;
+ float y1 = y + (float)Math.sin(a1) * radius;
+ float x2 = x + (float)Math.cos(a2) * radius;
+ float y2 = y + (float)Math.sin(a2) * radius;
+ sr.rectLine(x1, y1, x2, y2, lineWidth);
+ }
+ }
+}
diff --git a/src/main/java/uno/mloluyu/characters/effects/SparkParticle.java b/src/main/java/uno/mloluyu/characters/effects/SparkParticle.java
new file mode 100644
index 0000000..9fe7709
--- /dev/null
+++ b/src/main/java/uno/mloluyu/characters/effects/SparkParticle.java
@@ -0,0 +1,38 @@
+package uno.mloluyu.characters.effects;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+
+public class SparkParticle {
+ private float x, y;
+ private float vx, vy;
+ private float life;
+ private float maxLife;
+ private float length;
+ private Color color;
+
+ public SparkParticle(float x, float y, float speed, float angle, float life, float length, Color base) {
+ this.x = x; this.y = y;
+ this.vx = (float)Math.cos(angle) * speed;
+ this.vy = (float)Math.sin(angle) * speed;
+ this.life = life; this.maxLife = life; this.length = length; this.color = new Color(base);
+ }
+
+ public void update(float dt) {
+ life -= dt;
+ x += vx * dt;
+ y += vy * dt;
+ vy -= 900f * dt; // 轻微重力
+ }
+
+ public boolean isAlive() { return life > 0f; }
+
+ public void render(ShapeRenderer sr) {
+ float t = life / maxLife;
+ float a = t; // 线性淡出
+ sr.setColor(color.r, color.g, color.b, a * color.a);
+ float nx = x - vx * 0.015f; // 逆向一点形成拖尾
+ float ny = y - vy * 0.015f;
+ sr.rectLine(x, y, nx, ny, Math.max(1.2f, length * t));
+ }
+}
diff --git a/src/main/java/uno/mloluyu/characters/effects/Untitled-1.html b/src/main/java/uno/mloluyu/characters/effects/Untitled-1.html
new file mode 100644
index 0000000..aa7c054
--- /dev/null
+++ b/src/main/java/uno/mloluyu/characters/effects/Untitled-1.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/uno/mloluyu/desktop/GameScreen.java b/src/main/java/uno/mloluyu/desktop/GameScreen.java
index 417a340..d68394a 100644
--- a/src/main/java/uno/mloluyu/desktop/GameScreen.java
+++ b/src/main/java/uno/mloluyu/desktop/GameScreen.java
@@ -23,8 +23,14 @@ import uno.mloluyu.perf.PerfMetrics;
import uno.mloluyu.util.TimeStepLimiter;
import uno.mloluyu.util.GameConstants;
import uno.mloluyu.versatile.FighterController;
+import uno.mloluyu.characters.ai.SimpleFighterAI;
+import uno.mloluyu.characters.effects.AttackEffect;
+import uno.mloluyu.characters.effects.HitParticle;
+import uno.mloluyu.characters.effects.RadialRing;
+import uno.mloluyu.characters.effects.SparkParticle;
public class GameScreen extends ScreenAdapter {
+ private final MainGame game; // 保存引用以便切换屏幕(重开/返回主界面)
private final SimpleFighter player;
private final FighterController controller;
private final Map otherPlayers = new HashMap<>();
@@ -65,8 +71,27 @@ public class GameScreen extends ScreenAdapter {
private static final boolean SHOW_GROUND_STRIP = true;
private static final float GROUND_STRIP_HEIGHT = 14f; // 条带厚度
private static final float GROUND_STRIP_ALPHA = 0.20f; // 透明度 (0~1)
+ // AI 控制(单人模式自动生成一个 AI 对手)
+ private SimpleFighterAI aiController;
+ private SimpleFighter aiEnemy;
+ private final java.util.List attackEffects = new java.util.ArrayList<>();
+ private final java.util.List hitParticles = new java.util.ArrayList<>();
+ private final java.util.List radialRings = new java.util.ArrayList<>();
+ private final java.util.List sparkParticles = new java.util.ArrayList<>();
+
+ // ========== Game Over 相关 ==========
+ private boolean gameOver = false;
+ private String winnerName = "";
+ private com.badlogic.gdx.graphics.g2d.BitmapFont uiFont;
+ // 按钮区域(屏幕 UI 坐标)
+ private final int GO_PANEL_W = 640;
+ private final int GO_PANEL_H = 360;
+ private final int GO_BUTTON_W = 280;
+ private final int GO_BUTTON_H = 70;
+ // 运行时计算位置(在渲染里根据当前窗口尺寸居中)
public GameScreen(MainGame game, SimpleFighter player) {
+ this.game = game;
this.player = player;
this.controller = new FighterController(player);
}
@@ -92,6 +117,20 @@ public class GameScreen extends ScreenAdapter {
background = new Texture(Gdx.files.internal("innerbg.png"));
worldWidth = background.getWidth() * BACKGROUND_SCALE;
worldHeight = background.getHeight() * BACKGROUND_SCALE;
+ // 如果尚未联网,创建一个 AI 敌人
+ if (!NetworkManager.getInstance().isConnected()) {
+ aiEnemy = new SimpleFighter("AI");
+ aiEnemy.setPosition(player.getHitbox().x + 400f, GameConstants.GROUND_Y);
+ aiController = new SimpleFighterAI(aiEnemy, player);
+ otherPlayers.put("AI_LOCAL", aiEnemy); // 重用现有渲染/碰撞逻辑
+ }
+ // 加载 UI 字体(含中文)
+ try {
+ uiFont = uno.mloluyu.util.Font.loadChineseFont();
+ uiFont.getData().setScale(1.4f);
+ } catch (Exception e) {
+ // 忽略,fallback 在 Font 中已处理
+ }
}
@Override
@@ -101,10 +140,23 @@ public class GameScreen extends ScreenAdapter {
PerfMetrics.frame(delta);
// (原先背景在摄像机更新前绘制,会出现一帧“滞后”错位;已移到摄像机更新后)
- // 输入 / 逻辑
- player.update(delta);
- controller.update(delta);
- if (NetworkManager.getInstance().isConnected()) {
+ // ================= 主更新(若未 Game Over)=================
+ if (!gameOver) {
+ player.update(delta);
+ controller.update(delta);
+ if (aiController != null) {
+ aiController.update(delta);
+ aiEnemy.update(delta);
+ }
+ // 自动朝向(本地玩家朝向最近的一个对手)
+ autoFace(player, otherPlayers);
+ // 远程/AI 对手也朝向本地玩家(不在攻击主动阶段时翻转,防止攻击盒跳变)
+ for (SimpleFighter op : otherPlayers.values()) {
+ autoFace(op, java.util.Collections.singletonMap("LOCAL", player));
+ }
+ }
+ boolean connected = NetworkManager.getInstance().isConnected();
+ if (!gameOver && connected) {
NetworkManager.getInstance().sendPosition(player.getHitbox().x, player.getHitbox().y);
Map positions = NetworkManager.getInstance().getPlayerPositions();
if (positions != null) {
@@ -245,10 +297,108 @@ public class GameScreen extends ScreenAdapter {
}
respawns.clear();
}
+ } else if (!gameOver) {
+ // 离线模式:本地直接做攻击命中判定(本地玩家 -> AI,AI -> 本地)
+ if (player.isAttacking() && player.canDealDamage()) {
+ for (SimpleFighter op : otherPlayers.values()) {
+ if (op.isAlive() && player.getAttackbox().overlaps(op.getHitbox())) {
+ int dmg = player.getDamageForAttack(player.getLastAttackType());
+ op.takeHit(dmg, player.isFacingRight() ? 1 : -1);
+ spawnHitParticles(op, player.getLastAttackType(), player.isFacingRight());
+ player.markDamageApplied();
+ }
+ }
+ }
+ for (SimpleFighter op : otherPlayers.values()) {
+ if (op.isAttacking() && op.canDealDamage() && player.isAlive()
+ && op.getAttackbox().overlaps(player.getHitbox())) {
+ int dmg = op.getDamageForAttack(op.getLastAttackType());
+ player.takeHit(dmg, op.isFacingRight() ? 1 : -1);
+ spawnHitParticles(player, op.getLastAttackType(), op.isFacingRight());
+ op.markDamageApplied();
+ }
+ }
+ }
+
+ if (!gameOver) {
+ // 监测攻击进入 ACTIVE 阶段的瞬间生成一次特效
+ if (player.isInActivePhase() && player.canDealDamage()) {
+ attackEffects.add(new AttackEffect(new Rectangle(player.getAttackbox()), 0.22f,
+ new Color(0.3f, 0.6f, 1f, 0.55f)));
+ spawnActivationFX(player, true);
+ }
+ for (SimpleFighter remote : otherPlayers.values()) {
+ if (remote.isInActivePhase() && remote.canDealDamage()) {
+ attackEffects.add(new AttackEffect(new Rectangle(remote.getAttackbox()), 0.22f,
+ new Color(1f, 0.6f, 0.2f, 0.50f)));
+ spawnActivationFX(remote, false);
+ }
+ }
+ // 更新矩形攻击特效寿命
+ if (!attackEffects.isEmpty()) {
+ for (int i = attackEffects.size() - 1; i >= 0; i--) {
+ AttackEffect ef = attackEffects.get(i);
+ ef.update(delta);
+ if (!ef.isAlive())
+ attackEffects.remove(i);
+ }
+ }
+ // 更新粒子
+ if (!hitParticles.isEmpty()) {
+ for (int i = hitParticles.size() - 1; i >= 0; i--) {
+ HitParticle p = hitParticles.get(i);
+ p.update(delta);
+ if (!p.isAlive())
+ hitParticles.remove(i);
+ }
+ }
+ // 更新环形与火花
+ for (int i = radialRings.size() - 1; i >= 0; i--) {
+ RadialRing r = radialRings.get(i);
+ r.update(delta);
+ if (!r.isAlive()) radialRings.remove(i);
+ }
+ for (int i = sparkParticles.size() - 1; i >= 0; i--) {
+ SparkParticle sp = sparkParticles.get(i);
+ sp.update(delta);
+ if (!sp.isAlive()) sparkParticles.remove(i);
+ }
}
// 本地与远程玩家之间简单碰撞(防穿人)——放在摄像机更新前
- resolvePlayerCollisions();
+ if (!gameOver) {
+ resolvePlayerCollisions();
+ clampToWorld(player);
+ for (SimpleFighter op : otherPlayers.values()) clampToWorld(op);
+ }
+
+ // ================== Game Over 判定 ==================
+ if (!gameOver) {
+ boolean playerDead = !player.isAlive();
+ boolean allOthersDead = true;
+ for (SimpleFighter op : otherPlayers.values()) {
+ if (op.isAlive()) {
+ allOthersDead = false; // 有至少一个活着
+ break;
+ }
+ }
+ if (playerDead || (!otherPlayers.isEmpty() && allOthersDead)) {
+ gameOver = true;
+ if (playerDead) {
+ // 找第一个存活的对手
+ String wn = "对手";
+ for (SimpleFighter op : otherPlayers.values()) {
+ if (op.isAlive()) {
+ wn = op.getName();
+ break;
+ }
+ }
+ winnerName = wn;
+ } else {
+ winnerName = player.getName();
+ }
+ }
+ }
// 摄像机跟随
// 摄像头跟随:若有一个远程玩家,则居中于本地和远程玩家中点
@@ -327,23 +477,69 @@ public class GameScreen extends ScreenAdapter {
shapeRenderer.setColor(0.9f, 0.9f, 0.9f, GROUND_STRIP_ALPHA); // 近白轻薄层
shapeRenderer.rect(0f, y, worldWidth, GROUND_STRIP_HEIGHT);
}
+ // 先绘制攻击特效(填充)
+ for (AttackEffect ef : attackEffects) {
+ ef.render(shapeRenderer);
+ }
+ // 再绘制命中粒子
+ for (HitParticle hp : hitParticles) {
+ hp.render(shapeRenderer);
+ }
+ for (RadialRing rr : radialRings) rr.render(shapeRenderer);
+ for (SparkParticle sp : sparkParticles) sp.render(shapeRenderer);
drawHitbox(player, player.getDebugColor());
boolean showPlayerAttack = player.isAttacking()
|| (player.getCurrentAction() == Action.ATTACK && player.getAttackTimer() > 0);
- if (showPlayerAttack)
- // 攻击框颜色改为蓝色(原红色)
- drawAttackBox(player, 0.0f, 0.45f, 1f, 0.35f);
+ if (showPlayerAttack) {
+ // 分阶段颜色:startup=淡青, active=亮蓝, recovery=灰蓝
+ if (player.isInStartupPhase()) {
+ drawAttackBox(player, 0.3f, 0.8f, 1f, 0.18f);
+ } else if (player.isInActivePhase()) {
+ drawAttackBox(player, 0.0f, 0.6f, 1f, 0.45f);
+ } else if (player.isInRecoveryPhase()) {
+ drawAttackBox(player, 0.2f, 0.45f, 0.8f, 0.25f);
+ } else {
+ drawAttackBox(player, 0.0f, 0.45f, 1f, 0.35f);
+ }
+ }
+ // 防御白线(填充 pass 里画一条粗线模拟盾)
+ if (player.isDefending()) {
+ float lineX = player.isFacingRight() ? player.getHitbox().x + player.getHitbox().width + 4f
+ : player.getHitbox().x - 6f;
+ shapeRenderer.setColor(1f, 1f, 1f, 0.85f);
+ shapeRenderer.rect(lineX, player.getHitbox().y + 18f, 4f * (player.isFacingRight() ? 1f : -1f),
+ player.getHitbox().height - 36f);
+ }
for (SimpleFighter remote : otherPlayers.values()) {
drawHitbox(remote, remote.getDebugColor());
- if (remote.isAttacking())
- drawAttackBox(remote, 0.0f, 0.45f, 1f, 0.25f);
+ if (remote.isAttacking()) {
+ if (remote.isInStartupPhase()) {
+ drawAttackBox(remote, 1f, 0.75f, 0.25f, 0.18f);
+ } else if (remote.isInActivePhase()) {
+ drawAttackBox(remote, 1f, 0.55f, 0.10f, 0.45f);
+ } else if (remote.isInRecoveryPhase()) {
+ drawAttackBox(remote, 0.8f, 0.4f, 0.15f, 0.25f);
+ } else {
+ drawAttackBox(remote, 1f, 0.6f, 0.2f, 0.30f);
+ }
+ }
+ if (remote.isDefending()) {
+ float lineX = remote.isFacingRight() ? remote.getHitbox().x + remote.getHitbox().width + 4f
+ : remote.getHitbox().x - 6f;
+ shapeRenderer.setColor(1f, 1f, 1f, 0.75f);
+ shapeRenderer.rect(lineX, remote.getHitbox().y + 18f, 4f * (remote.isFacingRight() ? 1f : -1f),
+ remote.getHitbox().height - 36f);
+ }
}
shapeRenderer.end();
// -------- Sprite pass --------
batch.begin();
+ // 角色精灵(当前占位),未来可在这里设置 batch.setColor(alpha) 来影响纹理
+ batch.setColor(1f, 1f, 1f, player.getRenderAlpha());
player.renderSprite(batch);
+ batch.setColor(1f, 1f, 1f, 1f);
batch.end();
// -------- Debug line pass --------
@@ -352,6 +548,10 @@ public class GameScreen extends ScreenAdapter {
shapeRenderer.setColor(0.65f, 0.65f, 0.65f, 1f); // 浅灰色参考线
shapeRenderer.line(0f, GameConstants.GROUND_Y, worldWidth, GameConstants.GROUND_Y);
}
+ // 攻击特效描边
+ for (AttackEffect ef : attackEffects) {
+ ef.renderOutline(shapeRenderer);
+ }
player.renderDebug(shapeRenderer);
for (SimpleFighter remote : otherPlayers.values())
remote.renderDebug(shapeRenderer);
@@ -364,31 +564,109 @@ public class GameScreen extends ScreenAdapter {
shapeRenderer.setProjectionMatrix(uiCam.combined);
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
// 绘制本地玩家血条(左侧)和所有远程玩家血条(右侧依次排列)
- float barWidth = 200f, barHeight = 10f, padding = 10f;
+ float barWidth = 320f, barHeight = 22f, padding = 18f; // 放大并下移
float screenW = Gdx.graphics.getWidth(), screenH = Gdx.graphics.getHeight();
+ float baseY = screenH - padding - barHeight - 20f; // 下移 20 避免贴顶
// 本地玩家血条
- shapeRenderer.setColor(Color.DARK_GRAY);
- shapeRenderer.rect(padding, screenH - padding - barHeight, barWidth, barHeight);
+ shapeRenderer.setColor(0.15f, 0.15f, 0.15f, 0.9f);
+ shapeRenderer.rect(padding, baseY, barWidth, barHeight);
shapeRenderer.setColor(Color.RED);
- shapeRenderer.rect(padding, screenH - padding - barHeight,
- barWidth * (player.getHealth() / 100f), barHeight);
+ shapeRenderer.rect(padding, baseY,
+ barWidth * (player.getHealth() / (float) player.getMaxHealth()), barHeight);
// 远程玩家血条
int idx = 0;
for (SimpleFighter remote : otherPlayers.values()) {
float x = screenW - padding - barWidth - idx * (barWidth + padding);
- shapeRenderer.setColor(Color.DARK_GRAY);
- shapeRenderer.rect(x, screenH - padding - barHeight, barWidth, barHeight);
+ shapeRenderer.setColor(0.15f, 0.15f, 0.15f, 0.9f);
+ shapeRenderer.rect(x, baseY, barWidth, barHeight);
shapeRenderer.setColor(Color.GREEN);
- shapeRenderer.rect(x, screenH - padding - barHeight,
- barWidth * (remote.getHealth() / 100f), barHeight);
+ float ratio = remote.getHealth() / (float) remote.getMaxHealth();
+ shapeRenderer.rect(x, baseY,
+ barWidth * ratio, barHeight);
idx++;
}
shapeRenderer.end();
+
+ // ================== Game Over 覆盖层 ==================
+ if (gameOver) {
+ // 使用同一 UI 摄像机
+ shapeRenderer.setProjectionMatrix(uiCam.combined);
+ batch.setProjectionMatrix(uiCam.combined);
+ float screenW2 = screenW;
+ float screenH2 = screenH;
+ float panelX = (screenW2 - GO_PANEL_W) / 2f;
+ float panelY = (screenH2 - GO_PANEL_H) / 2f;
+ float btnRestartX = panelX + (GO_PANEL_W - GO_BUTTON_W) / 2f;
+ float btnRestartY = panelY + 110f;
+ float btnMenuX = btnRestartX;
+ float btnMenuY = panelY + 25f;
+
+ // 半透明遮罩 + 面板 + 按钮
+ shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
+ shapeRenderer.setColor(0, 0, 0, 0.55f);
+ shapeRenderer.rect(0, 0, screenW2, screenH2);
+ shapeRenderer.setColor(0.12f, 0.12f, 0.12f, 0.92f);
+ shapeRenderer.rect(panelX, panelY, GO_PANEL_W, GO_PANEL_H);
+ // 按钮背景
+ int mx = Gdx.input.getX();
+ int my = Gdx.graphics.getHeight() - Gdx.input.getY();
+ boolean hoverRestart = mx >= btnRestartX && mx <= btnRestartX + GO_BUTTON_W && my >= btnRestartY
+ && my <= btnRestartY + GO_BUTTON_H;
+ boolean hoverMenu = mx >= btnMenuX && mx <= btnMenuX + GO_BUTTON_W && my >= btnMenuY
+ && my <= btnMenuY + GO_BUTTON_H;
+ shapeRenderer.setColor(
+ hoverRestart ? new Color(0.25f, 0.5f, 0.25f, 0.95f) : new Color(0.18f, 0.38f, 0.18f, 0.9f));
+ shapeRenderer.rect(btnRestartX, btnRestartY, GO_BUTTON_W, GO_BUTTON_H);
+ shapeRenderer
+ .setColor(hoverMenu ? new Color(0.35f, 0.35f, 0.55f, 0.95f) : new Color(0.25f, 0.25f, 0.45f, 0.9f));
+ shapeRenderer.rect(btnMenuX, btnMenuY, GO_BUTTON_W, GO_BUTTON_H);
+ shapeRenderer.end();
+
+ // 文本
+ batch.begin();
+ if (uiFont != null) {
+ uiFont.draw(batch, "游戏结束", panelX + 180f, panelY + GO_PANEL_H - 60f);
+ uiFont.draw(batch, "胜者: " + winnerName, panelX + 180f, panelY + GO_PANEL_H - 120f);
+ uiFont.draw(batch, "重新开始", btnRestartX + 40f, btnRestartY + 48f);
+ uiFont.draw(batch, "返回主界面", btnMenuX + 20f, btnMenuY + 48f);
+ }
+ batch.end();
+
+ // 按钮交互(鼠标)
+ if (Gdx.input.justTouched()) {
+ int mx2 = Gdx.input.getX();
+ int my2 = Gdx.graphics.getHeight() - Gdx.input.getY();
+ if (mx2 >= btnRestartX && mx2 <= btnRestartX + GO_BUTTON_W && my2 >= btnRestartY
+ && my2 <= btnRestartY + GO_BUTTON_H) {
+ restartGame();
+ return; // 避免继续处理后续逻辑
+ } else if (mx2 >= btnMenuX && mx2 <= btnMenuX + GO_BUTTON_W && my2 >= btnMenuY
+ && my2 <= btnMenuY + GO_BUTTON_H) {
+ returnToMenu();
+ return;
+ }
+ }
+ // 键盘快捷键:R 重新开始 / M 或 ESC 返回菜单
+ if (Gdx.input.isKeyJustPressed(com.badlogic.gdx.Input.Keys.R)) {
+ restartGame();
+ } else if (Gdx.input.isKeyJustPressed(com.badlogic.gdx.Input.Keys.M)
+ || Gdx.input.isKeyJustPressed(com.badlogic.gdx.Input.Keys.ESCAPE)) {
+ returnToMenu();
+ }
+ }
// 原定期性能日志已移除(日志系统删除)
}
private void drawHitbox(SimpleFighter fighter, Color color) {
- shapeRenderer.setColor(color);
+ float a = fighter.getRenderAlpha();
+ if (a <= 0f)
+ return;
+ // 阵营基色:本地玩家=青蓝(#26BFF2),敌方=橙红(#F35A26);半透明以免过亮
+ boolean isLocal = fighter == player;
+ float br = isLocal ? 0.15f : 0.95f;
+ float bg = isLocal ? 0.75f : 0.35f;
+ float bb = isLocal ? 0.95f : 0.15f;
+ shapeRenderer.setColor(br, bg, bb, a * 0.85f);
Rectangle r = fighter.getHitbox();
float scale = GameConstants.DEBUG_BOX_SCALE;
if (scale <= 1.0001f && scale >= 0.9999f) { // 视为 1
@@ -407,7 +685,19 @@ public class GameScreen extends ScreenAdapter {
}
private void drawAttackBox(SimpleFighter fighter, float r, float g, float b, float a) {
- shapeRenderer.setColor(r, g, b, a);
+ float alpha = fighter.getRenderAlpha();
+ if (alpha <= 0f)
+ return;
+ boolean isLocal = fighter == player;
+ // 阵营色调
+ float teamR = isLocal ? 0.2f : 0.9f;
+ float teamG = isLocal ? 0.9f : 0.3f;
+ float teamB = isLocal ? 1.0f : 0.2f;
+ // 将阶段色与阵营色混合:阶段色 55%,阵营色 45%
+ float cr = r * 0.55f + teamR * 0.45f;
+ float cg = g * 0.55f + teamG * 0.45f;
+ float cb = b * 0.55f + teamB * 0.45f;
+ shapeRenderer.setColor(cr, cg, cb, a * alpha);
Rectangle box = fighter.getAttackbox();
float scale = GameConstants.DEBUG_BOX_SCALE;
if (scale <= 1.0001f && scale >= 0.9999f) {
@@ -442,6 +732,8 @@ public class GameScreen extends ScreenAdapter {
shapeRenderer.dispose();
if (background != null)
background.dispose();
+ if (uiFont != null)
+ uiFont.dispose();
NetworkManager.getInstance().disconnect();
}
@@ -477,4 +769,121 @@ public class GameScreen extends ScreenAdapter {
}
}
}
+
+ /** 限制角色 hitbox 不超出世界边界。 */
+ private void clampToWorld(SimpleFighter f) {
+ if (f == null) return;
+ Rectangle hb = f.getHitbox();
+ if (hb.x < 0) hb.x = 0;
+ if (hb.x + hb.width > worldWidth) hb.x = worldWidth - hb.width;
+ // 垂直:地面以上,顶部不超背景
+ if (hb.y < GameConstants.GROUND_Y) hb.y = GameConstants.GROUND_Y;
+ if (hb.y + hb.height > worldHeight) hb.y = worldHeight - hb.height;
+ }
+
+ private void restartGame() {
+ // 重新实例化一个同类型的玩家
+ SimpleFighter newPlayer;
+ if (player instanceof AdvancedFighter) {
+ newPlayer = new AdvancedFighter(player.getName());
+ } else {
+ newPlayer = new SimpleFighter(player.getName());
+ }
+ game.setScreen(new GameScreen(game, newPlayer));
+ }
+
+ private void returnToMenu() {
+ game.setScreen(new MainMenuScreen(game));
+ }
+
+ /**
+ * 让 fighter 朝向 map 中最近的一个对手(按 X 中心差)。
+ * 约束:1) 若自身正在攻击 ACTIVE 阶段则不翻转,避免攻击盒闪跳。
+ */
+ private void autoFace(SimpleFighter self, Map candidates) {
+ if (self == null || candidates == null || candidates.isEmpty())
+ return;
+ // 若正在攻击:仅允许在 STARTUP 之前或 RECOVERY 后翻转,这里简单:只要在攻击整体期间就不改
+ if (self.isAttacking())
+ return;
+ float selfCenter = self.getHitbox().x + self.getHitbox().width * 0.5f;
+ SimpleFighter nearest = null;
+ float bestDist = Float.MAX_VALUE;
+ for (SimpleFighter f : candidates.values()) {
+ if (f == null || f == self)
+ continue;
+ float center = f.getHitbox().x + f.getHitbox().width * 0.5f;
+ float d = Math.abs(center - selfCenter);
+ if (d < bestDist) {
+ bestDist = d;
+ nearest = f;
+ }
+ }
+ if (nearest != null) {
+ float targetCenter = nearest.getHitbox().x + nearest.getHitbox().width * 0.5f;
+ boolean shouldFaceRight = targetCenter >= selfCenter;
+ self.setFacingRight(shouldFaceRight);
+ }
+ }
+
+ /** 根据命中位置和攻击类型生成粒子喷射。 */
+ private void spawnHitParticles(SimpleFighter target, String attackType, boolean attackerFacingRight) {
+ if (target == null)
+ return;
+ // 命中点设为目标上半身区域
+ float cx = target.getHitbox().x + target.getHitbox().width * 0.5f;
+ float cy = target.getHitbox().y + target.getHitbox().height * 0.6f;
+ int count;
+ Color base;
+ switch (attackType) {
+ case "heavy":
+ count = 28;
+ base = new Color(1f, 0.55f, 0.20f, 1f);
+ break;
+ case "special":
+ count = 40;
+ base = new Color(0.9f, 0.25f, 0.95f, 1f);
+ break;
+ case "light":
+ default:
+ count = 18;
+ base = new Color(1f, 0.85f, 0.25f, 1f);
+ break;
+ }
+ for (int i = 0; i < count; i++) {
+ hitParticles.add(new HitParticle(cx, cy, base,
+ 260f, 520f, // 速度范围
+ 0.25f, 0.55f, // 寿命范围
+ 4f, 10f // 尺寸范围
+ ));
+ }
+ // 额外冲击条纹(用 AttackEffect 再叠一层细长)可选:略
+ }
+
+ /** 攻击进入 ACTIVE 时的环形+火花特效。 */
+ private void spawnActivationFX(SimpleFighter fighter, boolean isLocal) {
+ Rectangle ab = fighter.getAttackbox();
+ float cx = ab.x + ab.width * 0.5f;
+ float cy = ab.y + ab.height * 0.5f;
+ // 环颜色区分阵营
+ Color ringColor = isLocal ? new Color(0.35f, 0.8f, 1f, 0.9f) : new Color(1f, 0.55f, 0.2f, 0.85f);
+ radialRings.add(new RadialRing(cx, cy, 6f, Math.max(38f, ab.width * 0.55f), 0.28f, ringColor));
+ // 火花数量按攻击盒面积估计
+ int sparkCount = 14 + (int) (ab.width * ab.height / 800f);
+ Color sparkBase = isLocal ? new Color(0.55f, 0.9f, 1f, 1f) : new Color(1f, 0.7f, 0.25f, 1f);
+ for (int i = 0; i < sparkCount; i++) {
+ float ang = (float) (Math.random() * Math.PI * 2);
+ float speed = 420f + (float) Math.random() * 380f;
+ float life = 0.18f + (float) Math.random() * 0.16f;
+ float len = 6f + (float) Math.random() * 10f;
+ // 色彩轻度扰动
+ Color c = new Color(
+ clamp01(sparkBase.r + (float) (Math.random() * 0.15 - 0.075)),
+ clamp01(sparkBase.g + (float) (Math.random() * 0.15 - 0.075)),
+ clamp01(sparkBase.b + (float) (Math.random() * 0.15 - 0.075)), 1f);
+ sparkParticles.add(new SparkParticle(cx, cy, speed, ang, life, len, c));
+ }
+ }
+
+ private float clamp01(float v) { return v < 0f ? 0f : (v > 1f ? 1f : v); }
}
diff --git a/src/main/java/uno/mloluyu/desktop/StartScreen.java b/src/main/java/uno/mloluyu/desktop/StartScreen.java
index 9d278ef..4e04c91 100644
--- a/src/main/java/uno/mloluyu/desktop/StartScreen.java
+++ b/src/main/java/uno/mloluyu/desktop/StartScreen.java
@@ -34,7 +34,7 @@ public class StartScreen extends BaseScreen {
public void render(float delta) {
deltaSum += delta;
- if (deltaSum >= .01F) {
+ if (deltaSum >= 3.0F) {
if (mainGame != null) {
mainGame.showGameScreen();
return;
diff --git a/src/main/java/uno/mloluyu/util/Font.java b/src/main/java/uno/mloluyu/util/Font.java
index e7c1737..06e7c3d 100644
--- a/src/main/java/uno/mloluyu/util/Font.java
+++ b/src/main/java/uno/mloluyu/util/Font.java
@@ -16,7 +16,7 @@ public class Font {
parameter.color = Color.WHITE;
parameter.borderWidth = 1;
parameter.borderColor = Color.DARK_GRAY;
- parameter.characters = "返回主菜单确认角色选择了角色人游戏加入联机模式 - 等待其他玩家连接...房间创建房间联机设置开始游戏设置联网中国abcdefghijklmnopqrstuvw暂定xyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ parameter.characters = "返回主菜单确退出认角色选择了角色人游戏加入联机模式 - 等待其他玩家连接...房间创建房间联机设置开始游戏设置联网中国重新开始胜者游戏结束返回主界面abcdefghijklmnopqrstuvw暂定xyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return generator.generateFont(parameter);
} catch (Exception e) {
diff --git a/src/main/java/uno/mloluyu/util/GameConstants.java b/src/main/java/uno/mloluyu/util/GameConstants.java
index 3a17828..f815050 100644
--- a/src/main/java/uno/mloluyu/util/GameConstants.java
+++ b/src/main/java/uno/mloluyu/util/GameConstants.java
@@ -19,6 +19,14 @@ public final class GameConstants {
public static final float JUMP_SPEED = 1500f;
public static final float GRAVITY = 3200f; // 加大重力让落地更快
+ // ====== 新增:移动手感调优参数 ======
+ // 水平加速度(越大越快贴近目标速度)
+ public static final float MOVE_ACCEL = 6000f;
+ // 水平减速(松开方向键时朝 0 速度衰减)
+ public static final float MOVE_DECEL = 8000f;
+ // 空中加速度系数(降低空中水平掌控)
+ public static final float AIR_ACCEL_FACTOR = 0.6f;
+
// 调试命中盒渲染缩放(=1 表示真实大小;之前放大 3.6 现在回归可控)
// 调试盒缩放:1 = 实际大小;若想放大显示结构,可调大。
public static final float DEBUG_BOX_SCALE = 1.0f;
diff --git a/target/classes/uno/mloluyu/characters/Action.class b/target/classes/uno/mloluyu/characters/Action.class
index 7ea2ac5..cfc786f 100644
Binary files a/target/classes/uno/mloluyu/characters/Action.class and b/target/classes/uno/mloluyu/characters/Action.class differ
diff --git a/target/classes/uno/mloluyu/characters/ActionStateGuard.class b/target/classes/uno/mloluyu/characters/ActionStateGuard.class
index dcc21b6..03edef1 100644
Binary files a/target/classes/uno/mloluyu/characters/ActionStateGuard.class and b/target/classes/uno/mloluyu/characters/ActionStateGuard.class differ
diff --git a/target/classes/uno/mloluyu/characters/ActionTransitionMap.class b/target/classes/uno/mloluyu/characters/ActionTransitionMap.class
index da867dc..afac80f 100644
Binary files a/target/classes/uno/mloluyu/characters/ActionTransitionMap.class and b/target/classes/uno/mloluyu/characters/ActionTransitionMap.class differ
diff --git a/target/classes/uno/mloluyu/characters/AdvancedFighter.class b/target/classes/uno/mloluyu/characters/AdvancedFighter.class
index 6b749bf..047b468 100644
Binary files a/target/classes/uno/mloluyu/characters/AdvancedFighter.class and b/target/classes/uno/mloluyu/characters/AdvancedFighter.class differ
diff --git a/target/classes/uno/mloluyu/characters/FighterAnimationManager.class b/target/classes/uno/mloluyu/characters/FighterAnimationManager.class
index cf0ed32..48859b0 100644
Binary files a/target/classes/uno/mloluyu/characters/FighterAnimationManager.class and b/target/classes/uno/mloluyu/characters/FighterAnimationManager.class differ
diff --git a/target/classes/uno/mloluyu/characters/FighterBase.class b/target/classes/uno/mloluyu/characters/FighterBase.class
index 126da1a..eb28b00 100644
Binary files a/target/classes/uno/mloluyu/characters/FighterBase.class and b/target/classes/uno/mloluyu/characters/FighterBase.class differ
diff --git a/target/classes/uno/mloluyu/characters/SimpleFighter$AttackPhase.class b/target/classes/uno/mloluyu/characters/SimpleFighter$AttackPhase.class
new file mode 100644
index 0000000..c936e1b
Binary files /dev/null and b/target/classes/uno/mloluyu/characters/SimpleFighter$AttackPhase.class differ
diff --git a/target/classes/uno/mloluyu/characters/SimpleFighter.class b/target/classes/uno/mloluyu/characters/SimpleFighter.class
index 0e8605e..3813d0a 100644
Binary files a/target/classes/uno/mloluyu/characters/SimpleFighter.class and b/target/classes/uno/mloluyu/characters/SimpleFighter.class differ
diff --git a/target/classes/uno/mloluyu/characters/ai/SimpleFighterAI$AIState$1.class b/target/classes/uno/mloluyu/characters/ai/SimpleFighterAI$AIState$1.class
new file mode 100644
index 0000000..99852b6
Binary files /dev/null and b/target/classes/uno/mloluyu/characters/ai/SimpleFighterAI$AIState$1.class differ
diff --git a/target/classes/uno/mloluyu/characters/ai/SimpleFighterAI$AIState$2.class b/target/classes/uno/mloluyu/characters/ai/SimpleFighterAI$AIState$2.class
new file mode 100644
index 0000000..f8ad009
Binary files /dev/null and b/target/classes/uno/mloluyu/characters/ai/SimpleFighterAI$AIState$2.class differ
diff --git a/target/classes/uno/mloluyu/characters/ai/SimpleFighterAI$AIState.class b/target/classes/uno/mloluyu/characters/ai/SimpleFighterAI$AIState.class
new file mode 100644
index 0000000..4b2d4e3
Binary files /dev/null and b/target/classes/uno/mloluyu/characters/ai/SimpleFighterAI$AIState.class differ
diff --git a/target/classes/uno/mloluyu/characters/ai/SimpleFighterAI.class b/target/classes/uno/mloluyu/characters/ai/SimpleFighterAI.class
new file mode 100644
index 0000000..aa80cb1
Binary files /dev/null and b/target/classes/uno/mloluyu/characters/ai/SimpleFighterAI.class differ
diff --git a/target/classes/uno/mloluyu/characters/effects/AttackEffect.class b/target/classes/uno/mloluyu/characters/effects/AttackEffect.class
new file mode 100644
index 0000000..5f56c20
Binary files /dev/null and b/target/classes/uno/mloluyu/characters/effects/AttackEffect.class differ
diff --git a/target/classes/uno/mloluyu/characters/effects/HitParticle.class b/target/classes/uno/mloluyu/characters/effects/HitParticle.class
new file mode 100644
index 0000000..19665db
Binary files /dev/null and b/target/classes/uno/mloluyu/characters/effects/HitParticle.class differ
diff --git a/target/classes/uno/mloluyu/characters/effects/RadialRing.class b/target/classes/uno/mloluyu/characters/effects/RadialRing.class
new file mode 100644
index 0000000..bb48d46
Binary files /dev/null and b/target/classes/uno/mloluyu/characters/effects/RadialRing.class differ
diff --git a/target/classes/uno/mloluyu/characters/effects/SparkParticle.class b/target/classes/uno/mloluyu/characters/effects/SparkParticle.class
new file mode 100644
index 0000000..cf47851
Binary files /dev/null and b/target/classes/uno/mloluyu/characters/effects/SparkParticle.class differ
diff --git a/target/classes/uno/mloluyu/characters/effects/Untitled-1.html b/target/classes/uno/mloluyu/characters/effects/Untitled-1.html
new file mode 100644
index 0000000..aa7c054
--- /dev/null
+++ b/target/classes/uno/mloluyu/characters/effects/Untitled-1.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/target/classes/uno/mloluyu/desktop/BaseScreen.class b/target/classes/uno/mloluyu/desktop/BaseScreen.class
index a6a8708..10bae5c 100644
Binary files a/target/classes/uno/mloluyu/desktop/BaseScreen.class and b/target/classes/uno/mloluyu/desktop/BaseScreen.class differ
diff --git a/target/classes/uno/mloluyu/desktop/CharacterSelectScreen.class b/target/classes/uno/mloluyu/desktop/CharacterSelectScreen.class
index b4abd5d..15ad898 100644
Binary files a/target/classes/uno/mloluyu/desktop/CharacterSelectScreen.class and b/target/classes/uno/mloluyu/desktop/CharacterSelectScreen.class differ
diff --git a/target/classes/uno/mloluyu/desktop/DesktopLauncher.class b/target/classes/uno/mloluyu/desktop/DesktopLauncher.class
index 14c04bc..4fe9ab6 100644
Binary files a/target/classes/uno/mloluyu/desktop/DesktopLauncher.class and b/target/classes/uno/mloluyu/desktop/DesktopLauncher.class differ
diff --git a/target/classes/uno/mloluyu/desktop/GameScreen.class b/target/classes/uno/mloluyu/desktop/GameScreen.class
index 4b3f60d..da86a72 100644
Binary files a/target/classes/uno/mloluyu/desktop/GameScreen.class and b/target/classes/uno/mloluyu/desktop/GameScreen.class differ
diff --git a/target/classes/uno/mloluyu/desktop/MainGame.class b/target/classes/uno/mloluyu/desktop/MainGame.class
index 91fbf91..ed3a3b2 100644
Binary files a/target/classes/uno/mloluyu/desktop/MainGame.class and b/target/classes/uno/mloluyu/desktop/MainGame.class differ
diff --git a/target/classes/uno/mloluyu/desktop/MainMenuScreen.class b/target/classes/uno/mloluyu/desktop/MainMenuScreen.class
index 55edc9f..464d161 100644
Binary files a/target/classes/uno/mloluyu/desktop/MainMenuScreen.class and b/target/classes/uno/mloluyu/desktop/MainMenuScreen.class differ
diff --git a/target/classes/uno/mloluyu/desktop/NetworkSettingsScreen$1.class b/target/classes/uno/mloluyu/desktop/NetworkSettingsScreen$1.class
index 32054a2..935f29f 100644
Binary files a/target/classes/uno/mloluyu/desktop/NetworkSettingsScreen$1.class and b/target/classes/uno/mloluyu/desktop/NetworkSettingsScreen$1.class differ
diff --git a/target/classes/uno/mloluyu/desktop/NetworkSettingsScreen.class b/target/classes/uno/mloluyu/desktop/NetworkSettingsScreen.class
index 14617b8..2cae829 100644
Binary files a/target/classes/uno/mloluyu/desktop/NetworkSettingsScreen.class and b/target/classes/uno/mloluyu/desktop/NetworkSettingsScreen.class differ
diff --git a/target/classes/uno/mloluyu/desktop/ScreenManager.class b/target/classes/uno/mloluyu/desktop/ScreenManager.class
index d833a70..ecba573 100644
Binary files a/target/classes/uno/mloluyu/desktop/ScreenManager.class and b/target/classes/uno/mloluyu/desktop/ScreenManager.class differ
diff --git a/target/classes/uno/mloluyu/desktop/SettingsScreen.class b/target/classes/uno/mloluyu/desktop/SettingsScreen.class
index 5f8abb7..55b88e6 100644
Binary files a/target/classes/uno/mloluyu/desktop/SettingsScreen.class and b/target/classes/uno/mloluyu/desktop/SettingsScreen.class differ
diff --git a/target/classes/uno/mloluyu/desktop/StartScreen.class b/target/classes/uno/mloluyu/desktop/StartScreen.class
index bca2b1d..fed408b 100644
Binary files a/target/classes/uno/mloluyu/desktop/StartScreen.class and b/target/classes/uno/mloluyu/desktop/StartScreen.class differ
diff --git a/target/classes/uno/mloluyu/network/ConnectClient.class b/target/classes/uno/mloluyu/network/ConnectClient.class
index a050295..8104def 100644
Binary files a/target/classes/uno/mloluyu/network/ConnectClient.class and b/target/classes/uno/mloluyu/network/ConnectClient.class differ
diff --git a/target/classes/uno/mloluyu/network/ConnectServer.class b/target/classes/uno/mloluyu/network/ConnectServer.class
index c56c7a3..5dcc81f 100644
Binary files a/target/classes/uno/mloluyu/network/ConnectServer.class and b/target/classes/uno/mloluyu/network/ConnectServer.class differ
diff --git a/target/classes/uno/mloluyu/network/NetworkManager.class b/target/classes/uno/mloluyu/network/NetworkManager.class
index cdd9abd..824b3df 100644
Binary files a/target/classes/uno/mloluyu/network/NetworkManager.class and b/target/classes/uno/mloluyu/network/NetworkManager.class differ
diff --git a/target/classes/uno/mloluyu/perf/PerfMetrics.class b/target/classes/uno/mloluyu/perf/PerfMetrics.class
index 803cb01..95401d9 100644
Binary files a/target/classes/uno/mloluyu/perf/PerfMetrics.class and b/target/classes/uno/mloluyu/perf/PerfMetrics.class differ
diff --git a/target/classes/uno/mloluyu/util/ClearScreen.class b/target/classes/uno/mloluyu/util/ClearScreen.class
index 7d173c8..fec25b8 100644
Binary files a/target/classes/uno/mloluyu/util/ClearScreen.class and b/target/classes/uno/mloluyu/util/ClearScreen.class differ
diff --git a/target/classes/uno/mloluyu/util/Font.class b/target/classes/uno/mloluyu/util/Font.class
index f3d8782..52b8039 100644
Binary files a/target/classes/uno/mloluyu/util/Font.class and b/target/classes/uno/mloluyu/util/Font.class differ
diff --git a/target/classes/uno/mloluyu/util/GameConstants.class b/target/classes/uno/mloluyu/util/GameConstants.class
index e35d632..daa02ce 100644
Binary files a/target/classes/uno/mloluyu/util/GameConstants.class and b/target/classes/uno/mloluyu/util/GameConstants.class differ
diff --git a/target/classes/uno/mloluyu/util/ResourcePaths.class b/target/classes/uno/mloluyu/util/ResourcePaths.class
index 758f2ae..b0072b3 100644
Binary files a/target/classes/uno/mloluyu/util/ResourcePaths.class and b/target/classes/uno/mloluyu/util/ResourcePaths.class differ
diff --git a/target/classes/uno/mloluyu/util/SimpleFormatter.class b/target/classes/uno/mloluyu/util/SimpleFormatter.class
index 84af275..bdc4604 100644
Binary files a/target/classes/uno/mloluyu/util/SimpleFormatter.class and b/target/classes/uno/mloluyu/util/SimpleFormatter.class differ
diff --git a/target/classes/uno/mloluyu/util/TimeStepLimiter.class b/target/classes/uno/mloluyu/util/TimeStepLimiter.class
index 0700958..804154a 100644
Binary files a/target/classes/uno/mloluyu/util/TimeStepLimiter.class and b/target/classes/uno/mloluyu/util/TimeStepLimiter.class differ
diff --git a/target/classes/uno/mloluyu/versatile/FighterController.class b/target/classes/uno/mloluyu/versatile/FighterController.class
index 9f2fe30..a57690a 100644
Binary files a/target/classes/uno/mloluyu/versatile/FighterController.class and b/target/classes/uno/mloluyu/versatile/FighterController.class differ
diff --git a/target/game-1.0-SNAPSHOT.jar b/target/game-1.0-SNAPSHOT.jar
index 8f63653..caaea02 100644
Binary files a/target/game-1.0-SNAPSHOT.jar and b/target/game-1.0-SNAPSHOT.jar differ
diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
index c994b96..134a25b 100644
--- a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
+++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -1,19 +1,19 @@
-uno\mloluyu\desktop\StartScreen.class
uno\mloluyu\desktop\NetworkSettingsScreen$1.class
uno\mloluyu\network\NetworkManager.class
uno\mloluyu\characters\FighterAnimationManager.class
-uno\mloluyu\desktop\CharacterSelectScreen.class
uno\mloluyu\versatile\FighterController.class
-uno\mloluyu\desktop\MainGame.class
-uno\mloluyu\network\ConnectServer.class
uno\mloluyu\network\ConnectClient.class
uno\mloluyu\characters\AdvancedFighter.class
uno\mloluyu\desktop\GameScreen.class
uno\mloluyu\characters\Action.class
uno\mloluyu\util\Font.class
uno\mloluyu\desktop\MainMenuScreen.class
-uno\mloluyu\util\ClearScreen.class
uno\mloluyu\characters\SimpleFighter.class
uno\mloluyu\desktop\DesktopLauncher.class
uno\mloluyu\util\SimpleFormatter.class
uno\mloluyu\desktop\NetworkSettingsScreen.class
+uno\mloluyu\desktop\StartScreen.class
+uno\mloluyu\desktop\CharacterSelectScreen.class
+uno\mloluyu\desktop\MainGame.class
+uno\mloluyu\network\ConnectServer.class
+uno\mloluyu\util\ClearScreen.class
diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
index 46662ee..eaeec34 100644
--- a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
+++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -2,6 +2,9 @@ C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characte
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\ActionStateGuard.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\ActionTransitionMap.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\AdvancedFighter.java
+C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\ai\SimpleFighterAI.java
+C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\effects\AttackEffect.java
+C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\effects\HitParticle.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\FighterAnimationManager.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\FighterBase.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\SimpleFighter.java
diff --git a/target/test-classes/uno/mloluyu/assets/AssetsExistenceTest.class b/target/test-classes/uno/mloluyu/assets/AssetsExistenceTest.class
index 7e5bba1..e7431f6 100644
Binary files a/target/test-classes/uno/mloluyu/assets/AssetsExistenceTest.class and b/target/test-classes/uno/mloluyu/assets/AssetsExistenceTest.class differ
diff --git a/target/test-classes/uno/mloluyu/characters/ActionStateTest.class b/target/test-classes/uno/mloluyu/characters/ActionStateTest.class
index 7d5a884..cc5f746 100644
Binary files a/target/test-classes/uno/mloluyu/characters/ActionStateTest.class and b/target/test-classes/uno/mloluyu/characters/ActionStateTest.class differ