Compare commits

...

7 Commits

Author SHA1 Message Date
wsj
0e8c95ba99 删除 target/game-1.0-SNAPSHOT.jar 2025-10-16 10:17:59 +08:00
4f03408682 更新 README.md 2025-09-28 09:21:41 +08:00
cee525b82e 优化项目结构 2025-09-27 15:02:52 +08:00
4f486b367f 优化 2025-09-26 09:31:46 +08:00
f5d5939f29 最后一版 2025-09-26 09:31:35 +08:00
21ee67a91e Merge branch 'test' of http://124.70.85.113:3000/wsj/Game into test 2025-09-26 00:23:38 +08:00
e8ad2f407c .。 2025-09-26 00:23:36 +08:00
95 changed files with 1817 additions and 366 deletions

BIN
Game/.gitignore vendored

Binary file not shown.

View File

@@ -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)

View File

@@ -1,114 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>uno.mloluyu</groupId>
<artifactId>game</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<gdx.version>1.12.1</gdx.version>
</properties>
<dependencies>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>
<version>1.12.1</version> <!-- 替换为你的 LibGDX 版本 -->
</dependency>
<!-- FreeType 字体扩展 -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype</artifactId>
<version>1.12.1</version> <!-- 与 LibGDX 版本保持一致 -->
</dependency>
<!-- 桌面平台的 FreeType 原生库 -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype-platform</artifactId>
<version>1.12.1</version>
<classifier>natives-desktop</classifier>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl</artifactId>
<version>1.12.1</version> <!-- 使用你的 LibGDX 版本号 -->
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-platform</artifactId>
<version>1.12.1</version>
<classifier>natives-desktop</classifier>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>
<version>${gdx.version}</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl3</artifactId>
<version>${gdx.version}</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-platform</artifactId>
<version>${gdx.version}</version>
<classifier>natives-desktop</classifier>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<!-- 建议使用与LibGDX兼容的JDK版本11或17比较合适 -->
<source>17</source>
<target>17</target>
<!-- 移除不必要的预览特性,除非确实需要 -->
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<!-- 确保这个类路径与你实际的Launcher类位置一致 -->
<!-- 例如如果你的类文件在src/main/java/uno/mloluyu/Launcher.java -->
<mainClass>uno.mloluyu.desktop.Launcher</mainClass>
</configuration>
</plugin>
<!-- 添加运行时类路径配置,解决类找不到问题 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>uno.mloluyu.desktop.Launcher</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -5,21 +5,23 @@ This project is about Java Project Training 2025, Major.Software Technology
### 本项目目前需要做的
- 实现游戏主界面,添加素材
- 完成游戏的核心功能,即双人格斗
- 实现基本的局域网内联机游戏功能
- ~~实现游戏主界面,添加素材~~
- ~~完成游戏的核心功能,即双人格斗~~
- ~~实现基本的局域网内联机游戏功能~~
- *~~打则死路一条~~*
- 完善游戏素材
- 在操作上实现部分优化
### 目前正在做的
- 编写单个角色并实现其状态 (@wsj)
- 游戏素材整理为精灵图 (@wsj)
- 给游戏进行界面类添加BUG (@mloluyu)
- 给标题界面类添加BUG (@mloluyu)
- 寻找一个合适的数据处理类编写方法
- ~~编写单个角色并实现其状态~~ (@wsj)
- 完善游戏素材 (@wsj)
- ~~给游戏进行界面类添加BUG~~ (@mloluyu)
- ~~给标题界面类添加BUG (@mloluyu)
- ~~寻找一个合适的数据处理类编写方法 (@wsj)~~
- *~~丰矿地打则~~*
### 参与项目的人
- 武术家 [@wsj](varia.mloluyu.uno/wsj)
- 大货车 [@mloluyu](varia.mloluyu.uno/mloluyu)
- 武术家 @wsj
- 大货车 @mloluyu

131
pom.xml
View File

@@ -14,71 +14,93 @@
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<gdx.version>1.12.1</gdx.version>
</properties>
<dependencies>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>
<version>1.12.1</version> <!-- 替换为你的 LibGDX 版本 -->
<!-- Core LibGDX -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>
<version>${gdx.version}</version>
</dependency>
<!-- LWJGL2 Desktop Backend (使用 LwjglApplication 而不是 Lwjgl3Application) -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl</artifactId>
<version>${gdx.version}</version>
</dependency>
<!-- 平台原生库(桌面) -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-platform</artifactId>
<version>${gdx.version}</version>
<classifier>natives-desktop</classifier>
</dependency>
<!-- FreeType 字体扩展 -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype</artifactId>
<version>1.12.1</version> <!-- 与 LibGDX 版本保持一致 -->
</dependency>
<!-- 桌面平台的 FreeType 原生库 -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype-platform</artifactId>
<version>1.12.1</version>
<classifier>natives-desktop</classifier>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl</artifactId>
<version>1.12.1</version> <!-- 使用你的 LibGDX 版本号 -->
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-platform</artifactId>
<version>1.12.1</version>
<classifier>natives-desktop</classifier>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>
<version>1.12.1</version>
<artifactId>gdx-freetype</artifactId>
<version>${gdx.version}</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl3</artifactId>
<version>1.12.1</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-platform</artifactId>
<version>1.12.1</version>
<artifactId>gdx-freetype-platform</artifactId>
<version>${gdx.version}</version>
<classifier>natives-desktop</classifier>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<!-- gdx-ai 行为树 / FSM / Steering -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-ai</artifactId>
<version>1.8.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Enforcer 防止依赖冲突 / 旧 JDK -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<id>enforce</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<requireJavaVersion>
<version>[21,)</version>
</requireJavaVersion>
<dependencyConvergence/>
</rules>
<fail>false</fail>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<!-- 建议使用与LibGDX兼容的JDK版本11或17比较合适 -->
<source>17</source>
<target>17</target>
<!-- 移除不必要的预览特性,除非确实需要 -->
<!-- 与 properties 中保持一致 -->
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<release>${maven.compiler.release}</release>
</configuration>
</plugin>
@@ -87,8 +109,6 @@
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<!-- 确保这个类路径与你实际的Launcher类位置一致 -->
<!-- 例如如果你的类文件在src/main/java/uno/mloluyu/Launcher.java -->
<mainClass>uno.mloluyu.desktop.DesktopLauncher</mainClass>
</configuration>
</plugin>
@@ -107,6 +127,31 @@
</archive>
</configuration>
</plugin>
<!-- 运行 JUnit 5 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
</configuration>
</plugin>
<!-- 初步覆盖率,仅生成报告,不设阈值 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals><goal>report</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@@ -0,0 +1,20 @@
package uno.mloluyu.characters;
/** 提供状态切换验证。 */
public final class ActionStateGuard {
private ActionStateGuard() {
}
public static Action transition(FighterBase fighter, Action next) {
Action current = fighter.getCurrentAction();
if (current == next)
return current;
if (!ActionTransitionMap.can(current, next)) {
return current;
}
if (fighter instanceof uno.mloluyu.characters.SimpleFighter sf) {
sf.directSetAction(next);
}
return next;
}
}

View File

@@ -0,0 +1,25 @@
package uno.mloluyu.characters;
import java.util.*;
/** 定义合法动作迁移,供校验/日志使用。 */
public final class ActionTransitionMap {
private static final Map<Action, Set<Action>> ALLOWED = new EnumMap<>(Action.class);
static {
allow(Action.IDLE, Action.MOVE, Action.JUMP, Action.ATTACK, Action.DEFEND, Action.HIT, Action.DEAD);
allow(Action.MOVE, Action.IDLE, Action.JUMP, Action.ATTACK, Action.HIT, Action.DEAD);
allow(Action.JUMP, Action.HIT, Action.ATTACK, Action.DEAD, Action.IDLE, Action.MOVE);
allow(Action.ATTACK, Action.HIT, Action.DEAD, Action.IDLE, Action.MOVE);
allow(Action.DEFEND, Action.HIT, Action.IDLE, Action.MOVE, Action.DEAD);
allow(Action.HIT, Action.IDLE, Action.MOVE, Action.DEAD);
allow(Action.DEAD); // 终止状态
}
private static void allow(Action from, Action... tos) {
ALLOWED.put(from, new HashSet<>(Arrays.asList(tos)));
}
public static boolean can(Action from, Action to) {
return ALLOWED.getOrDefault(from, Collections.emptySet()).contains(to);
}
}

View File

@@ -3,14 +3,12 @@ package uno.mloluyu.characters;
public class AdvancedFighter extends SimpleFighter {
public AdvancedFighter(String name) {
super(name); // 调用父类构造函数
super(name);
}
@Override
public void attack(String attackType) {
// 先使用父类的攻击逻辑来保证 isAttacking/attackTimer/attackbox 等状态被正确设置
super.attack(attackType);
// 在这里可以添加 AdvancedFighter 特有的扩展行为(攻击力、特效等)
// 例如:根据 attackType 调整伤害或触发粒子/声音,但不要忘记保留父类的状态设置
}
}

View File

@@ -0,0 +1,47 @@
package uno.mloluyu.characters;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.graphics.Color;
import java.util.concurrent.ThreadLocalRandom;
/** 公共角色基类:后续 Simple/Advanced 统一继承。 */
public abstract class FighterBase {
protected String name;
protected Action currentAction = Action.IDLE;
protected Rectangle hitbox = new Rectangle(0, 0, 64, 128);
protected Rectangle attackbox = new Rectangle(0, 0, 80, 80);
protected final Color debugColor;
public FighterBase(String name) {
this.name = name;
ThreadLocalRandom r = ThreadLocalRandom.current();
float rr = 0.35f + r.nextFloat() * 0.65f;
float gg = 0.35f + r.nextFloat() * 0.65f;
float bb = 0.35f + r.nextFloat() * 0.65f;
this.debugColor = new Color(rr, gg, bb, 1f);
}
public Action getCurrentAction() {
return currentAction;
}
public Rectangle getHitbox() {
return hitbox;
}
public Rectangle getAttackbox() {
return attackbox;
}
public String getName() {
return name;
}
public Color getDebugColor() {
return debugColor;
}
protected void setAction(Action next) {
this.currentAction = next;
}
}

View File

@@ -10,25 +10,40 @@ 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 {
public class SimpleFighter extends FighterBase {
private String name; // 角色名称
private Action currentAction = Action.IDLE; // 当前动作状态
// 继承: 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 = 300f; // 水平移动速度
private int health = 100; // 生命值
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,9 +54,19 @@ public class SimpleFighter {
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) {
this.name = name;
super(name);
}
public void update(float deltaTime) {
@@ -61,30 +86,57 @@ public class SimpleFighter {
}
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) {
verticalSpeed -= 2500 * deltaTime;
verticalSpeed -= GameConstants.GRAVITY * deltaTime;
hitbox.y += verticalSpeed * deltaTime;
if (hitbox.y <= 0) {
hitbox.y = 0;
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) {
@@ -103,7 +155,7 @@ public class SimpleFighter {
hitbox.y + hitbox.height * 0.7f);
}
public void handleInput(int keycode, boolean isPressed) {
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());
@@ -113,7 +165,7 @@ public class SimpleFighter {
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 {
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);
}
}
}
@@ -138,23 +202,53 @@ public class SimpleFighter {
}
public void changeAction(Action newAction) {
this.currentAction = newAction;
this.currentAction = ActionStateGuard.transition(this, newAction);
}
void directSetAction(Action a) {
this.currentAction = a;
}
public void jump() {
if (isGrounded) {
verticalSpeed = 1000f;
verticalSpeed = GameConstants.JUMP_SPEED;
isGrounded = false;
changeAction(Action.JUMP);
}
}
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);
}
}
@@ -181,39 +275,62 @@ public class SimpleFighter {
}
private void updateAttackbox(String attackType) {
float offsetX, offsetY = 20, width = 80, height = 80;
float baseOffsetY = 20f;
float width = 80f, height = 80f;
float offsetX;
float offsetY = baseOffsetY;
// 先决定尺寸,再根据朝向计算 offset不再用旧 attackbox.width 避免漂移)
switch (attackType) {
case "heavy":
offsetX = isFacingRight ? hitbox.width : -100;
offsetY = 40;
width = 100;
height = 100;
break;
case "light":
offsetX = isFacingRight ? hitbox.width - 10 : -attackbox.width + 10;
width = 100f;
height = 100f;
offsetY = 40f;
offsetX = isFacingRight ? hitbox.width : -width; // 重击靠近身体或覆盖前方
break;
case "special":
offsetX = isFacingRight ? hitbox.width + 20 : -attackbox.width - 20;
offsetY = 50;
width = 120;
height = 60;
width = 120f;
height = 60f;
offsetY = 50f;
offsetX = isFacingRight ? hitbox.width + 20f : -width - 20f;
break;
case "light":
default:
offsetX = isFacingRight ? hitbox.width - 10 : -attackbox.width + 10;
// 轻击稍微往前,不再参考旧 attackbox.width
offsetX = isFacingRight ? hitbox.width - 10f : -80f + 10f;
break;
}
attackbox.setPosition(hitbox.x + offsetX, hitbox.y + offsetY);
attackbox.setSize(width, height);
attackbox.setPosition(hitbox.x + offsetX, hitbox.y + offsetY);
}
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) {
@@ -222,17 +339,28 @@ public class SimpleFighter {
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() {
@@ -259,6 +387,10 @@ public class SimpleFighter {
return health;
}
public int getMaxHealth() {
return MAX_HEALTH;
}
public int getAttackSequence() {
return attackSequence;
}
@@ -276,7 +408,8 @@ public class SimpleFighter {
}
public boolean canDealDamage() {
return isAttacking && attackSequence != lastDamageAppliedSeq; // 未对当前序号造成过伤害
// 只有 ACTIVE 阶段且本序号造成过伤害才允许
return isAttacking && attackPhase == AttackPhase.ACTIVE && attackSequence != lastDamageAppliedSeq;
}
public void markDamageApplied() {
@@ -304,13 +437,32 @@ public class SimpleFighter {
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 = 100;
health = MAX_HEALTH;
isAttacking = false;
attackTimer = 0f;
attackJustStarted = false;
@@ -318,5 +470,20 @@ public class SimpleFighter {
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;
}
}

View File

@@ -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<SimpleFighterAI, AIState> 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<SimpleFighterAI> {
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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<form>
<label for="username">用户名:</label>
<input type="text" id="username" name="username"><br><br>
<input type="checkbox" id="remember">
<label for="remember">记住用户名</label><br><br>
<input type="button" value="提交" onclick="submitForm()">
</form>
</body>
</html>

View File

@@ -0,0 +1,16 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.ScreenAdapter;
/** 所有 Screen 的基础类,集中生命周期钩子扩展。 */
public abstract class BaseScreen extends ScreenAdapter {
protected final MainGame game;
protected BaseScreen(MainGame game) {
this.game = game;
}
/** 提供可选的资源预加载完成回调 */
public void onAssetsReady() {
}
}

View File

@@ -33,14 +33,12 @@ public class CharacterSelectScreen extends ScreenAdapter implements InputProcess
private SimpleFighter selectedFighter2;
private final List<Texture> bgs = Arrays.asList(
new Texture(Gdx.files.internal("src/main/resources/selectpage/10b_back_blue2p.png")),
//new Texture(Gdx.files.internal("src/main/resources/selectpage/back_door.png")),
new Texture(Gdx.files.internal("src/main/resources/selectpage/11b_back_red1p.png"))
);
new Texture(Gdx.files.internal("selectpage/10b_back_blue2p.png")),
// new Texture(Gdx.files.internal("selectpage/back_door.png")),
new Texture(Gdx.files.internal("selectpage/11b_back_red1p.png")));
private final List<Texture> charsTexts = Arrays.asList(
new Texture(Gdx.files.internal("src/main/resources/selectpage/character_03.png")),
new Texture(Gdx.files.internal("src/main/resources/selectpage/character_00.png"))
);
new Texture(Gdx.files.internal("selectpage/character_03.png")),
new Texture(Gdx.files.internal("selectpage/character_00.png")));
private final List<String> characters = Arrays.asList("Alice", "Reimu", "暂定");
private Texture profile1p = charsTexts.get(0);
private Texture profile2p = charsTexts.get(1);
@@ -49,11 +47,13 @@ public class CharacterSelectScreen extends ScreenAdapter implements InputProcess
private static int selectedIndex = 0;
private static boolean is1P = true;
private static final int BUTTON_WIDTH = 300;
private static final int BUTTON_HEIGHT = 80;
private static final int BUTTON_X = 800;
private static final int CONFIRM_Y = 200;
private static final int BACK_Y = 100;
// 下面这些按钮常量原本用于文本/按钮绘制,当前 UI 逻辑已注释。
// 如果后续恢复 renderTexts() 可重新启用;为减少无用警告暂时注释。
// private static final int BUTTON_WIDTH = 300;
// private static final int BUTTON_HEIGHT = 80;
// private static final int BUTTON_X = 800;
// private static final int CONFIRM_Y = 200;
// private static final int BACK_Y = 100;
public CharacterSelectScreen(MainGame game) {
this.game = game;
@@ -75,13 +75,14 @@ public class CharacterSelectScreen extends ScreenAdapter implements InputProcess
@Override
public void render(float delta) {
new ClearScreen();
// 清屏:使用工具静态方法,避免误用私有构造器
ClearScreen.clear();
int mouseX = Gdx.input.getX();
int mouseY = Gdx.graphics.getHeight() - Gdx.input.getY();
renderBackground();
renderCharacters(multiplayerMode);
// renderTexts();
// renderTexts();
handleInput(mouseX, mouseY);
if (multiplayerMode) {
@@ -99,7 +100,7 @@ public class CharacterSelectScreen extends ScreenAdapter implements InputProcess
private void renderBackground() {
batch.begin();
for (int i = 0; i < bgs.size(); i ++) {
for (int i = 0; i < bgs.size(); i++) {
batch.draw(bgs.get(i), 0, 528 * i, 1920, 528);
}
batch.end();
@@ -107,45 +108,47 @@ public class CharacterSelectScreen extends ScreenAdapter implements InputProcess
private void renderCharacters(boolean multiplayerMode) {
batch.begin();
batch.draw(profile1p, 0, 0, profile1p.getWidth()*3, profile1p.getHeight()*3);
batch.draw(profile2p, 0, 528, profile2p.getWidth()*3, profile2p.getHeight()*3);
batch.draw(profile1p, 0, 0, profile1p.getWidth() * 3, profile1p.getHeight() * 3);
batch.draw(profile2p, 0, 528, profile2p.getWidth() * 3, profile2p.getHeight() * 3);
batch.end();
}
//
// private void renderTexts() {
// batch.begin();
// font.draw(batch, "选择你的角色", 200, 650);
// for (int i = 0; i < characters.size(); i++) {
// int x = 200 + 30;
// int y = 500 - i * 120 + 50;
// font.draw(batch, characters.get(i), x, y);
//
// }
// if (selectedIndex != -1) {
// font.draw(batch, "已选择: " + characters.get(selectedIndex), 200, 100);
// }
// drawButtonText(CONFIRM_Y, "确认");
// drawButtonText(BACK_Y, "返回");
// batch.end();
// }
//
//
// private void renderTexts() {
// batch.begin();
// font.draw(batch, "选择你的角色", 200, 650);
// for (int i = 0; i < characters.size(); i++) {
// int x = 200 + 30;
// int y = 500 - i * 120 + 50;
// font.draw(batch, characters.get(i), x, y);
//
// }
// if (selectedIndex != -1) {
// font.draw(batch, "已选择: " + characters.get(selectedIndex), 200, 100);
// }
// drawButtonText(CONFIRM_Y, "确认");
// drawButtonText(BACK_Y, "返回");
// batch.end();
// }
//
private void handleInput(int mouseX, int mouseY) {
if (selectedFighter1 != null && selectedFighter2 != null) {
if (multiplayerMode) {
// 设置唯一玩家 ID 并发送角色选择
if (NetworkManager.getInstance().getLocalPlayerId() == null) {
String playerId = UUID.randomUUID().toString();
NetworkManager.getInstance().setLocalPlayerId(playerId);
Gdx.app.log("Network", "设置玩家ID: " + playerId);
}
NetworkManager.getInstance().sendCharacterSelection(selectedFighter1.getName());
// 单人模式只要1P选择了角色就进入游戏
if (!multiplayerMode && selectedFighter1 != null) {
game.setScreen(new GameScreen(game, selectedFighter1));
return;
}
// 联机模式等待双方选择当前逻辑仍采用2人都选才进入
if (multiplayerMode && selectedFighter1 != null && selectedFighter2 != null) {
if (NetworkManager.getInstance().getLocalPlayerId() == null) {
String playerId = UUID.randomUUID().toString();
NetworkManager.getInstance().setLocalPlayerId(playerId);
Gdx.app.log("Network", "设置玩家ID: " + playerId);
}
NetworkManager.getInstance().sendCharacterSelection(selectedFighter1.getName());
game.setScreen(new GameScreen(game, selectedFighter1));
}
}
@Override
public void dispose() {
batch.dispose();
@@ -168,7 +171,7 @@ public class CharacterSelectScreen extends ScreenAdapter implements InputProcess
}
value = true;
} else {
//占坑说是
// 占坑说是
}
if (i == Input.Keys.Z) {
if (is1P) {

View File

@@ -3,6 +3,8 @@ package uno.mloluyu.desktop;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
// 日志功能已移除
/**
* Desktop 平台启动器
*/
@@ -10,6 +12,8 @@ public class DesktopLauncher {
public static void main(String[] args) {
// 若需要异常抓取,可在此处添加简单的 Thread.setDefaultUncaughtExceptionHandler
LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
float scale = 1.0F;

View File

@@ -5,6 +5,7 @@ import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.GL20;
import uno.mloluyu.characters.Action;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
@@ -18,9 +19,18 @@ import uno.mloluyu.characters.SimpleFighter;
import uno.mloluyu.characters.AdvancedFighter;
import uno.mloluyu.network.NetworkManager;
import uno.mloluyu.util.ClearScreen;
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<String, SimpleFighter> otherPlayers = new HashMap<>();
@@ -30,31 +40,123 @@ public class GameScreen extends ScreenAdapter {
private SpriteBatch batch;
private ShapeRenderer shapeRenderer;
private OrthographicCamera camera;
private Texture background;
// 世界尺寸(基于背景原始尺寸 * 缩放)。如果需要可读取 Texture 宽高后动态设。
private float worldWidth;
private float worldHeight;
// 背景整体缩放倍数:>1 表示背景比屏幕大,可只显示局部
private static final float BACKGROUND_SCALE = 1.5f;
// 摄像机竖直偏移(正值=镜头上移,让玩家更靠下;这里改为较小的正值防止看不到下方)
private static final float CAMERA_Y_OFFSET = 60f;
// 允许相机向下多看到的底部扩展(不被 clamp 过早挡住),解决放大 hitbox 下半部分出框
private static final float CAMERA_BOTTOM_MARGIN = -30f;
// 平滑跟随的垂直插值系数(独立控制 y防止瞬间跳
private static final float CAMERA_LERP_ALPHA = 0.12f;
// ========== 摄像机缩放配置 ==========
// 是否使用动态缩放(多人时根据距离自动拉远 / 靠近)
private static final boolean CAMERA_DYNAMIC_ZOOM = true;
// (保留)固定缩放模式开关 & 数值;若需要强制固定视角可把 CAMERA_DYNAMIC_ZOOM 设 false
private static final boolean CAMERA_USE_FIXED_ZOOM = false;
private static final float CAMERA_FIXED_ZOOM = 0.80f;
// 动态缩放参数最小与最大OrthographicCamera: <1 视角更近,>1 更远)
private static final float CAMERA_MIN_ZOOM = 0.55f; // 角色很近时
private static final float CAMERA_MAX_ZOOM = 1.25f; // 距离很远时
// 达到最大缩放所对应的“玩家距离”基准(屏幕世界单位,按你的角色移动范围调)
private static final float CAMERA_MAX_DISTANCE = 1800f;
// 缩放插值速度(越大越快贴近目标)
private static final float CAMERA_ZOOM_LERP = 0.10f;
// 显示地面参考线
private static final boolean SHOW_GROUND_LINE = true;
// 半透明地面条带显示
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<AttackEffect> attackEffects = new java.util.ArrayList<>();
private final java.util.List<HitParticle> hitParticles = new java.util.ArrayList<>();
private final java.util.List<RadialRing> radialRings = new java.util.ArrayList<>();
private final java.util.List<SparkParticle> 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);
}
@Override
public void show() {
// 确保角色初始贴地(地面抬高后老存档/默认 0 需要调整)
player.alignToGround();
camera = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
camera.position.set(player.getHitbox().x, player.getHitbox().y, 0); // 初始化摄像机位置
// 初始化缩放:动态优先,否则固定
if (CAMERA_DYNAMIC_ZOOM) {
camera.zoom = CAMERA_MIN_ZOOM;
} else if (CAMERA_USE_FIXED_ZOOM) {
camera.zoom = CAMERA_FIXED_ZOOM;
}
camera.update();
batch = new SpriteBatch();
shapeRenderer = new ShapeRenderer();
Gdx.input.setInputProcessor(controller);
// 背景图(与主菜单共用 bg.png可后续扩展成多关卡背景
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
public void render(float delta) {
new ClearScreen();
delta = TimeStepLimiter.clamp(delta);
ClearScreen.clear();
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<String, float[]> positions = NetworkManager.getInstance().getPlayerPositions();
if (positions != null) {
@@ -150,7 +252,7 @@ public class GameScreen extends ScreenAdapter {
String pid = dt.getKey();
// 重生位置简单放原点附近随机
float rx = (float) (Math.random() * 200 - 100);
float ry = 0;
float ry = GameConstants.GROUND_Y;
NetworkManager.getInstance().sendRespawn(pid, rx, ry);
deathTimers.remove(pid);
}
@@ -195,6 +297,107 @@ public class GameScreen extends ScreenAdapter {
}
respawns.clear();
}
} else if (!gameOver) {
// 离线模式:本地直接做攻击命中判定(本地玩家 -> AIAI -> 本地)
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);
}
}
// 本地与远程玩家之间简单碰撞(防穿人)——放在摄像机更新前
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();
}
}
}
// 摄像机跟随
@@ -204,47 +407,154 @@ public class GameScreen extends ScreenAdapter {
SimpleFighter remote = otherPlayers.values().iterator().next();
float midX = (player.getHitbox().x + remote.getHitbox().x) * 0.5f;
float midY = (player.getHitbox().y + remote.getHitbox().y) * 0.5f;
targetPos = new Vector3(midX, midY, 0);
targetPos = new Vector3(midX, midY + CAMERA_Y_OFFSET, 0);
} else {
// 默认为跟随本地玩家
targetPos = new Vector3(player.getHitbox().x, player.getHitbox().y, 0);
targetPos = new Vector3(player.getHitbox().x, player.getHitbox().y + CAMERA_Y_OFFSET, 0);
}
camera.position.lerp(targetPos, 0.1f);
// 仅对 x,y 做分量插值,可单独调节速度
camera.position.x += (targetPos.x - camera.position.x) * CAMERA_LERP_ALPHA;
camera.position.y += (targetPos.y - camera.position.y) * (CAMERA_LERP_ALPHA * 1.1f);
// 计算动态缩放目标
if (CAMERA_DYNAMIC_ZOOM && otherPlayers.size() >= 1) {
// 取所有玩家(本地 + 远程x 坐标的最大跨度作为距离依据,可扩展为对角线距离
float minX = player.getHitbox().x;
float maxX = player.getHitbox().x;
float minY = player.getHitbox().y;
float maxY = player.getHitbox().y;
for (SimpleFighter r : otherPlayers.values()) {
Rectangle hb = r.getHitbox();
if (hb.x < minX)
minX = hb.x;
if (hb.x > maxX)
maxX = hb.x;
if (hb.y < minY)
minY = hb.y;
if (hb.y > maxY)
maxY = hb.y;
}
float dx = maxX - minX;
float dy = maxY - minY;
// 这里主要横向对战,优先 dx若想考虑纵向可用距离 = max(dx, dy*系数)
float dist = Math.max(dx, dy * 0.6f);
float t = Math.min(1f, dist / CAMERA_MAX_DISTANCE); // 0~1
float targetZoom = CAMERA_MIN_ZOOM + (CAMERA_MAX_ZOOM - CAMERA_MIN_ZOOM) * t;
camera.zoom += (targetZoom - camera.zoom) * CAMERA_ZOOM_LERP;
} else if (CAMERA_USE_FIXED_ZOOM) {
// 固定缩放平滑
if (Math.abs(camera.zoom - CAMERA_FIXED_ZOOM) > 0.0001f) {
camera.zoom += (CAMERA_FIXED_ZOOM - camera.zoom) * 0.25f;
}
}
// 约束摄像机在世界边界内(视口以中心为基准)
float halfW = (camera.viewportWidth * camera.zoom) / 2f;
float halfH = (camera.viewportHeight * camera.zoom) / 2f;
camera.position.x = Math.max(halfW, Math.min(worldWidth - halfW, camera.position.x));
float minY = halfH - CAMERA_BOTTOM_MARGIN; // 允许比世界底部再低一些显示底部区域
camera.position.y = Math.max(minY, Math.min(worldHeight - halfH, camera.position.y));
camera.update();
batch.setProjectionMatrix(camera.combined);
shapeRenderer.setProjectionMatrix(camera.combined);
// -------- Background pass --------
batch.begin();
// 仅绘制背景的局部:通过在更大的缩放空间中直接拉伸整张图并限制摄像机
// 若想真正裁剪一部分,可改用纹理区域;此处使用整图放大后让摄像机在其内游走
batch.draw(background,
0, 0,
worldWidth, worldHeight);
batch.end();
// 混合
Gdx.gl.glEnable(GL20.GL_BLEND);
Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
// -------- Filled pass --------
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
drawHitbox(player, Color.BLUE);
// 半透明地面条带(先于 hitbox 绘制,这样人物在其上方)
if (SHOW_GROUND_STRIP) {
float y = GameConstants.GROUND_Y - GROUND_STRIP_HEIGHT * 0.5f; // 居中穿过地面线
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, 1f, 0f, 0f, 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, Color.GREEN);
if (remote.isAttacking())
drawAttackBox(remote, 1f, 0f, 0f, 0.25f);
drawHitbox(remote, remote.getDebugColor());
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 --------
shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
if (SHOW_GROUND_LINE) {
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);
shapeRenderer.setColor(Color.WHITE);
shapeRenderer.rect(0, 0, 1000, 1000);
shapeRenderer.end();
// -------- UI health bar pass --------
// 使用屏幕坐标绘制血条
@@ -254,38 +564,155 @@ 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();
shapeRenderer.rect(r.x, r.y, r.width, r.height);
float scale = GameConstants.DEBUG_BOX_SCALE;
if (scale <= 1.0001f && scale >= 0.9999f) { // 视为 1
shapeRenderer.rect(r.x, r.y, r.width, r.height);
return;
}
float cx = r.x + r.width / 2f;
float cy = r.y + r.height / 2f;
float w = r.width * scale;
float h = r.height * scale;
if (GameConstants.DEBUG_SCALE_FROM_CENTER) {
shapeRenderer.rect(cx - w / 2f, cy - h / 2f, w, h);
} else {
shapeRenderer.rect(r.x, r.y, w, h);
}
}
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();
shapeRenderer.rect(box.x, box.y, box.width, box.height);
float scale = GameConstants.DEBUG_BOX_SCALE;
if (scale <= 1.0001f && scale >= 0.9999f) {
shapeRenderer.rect(box.x, box.y, box.width, box.height);
return;
}
float cx = box.x + box.width / 2f;
float cy = box.y + box.height / 2f;
float w = box.width * scale;
float h = box.height * scale;
if (GameConstants.DEBUG_SCALE_FROM_CENTER) {
shapeRenderer.rect(cx - w / 2f, cy - h / 2f, w, h);
} else {
shapeRenderer.rect(box.x, box.y, w, h);
}
}
// private void checkPlayerAttacks() {
@@ -303,6 +730,160 @@ public class GameScreen extends ScreenAdapter {
public void dispose() {
batch.dispose();
shapeRenderer.dispose();
if (background != null)
background.dispose();
if (uiFont != null)
uiFont.dispose();
NetworkManager.getInstance().disconnect();
}
/**
* 解决本地玩家与每个远程玩家的水平重叠,避免“穿人”视觉效果。
* 当前策略:仅移动本地玩家(不修改远程玩家坐标,避免产生需要网络回传的状态)。
* 若需要更严格的对等碰撞,可在主机端双向分离并广播,但此处先满足基本需求。
*/
private void resolvePlayerCollisions() {
if (otherPlayers.isEmpty())
return;
Rectangle a = player.getHitbox();
for (SimpleFighter remote : otherPlayers.values()) {
Rectangle b = remote.getHitbox();
if (!a.overlaps(b))
continue;
// 仅考虑水平最小位移分离2D 侧向格斗常见做法)
float axCenter = a.x + a.width / 2f;
float bxCenter = b.x + b.width / 2f;
float dx = axCenter - bxCenter; // 正值:本地在右侧
float overlapX = (a.width + b.width) / 2f - Math.abs(dx);
if (overlapX > 0) {
if (dx >= 0) {
a.x += overlapX; // 本地向右推
} else {
a.x -= overlapX; // 本地向左推
}
// 世界边界限制(假设世界从 0 开始到 worldWidth
if (a.x < 0)
a.x = 0;
if (a.x + a.width > worldWidth)
a.x = worldWidth - a.width;
}
}
}
/** 限制角色 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<String, SimpleFighter> 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); }
}

View File

@@ -1,8 +1,6 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Texture;
public class MainGame extends Game {
public static final float WORLD_WIDTH = 1920;

View File

@@ -9,8 +9,6 @@ import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import uno.mloluyu.versatile.FighterController;
import static uno.mloluyu.util.Font.loadChineseFont;
public class MainMenuScreen extends ScreenAdapter {
@@ -32,7 +30,7 @@ public class MainMenuScreen extends ScreenAdapter {
public MainMenuScreen(MainGame game) {
this.game = game;
texture = new Texture(Gdx.files.internal("src\\main\\resources\\bg.png"));
texture = new Texture(Gdx.files.internal("bg.png"));
}
@Override
@@ -46,7 +44,7 @@ public class MainMenuScreen extends ScreenAdapter {
@Override
public void render(float delta) {
Gdx.gl.glClearColor(0,0,0,0);
Gdx.gl.glClearColor(0, 0, 0, 0);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
int mouseX = Gdx.input.getX();
@@ -76,6 +74,7 @@ public class MainMenuScreen extends ScreenAdapter {
game.setScreen(new CharacterSelectScreen(game));
} else if (isTouched(mouseX, mouseY, buttonX, settingsY)) {
Gdx.app.log("Button", "设置按钮被点击!");
game.setScreen(new SettingsScreen(game));
} else if (isTouched(mouseX, mouseY, buttonX, networkY)) {
Gdx.app.log("Button", "联网设置按钮被点击!");
game.setScreen(new NetworkSettingsScreen(game));
@@ -92,8 +91,7 @@ public class MainMenuScreen extends ScreenAdapter {
}
private void drawButtonText(int y, String text) {
float textWidth = font.getRegion().getRegionWidth(); // 粗略估算
float textX = buttonX + buttonWidth / 2f - text.length() * 20; // 居中估算
float textX = buttonX + buttonWidth / 2f - text.length() * 20; // 简单估算居中
float textY = y + buttonHeight / 2f + 20;
font.draw(batch, text, textX, textY);
}
@@ -104,8 +102,11 @@ public class MainMenuScreen extends ScreenAdapter {
@Override
public void dispose() {
if (batch != null) batch.dispose();
if (font != null) font.dispose();
if (shapeRenderer != null) shapeRenderer.dispose();
if (batch != null)
batch.dispose();
if (font != null)
font.dispose();
if (shapeRenderer != null)
shapeRenderer.dispose();
}
}

View File

@@ -3,7 +3,6 @@ package uno.mloluyu.desktop;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
@@ -40,7 +39,8 @@ public class NetworkSettingsScreen extends ScreenAdapter {
@Override
public void render(float delta) {
new ClearScreen();
// 使用静态工具方法清屏
ClearScreen.clear();
int mouseX = Gdx.input.getX();
int mouseY = Gdx.graphics.getHeight() - Gdx.input.getY();
@@ -64,6 +64,12 @@ public class NetworkSettingsScreen extends ScreenAdapter {
drawButtonText(CREATE_ROOM_Y, "创建房间");
drawButtonText(JOIN_ROOM_Y, "加入房间");
drawButtonText(EXIT_Y, "返回");
// 状态信息
NetworkManager nm = NetworkManager.getInstance();
String id = nm.getLocalPlayerId();
font.draw(batch, "本机ID: " + (id == null ? "(未分配)" : id.substring(0, Math.min(8, id.length()))), 50, 200);
font.draw(batch, nm.isHost() ? "当前: 房主" : (nm.isConnected() ? "当前: 已连接客户端" : "当前: 未连接"), 50, 160);
font.draw(batch, "在线玩家: " + (nm.getPlayerPositions() == null ? 0 : nm.getPlayerPositions().size()), 50, 120);
batch.end();
}

View File

@@ -0,0 +1,25 @@
package uno.mloluyu.desktop;
/** 简易屏幕管理器,负责切换与异常保护。 */
public class ScreenManager {
private final MainGame game;
private BaseScreen current;
public ScreenManager(MainGame game) {
this.game = game;
}
public void set(BaseScreen next) {
try {
if (current != null) {
current.hide();
current.dispose();
}
current = next;
game.setScreen(next);
} catch (Throwable t) {
// 忽略或可加简单 System.err
t.printStackTrace();
}
}
}

View File

@@ -0,0 +1,75 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import uno.mloluyu.util.ClearScreen;
import static uno.mloluyu.util.Font.loadChineseFont;
/**
* 简单设置界面占位:未来可扩展(音量/按键设置)。
*/
public class SettingsScreen extends ScreenAdapter {
private final MainGame game;
private SpriteBatch batch;
private BitmapFont font;
private ShapeRenderer shapeRenderer;
private static final int BACK_X = 100; // 返回按钮区域
private static final int BACK_Y = 100;
private static final int BACK_W = 220;
private static final int BACK_H = 70;
public SettingsScreen(MainGame game) {
this.game = game;
}
@Override
public void show() {
batch = new SpriteBatch();
shapeRenderer = new ShapeRenderer();
font = loadChineseFont();
font.setColor(Color.WHITE);
font.getData().setScale(2f);
}
@Override
public void render(float delta) {
ClearScreen.clear();
int mouseX = Gdx.input.getX();
int mouseY = Gdx.graphics.getHeight() - Gdx.input.getY();
// 绘制背景与返回按钮
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
shapeRenderer.setColor(Color.DARK_GRAY);
shapeRenderer.rect(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
boolean backHover = isHovered(mouseX, mouseY, BACK_X, BACK_Y, BACK_W, BACK_H);
shapeRenderer.setColor(backHover ? Color.LIGHT_GRAY : Color.GRAY);
shapeRenderer.rect(BACK_X, BACK_Y, BACK_W, BACK_H);
shapeRenderer.end();
batch.begin();
font.draw(batch, "设置 (占位界面)", 100, Gdx.graphics.getHeight() - 120);
font.draw(batch, "此处可添加: 音量 / 按键 / 分辨率 / 语言 等", 100, Gdx.graphics.getHeight() - 180);
font.draw(batch, "返回", BACK_X + 50, BACK_Y + 45);
batch.end();
if (Gdx.input.justTouched() && backHover) {
game.setScreen(new MainMenuScreen(game));
}
}
private boolean isHovered(int x, int y, int bx, int by, int bw, int bh) {
return x >= bx && x <= bx + bw && y >= by && y <= by + bh;
}
@Override
public void dispose() {
batch.dispose();
font.dispose();
shapeRenderer.dispose();
}
}

View File

@@ -1,17 +1,17 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import uno.mloluyu.util.ResourcePaths;
/**
* 启动屏幕类
* 显示游戏Logo并在3秒后切换到主菜单界面
*/
public class StartScreen implements Screen {
public class StartScreen extends BaseScreen {
private MainGame mainGame;
private MainGame mainGame; // TODO: 后续可直接用BaseScreen.game
private Texture logoTexture;
private com.badlogic.gdx.graphics.g2d.SpriteBatch batch;
@@ -19,8 +19,9 @@ public class StartScreen implements Screen {
private float deltaSum;
public StartScreen(MainGame mainGame) {
super(mainGame);
this.mainGame = mainGame;
logoTexture = new Texture(Gdx.files.internal("src\\main\\resources\\logo.png"));
logoTexture = new Texture(Gdx.files.internal(ResourcePaths.LOGO));
batch = new com.badlogic.gdx.graphics.g2d.SpriteBatch();
}
@@ -33,10 +34,9 @@ public class StartScreen implements Screen {
public void render(float delta) {
deltaSum += delta;
if (deltaSum >= .01F) {
if (deltaSum >= 3.0F) {
if (mainGame != null) {
mainGame.showGameScreen();
System.out.println("已经切换到主菜单");
return;
}
}

View File

@@ -73,6 +73,14 @@ public class NetworkManager {
}
}
/**
* 本地玩家所选角色(仅本地缓存,远程映射存于 playerCharacters
* 供界面或后续同步逻辑查询。
*/
public String getLocalCharacter() {
return localCharacter;
}
public void receiveMessage(String message) {// 解析消息
if (message.startsWith("POS:")) {

View File

@@ -0,0 +1,20 @@
package uno.mloluyu.perf;
/** 简单性能指标收集(首版)。 */
public final class PerfMetrics {
private static long frameCount;
private static double accumTime;
private static double maxFrame = 0;
public static void frame(double delta) {
frameCount++;
accumTime += delta;
if (delta > maxFrame)
maxFrame = delta;
}
public static String summary() {
double avg = frameCount == 0 ? 0 : accumTime / frameCount;
return String.format("frames=%d avg=%.4f max=%.4f", frameCount, avg, maxFrame);
}
}

View File

@@ -3,8 +3,12 @@ package uno.mloluyu.util;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
public class ClearScreen {
public ClearScreen() {
/** 清屏工具:改为静态方法避免每帧 new 对象。 */
public final class ClearScreen {
private ClearScreen() {
}
public static void clear() {
Gdx.gl.glClearColor(0.3F, 0.3F, 0.5F, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
}

View File

@@ -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) {

View File

@@ -0,0 +1,40 @@
package uno.mloluyu.util;
/**
* 全局游戏常量集中放置。
*/
public final class GameConstants {
/**
* 地面空气墙Y 坐标。抬高后便于更好构图。
*/
public static final float GROUND_Y = 180f; // 可按需要再调
// 角色移动/物理参数(集中配置便于统一手感调节)
// 用户要求:速度 *2跳跃 *3基于当前 520 / 1250
public static final float MOVE_SPEED = 1040f; // 520 *2 (最初 ~300)
// 调低跳跃:更低高度 + 更短滞空v0 ↓,同时重力 ↑
// 说明:上升时间 t_up = v0 / g本次取 v0=1500, g=3200 =>
// t_up≈0.47s总滞空≈0.94s,高度≈(v0^2)/(2g)≈351
// 若想再更低JUMP_SPEED 1400 + GRAVITY 3400再更高一点JUMP_SPEED 1600 + GRAVITY 3000。
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;
// 是否按中心放大true 则保持角色中心位置,不会视觉漂移)
public static final boolean DEBUG_SCALE_FROM_CENTER = true;
// (可选)相机或后续平衡参数也可集中放这里
private GameConstants() {
}
}

View File

@@ -0,0 +1,12 @@
package uno.mloluyu.util;
/** 统一资源常量,避免魔法字符串散落。 */
public final class ResourcePaths {
private ResourcePaths() {
}
public static final String LOGO = "logo.png";
public static final String FONT_MAIN = "FLyouzichati-Regular-2.ttf";
public static final String CHARACTER_ROOT = "character/";
public static final String UI_SKIN_JSON = "ui/uiskin.json";
}

View File

@@ -0,0 +1,13 @@
package uno.mloluyu.util;
/** 限制delta时间防止窗口拖拽/卡顿后出现物理跳跃。 */
public final class TimeStepLimiter {
private TimeStepLimiter() {
}
private static final float MAX_DELTA = 1f / 30f; // 上限: 相当于最低30FPS
public static float clamp(float delta) {
return delta > MAX_DELTA ? MAX_DELTA : Math.max(delta, 0f);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@@ -0,0 +1,22 @@
package uno.mloluyu.assets;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class AssetsExistenceTest {
private static final Path RES = Path.of("src", "main", "resources");
@Test
void testCoreAssetsPresent() {
List<String> required = List.of("logo.png", "character/alice/alice.png", "character/reimu/reimu-0.png",
"ui/uiskin.json");
for (String r : required) {
Path p = RES.resolve(r);
Assertions.assertTrue(Files.exists(p), "缺失资源: " + r);
}
}
}

View File

@@ -0,0 +1,21 @@
package uno.mloluyu.characters;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class ActionStateTest {
@Test
void testLegalTransitionIdleToMove() {
SimpleFighter f = new SimpleFighter("Test");
f.changeAction(Action.MOVE);
Assertions.assertEquals(Action.MOVE, f.getCurrentAction());
}
@Test
void testIllegalTransitionDeadToMove() {
SimpleFighter f = new SimpleFighter("Test");
f.changeAction(Action.DEAD);
f.changeAction(Action.MOVE); // 应被拒绝
Assertions.assertEquals(Action.DEAD, f.getCurrentAction());
}
}

BIN
target/classes/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
target/classes/innerbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

View File

@@ -0,0 +1,24 @@
uiskin.png
format: RGBA8888
filter: Nearest,Nearest
repeat: none
button-up
rotate: false
xy: 0, 0
size: 200, 60
split: 10, 10, 10, 10
orig: 200, 60
offset: 0, 0
button-down
rotate: false
xy: 0, 60
size: 200, 60
split: 10, 10, 10, 10
orig: 200, 60
offset: 0, 0
white
rotate: false
xy: 0, 120
size: 1, 1
orig: 1, 1
offset: 0, 0

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<form>
<label for="username">用户名:</label>
<input type="text" id="username" name="username"><br><br>
<input type="checkbox" id="remember">
<label for="remember">记住用户名</label><br><br>
<input type="button" value="提交" onclick="submitForm()">
</form>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,2 +1,19 @@
uno\mloluyu\network\ConnectServer.class
uno\mloluyu\desktop\NetworkSettingsScreen$1.class
uno\mloluyu\network\NetworkManager.class
uno\mloluyu\characters\FighterAnimationManager.class
uno\mloluyu\versatile\FighterController.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\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

View File

@@ -1,17 +1,31 @@
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\characters\Action.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\SimpleFighter.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\desktop\CharacterSelectScreen.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\desktop\DesktopLauncher.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\desktop\GameScreen.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\desktop\MainGame.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\desktop\MainMenuScreen.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\desktop\NetworkSettingsScreen.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\desktop\StartScreen.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\network\ConnectClient.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\network\ConnectServer.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\network\NetworkManager.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\util\ClearScreen.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\util\Font.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\util\SimpleFormatter.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\versatile\FighterController.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\Action.java
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
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\BaseScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\CharacterSelectScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\DesktopLauncher.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\GameScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\MainGame.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\MainMenuScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\NetworkSettingsScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\ScreenManager.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\SettingsScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\StartScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\network\ConnectClient.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\network\ConnectServer.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\network\NetworkManager.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\perf\PerfMetrics.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\util\ClearScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\util\Font.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\util\GameConstants.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\util\ResourcePaths.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\util\SimpleFormatter.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\util\TimeStepLimiter.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\versatile\FighterController.java

View File

@@ -0,0 +1,2 @@
C:\Users\www\Documents\Game\格斗游戏\Game\src\test\java\uno\mloluyu\assets\AssetsExistenceTest.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\test\java\uno\mloluyu\characters\ActionStateTest.java