update
Build / build (push) Has been cancelled

This commit is contained in:
gpatruno
2026-06-08 23:35:37 +02:00
parent 836499d10d
commit 96144d809c
78 changed files with 5871 additions and 502 deletions
+42
View File
@@ -1,4 +1,5 @@
import org.gradle.jvm.toolchain.JavaLanguageVersion import org.gradle.jvm.toolchain.JavaLanguageVersion
import org.gradle.api.file.DuplicatesStrategy
plugins { plugins {
id("com.gradleup.shadow") version "9.0.0-rc1" id("com.gradleup.shadow") version "9.0.0-rc1"
@@ -30,6 +31,7 @@ dependencies {
tasks.shadowJar { tasks.shadowJar {
archiveClassifier.set("") archiveClassifier.set("")
mergeServiceFiles() mergeServiceFiles()
duplicatesStrategy = DuplicatesStrategy.INCLUDE
} }
tasks.jar { tasks.jar {
@@ -43,3 +45,43 @@ tasks.assemble {
tasks.build { tasks.build {
dependsOn(tasks.shadowJar) dependsOn(tasks.shadowJar)
} }
tasks.register<Delete>("cleanDevModUiAssets") {
delete("src/main/resources/Common")
}
tasks.register<Delete>("cleanDevModAssetPack") {
delete("devserver/mods/com.disklexar_MMORPG/Common")
delete("devserver/mods/com.disklexar_MMORPG/manifest.json")
}
tasks.register<Copy>("syncDevModAssetPack") {
dependsOn("cleanDevModAssetPack")
from("plugins/mmorpg/src/main/resources") {
include("Common/**")
include("manifest.json")
}
into("devserver/mods/com.disklexar_MMORPG")
}
tasks.register<Copy>("syncDevModAssets") {
dependsOn("generateManifest", "cleanDevModUiAssets", "syncDevModAssetPack")
from("plugins/mmorpg/src/main/resources") {
include("Server/**")
include("Common/**")
include("manifest.json")
}
into("src/main/resources")
}
tasks.named("runServer") {
dependsOn("syncDevModAssets")
}
tasks.named("setupServer") {
dependsOn("syncDevModAssets")
}
tasks.named("processResources") {
finalizedBy("syncDevModAssets")
}
@@ -0,0 +1,13 @@
{
"SpawnProvider": {
"Type": "Global",
"SpawnPoint": {
"X": 195.0,
"Y": 98.0,
"Z": -44.0,
"Pitch": 0.0,
"Yaw": 0.0,
"Roll": 0.0
}
}
}
@@ -7,12 +7,18 @@ import com.disklexar.mmorpg.combat.CombatBuffService;
import com.disklexar.mmorpg.combat.CombatStateService; import com.disklexar.mmorpg.combat.CombatStateService;
import com.disklexar.mmorpg.combat.PoisonService; import com.disklexar.mmorpg.combat.PoisonService;
import com.disklexar.mmorpg.combat.system.MinerBlockBreakSystem; import com.disklexar.mmorpg.combat.system.MinerBlockBreakSystem;
import com.disklexar.mmorpg.combat.system.MmorpgAttackSpeedSystem;
import com.disklexar.mmorpg.combat.system.MmorpgDamageSystem; import com.disklexar.mmorpg.combat.system.MmorpgDamageSystem;
import com.disklexar.mmorpg.combat.MmorpgScheduler;
import com.disklexar.mmorpg.combat.system.MmorpgDeathSystem; import com.disklexar.mmorpg.combat.system.MmorpgDeathSystem;
import com.disklexar.mmorpg.combat.system.MmorpgPlayerDeathSystem;
import com.disklexar.mmorpg.combat.system.MmorpgPlayerRespawnSystem;
import com.disklexar.mmorpg.player.CharacterService;
import com.disklexar.mmorpg.command.MmorpgCommand; import com.disklexar.mmorpg.command.MmorpgCommand;
import com.disklexar.mmorpg.economy.EconomyService; import com.disklexar.mmorpg.economy.EconomyService;
import com.disklexar.mmorpg.events.AbilityPacketFilter; import com.disklexar.mmorpg.events.AbilityPacketFilter;
import com.disklexar.mmorpg.events.PlayerConnectionHandler; import com.disklexar.mmorpg.events.PlayerConnectionHandler;
import com.disklexar.mmorpg.interaction.MmorpgArcherShootInteraction;
import com.disklexar.mmorpg.interaction.MmorpgCastAbilityInteraction; import com.disklexar.mmorpg.interaction.MmorpgCastAbilityInteraction;
import com.disklexar.mmorpg.player.OnlinePlayerRegistry; import com.disklexar.mmorpg.player.OnlinePlayerRegistry;
import com.disklexar.mmorpg.player.PlayerSessionService; import com.disklexar.mmorpg.player.PlayerSessionService;
@@ -20,6 +26,7 @@ import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.progression.ProgressionService; import com.disklexar.mmorpg.progression.ProgressionService;
import com.disklexar.mmorpg.rpg.group.GroupService; import com.disklexar.mmorpg.rpg.group.GroupService;
import com.disklexar.mmorpg.rpg.job.JobService; import com.disklexar.mmorpg.rpg.job.JobService;
import com.disklexar.mmorpg.rpg.stats.CharacterStatService;
import com.disklexar.mmorpg.ui.PlayerInfoPage; import com.disklexar.mmorpg.ui.PlayerInfoPage;
import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent; import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent;
import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent; import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
@@ -85,6 +92,10 @@ public final class MmorpgPlugin extends JavaPlugin {
MmorpgCastAbilityInteraction.ID, MmorpgCastAbilityInteraction.ID,
MmorpgCastAbilityInteraction.class, MmorpgCastAbilityInteraction.class,
MmorpgCastAbilityInteraction.CODEC); MmorpgCastAbilityInteraction.CODEC);
getCodecRegistry(Interaction.CODEC).register(
MmorpgArcherShootInteraction.ID,
MmorpgArcherShootInteraction.class,
MmorpgArcherShootInteraction.CODEC);
getCommandRegistry().registerCommand(new MmorpgCommand(this)); getCommandRegistry().registerCommand(new MmorpgCommand(this));
getEventRegistry().registerGlobal(PlayerReadyEvent.class, connectionHandler::onPlayerReady); getEventRegistry().registerGlobal(PlayerReadyEvent.class, connectionHandler::onPlayerReady);
@@ -107,12 +118,21 @@ public final class MmorpgPlugin extends JavaPlugin {
GroupService groups = registry.require(GroupService.class); GroupService groups = registry.require(GroupService.class);
JobService jobs = registry.require(JobService.class); JobService jobs = registry.require(JobService.class);
EconomyService economy = registry.require(EconomyService.class); EconomyService economy = registry.require(EconomyService.class);
CharacterService characterService = registry.require(CharacterService.class);
CharacterStatService characterStats = registry.require(CharacterStatService.class);
MmorpgScheduler scheduler = registry.require(MmorpgScheduler.class);
int killExperience = bootstrap.getConfig().getKillExperience(); int killExperience = bootstrap.getConfig().getKillExperience();
getEntityStoreRegistry().registerSystem( getEntityStoreRegistry().registerSystem(
new MmorpgDamageSystem(sessions, combatState, combatBuffs, poisonService, onlinePlayers)); new MmorpgDamageSystem(sessions, combatState, combatBuffs, poisonService, onlinePlayers, characterStats));
getEntityStoreRegistry().registerSystem(
new MmorpgAttackSpeedSystem(sessions, characterStats));
getEntityStoreRegistry().registerSystem( getEntityStoreRegistry().registerSystem(
new MmorpgDeathSystem(sessions, progression, groups, jobs, economy, killExperience)); new MmorpgDeathSystem(sessions, progression, groups, jobs, economy, killExperience));
getEntityStoreRegistry().registerSystem(
new MmorpgPlayerDeathSystem(sessions, characterService));
getEntityStoreRegistry().registerSystem(
new MmorpgPlayerRespawnSystem(characterService, scheduler));
getEntityStoreRegistry().registerSystem(new MinerBlockBreakSystem(sessions, jobs, economy)); getEntityStoreRegistry().registerSystem(new MinerBlockBreakSystem(sessions, jobs, economy));
OpenCustomUIInteraction.registerSimple(this, PlayerInfoPage.class, "mmorpg_player_info", playerRef -> { OpenCustomUIInteraction.registerSimple(this, PlayerInfoPage.class, "mmorpg_player_info", playerRef -> {
@@ -2,6 +2,7 @@ package com.disklexar.mmorpg.bootstrap;
import com.disklexar.mmorpg.MmorpgPlugin; import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.combat.AbilityService; import com.disklexar.mmorpg.combat.AbilityService;
import com.disklexar.mmorpg.combat.ArcherBowService;
import com.disklexar.mmorpg.combat.CombatBuffService; import com.disklexar.mmorpg.combat.CombatBuffService;
import com.disklexar.mmorpg.combat.CombatStateService; import com.disklexar.mmorpg.combat.CombatStateService;
import com.disklexar.mmorpg.combat.MmorpgScheduler; import com.disklexar.mmorpg.combat.MmorpgScheduler;
@@ -25,6 +26,7 @@ import com.disklexar.mmorpg.rpg.group.GroupService;
import com.disklexar.mmorpg.rpg.job.JobService; import com.disklexar.mmorpg.rpg.job.JobService;
import com.disklexar.mmorpg.rpg.power.PowerService; import com.disklexar.mmorpg.rpg.power.PowerService;
import com.disklexar.mmorpg.rpg.race.RaceService; import com.disklexar.mmorpg.rpg.race.RaceService;
import com.disklexar.mmorpg.rpg.stats.CharacterStatService;
import com.disklexar.mmorpg.ui.AbilityBarService; import com.disklexar.mmorpg.ui.AbilityBarService;
import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.logger.HytaleLogger;
@@ -69,6 +71,7 @@ public final class Bootstrap {
PlayerSessionService sessionService = new PlayerSessionService( PlayerSessionService sessionService = new PlayerSessionService(
plugin, config, accountRepository, characterRepository); plugin, config, accountRepository, characterRepository);
CharacterStatService characterStats = new CharacterStatService(sessionService);
CharacterInventoryService characterInventoryService = CharacterInventoryService characterInventoryService =
new CharacterInventoryService(inventoryRepository); new CharacterInventoryService(inventoryRepository);
CharacterService characterService = new CharacterService( CharacterService characterService = new CharacterService(
@@ -83,9 +86,11 @@ public final class Bootstrap {
GroupService groupService = GroupService groupService =
new GroupService(plugin, groupRepository, sessionService, progression); new GroupService(plugin, groupRepository, sessionService, progression);
AbilityService abilityService = AbilityService abilityService =
new AbilityService(scheduler, stunService, combatState, combatBuffs); new AbilityService(scheduler, stunService, combatState, combatBuffs, sessionService, characterStats);
ArcherBowService archerBowService =
new ArcherBowService(sessionService, classService, combatState, combatBuffs);
PassiveEffectService passiveEffects = PassiveEffectService passiveEffects =
new PassiveEffectService(plugin, scheduler, onlinePlayers, sessionService, combatState); new PassiveEffectService(plugin, scheduler, onlinePlayers, sessionService, combatState, characterStats);
AbilityBarService abilityBarService = new AbilityBarService( AbilityBarService abilityBarService = new AbilityBarService(
plugin, classService, abilityService, onlinePlayers, sessionService, progression, scheduler); plugin, classService, abilityService, onlinePlayers, sessionService, progression, scheduler);
@@ -107,11 +112,13 @@ public final class Bootstrap {
registry.register(StunService.class, stunService); registry.register(StunService.class, stunService);
registry.register(OnlinePlayerRegistry.class, onlinePlayers); registry.register(OnlinePlayerRegistry.class, onlinePlayers);
registry.register(PlayerSessionService.class, sessionService); registry.register(PlayerSessionService.class, sessionService);
registry.register(CharacterStatService.class, characterStats);
registry.register(CharacterInventoryService.class, characterInventoryService); registry.register(CharacterInventoryService.class, characterInventoryService);
registry.register(CharacterService.class, characterService); registry.register(CharacterService.class, characterService);
registry.register(MmorpgScheduler.class, scheduler); registry.register(MmorpgScheduler.class, scheduler);
registry.register(GroupService.class, groupService); registry.register(GroupService.class, groupService);
registry.register(AbilityService.class, abilityService); registry.register(AbilityService.class, abilityService);
registry.register(ArcherBowService.class, archerBowService);
registry.register(AbilityBarService.class, abilityBarService); registry.register(AbilityBarService.class, abilityBarService);
registry.register(PassiveEffectService.class, passiveEffects); registry.register(PassiveEffectService.class, passiveEffects);
@@ -1,8 +1,11 @@
package com.disklexar.mmorpg.combat; package com.disklexar.mmorpg.combat;
import com.disklexar.mmorpg.player.PlayerSessionService;
import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.clazz.AbilityDefinition; import com.disklexar.mmorpg.rpg.clazz.AbilityDefinition;
import com.disklexar.mmorpg.rpg.clazz.ClassCatalog; import com.disklexar.mmorpg.rpg.clazz.ClassCatalog;
import com.disklexar.mmorpg.rpg.clazz.PlayerClass; import com.disklexar.mmorpg.rpg.clazz.PlayerClass;
import com.disklexar.mmorpg.rpg.stats.CharacterStatService;
import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.entity.UUIDComponent; import com.hypixel.hytale.server.core.entity.UUIDComponent;
@@ -13,6 +16,7 @@ import com.hypixel.hytale.server.core.modules.entity.component.TransformComponen
import com.hypixel.hytale.server.core.modules.entity.damage.Damage; import com.hypixel.hytale.server.core.modules.entity.damage.Damage;
import com.hypixel.hytale.server.core.modules.entity.damage.DamageCause; import com.hypixel.hytale.server.core.modules.entity.damage.DamageCause;
import com.hypixel.hytale.server.core.modules.entity.damage.DamageSystems; import com.hypixel.hytale.server.core.modules.entity.damage.DamageSystems;
import com.hypixel.hytale.server.core.modules.entitystats.EntityStatMap;
import com.hypixel.hytale.server.core.modules.physics.component.Velocity; import com.hypixel.hytale.server.core.modules.physics.component.Velocity;
import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.World;
@@ -61,8 +65,9 @@ public final class AbilityService {
private static final double VOLLEY_SPREAD_DEG = 9.0; private static final double VOLLEY_SPREAD_DEG = 9.0;
private static final float VOLLEY_ARROW_DAMAGE = 4.0f; private static final float VOLLEY_ARROW_DAMAGE = 4.0f;
private static final double VOLLEY_RANGE = 18.0; private static final double VOLLEY_RANGE = 18.0;
private static final double VOLLEY_BACK_KNOCKBACK = 12.0; private static final double VOLLEY_HIT_RADIUS = 2.0;
private static final double VOLLEY_BACK_KNOCKBACK_Y = 3.0; private static final double VOLLEY_BACK_KNOCKBACK = 24.0;
private static final double VOLLEY_BACK_KNOCKBACK_Y = 6.0;
// --- Mage --- // --- Mage ---
private static final int FIREBALL_COUNT = 3; private static final int FIREBALL_COUNT = 3;
@@ -70,6 +75,8 @@ public final class AbilityService {
private static final float FIREBALL_DAMAGE = 7.0f; private static final float FIREBALL_DAMAGE = 7.0f;
private static final double FIREBALL_RANGE = 20.0; private static final double FIREBALL_RANGE = 20.0;
private static final double FIREBALL_HIT_RADIUS = 1.6; private static final double FIREBALL_HIT_RADIUS = 1.6;
private static final double PROJECTILE_TRACE_STEP = 0.5;
private static final double PROJECTILE_CYLINDER_HEIGHT = 2.5;
private static final double TELEPORT_DISTANCE = 8.0; private static final double TELEPORT_DISTANCE = 8.0;
private static final double TORNADO_DISTANCE = 15.0; private static final double TORNADO_DISTANCE = 15.0;
private static final double TORNADO_RADIUS = 3.5; private static final double TORNADO_RADIUS = 3.5;
@@ -124,6 +131,10 @@ public final class AbilityService {
private final StunService stunService; private final StunService stunService;
private final CombatStateService combatState; private final CombatStateService combatState;
private final CombatBuffService combatBuffs; private final CombatBuffService combatBuffs;
@Nullable
private final PlayerSessionService sessions;
@Nullable
private final CharacterStatService characterStats;
private final Map<UUID, Map<String, Long>> cooldowns = new ConcurrentHashMap<>(); private final Map<UUID, Map<String, Long>> cooldowns = new ConcurrentHashMap<>();
@@ -132,10 +143,22 @@ public final class AbilityService {
@Nonnull StunService stunService, @Nonnull StunService stunService,
@Nonnull CombatStateService combatState, @Nonnull CombatStateService combatState,
@Nonnull CombatBuffService combatBuffs) { @Nonnull CombatBuffService combatBuffs) {
this(scheduler, stunService, combatState, combatBuffs, null, null);
}
public AbilityService(
@Nonnull MmorpgScheduler scheduler,
@Nonnull StunService stunService,
@Nonnull CombatStateService combatState,
@Nonnull CombatBuffService combatBuffs,
@Nullable PlayerSessionService sessions,
@Nullable CharacterStatService characterStats) {
this.scheduler = scheduler; this.scheduler = scheduler;
this.stunService = stunService; this.stunService = stunService;
this.combatState = combatState; this.combatState = combatState;
this.combatBuffs = combatBuffs; this.combatBuffs = combatBuffs;
this.sessions = sessions;
this.characterStats = characterStats;
} }
public long cooldownRemaining(@Nonnull UUID player, @Nonnull String abilityId) { public long cooldownRemaining(@Nonnull UUID player, @Nonnull String abilityId) {
@@ -210,8 +233,15 @@ public final class AbilityService {
} }
combatState.markCombat(caster); combatState.markCombat(caster);
long cooldownMillis = ability.cooldownMillis();
if (sessions != null && characterStats != null) {
PlayerProfile profile = sessions.getSession(caster);
if (profile != null) {
cooldownMillis = characterStats.cooldownMillis(profile, cooldownMillis);
}
}
cooldowns.computeIfAbsent(caster, k -> new ConcurrentHashMap<>()) cooldowns.computeIfAbsent(caster, k -> new ConcurrentHashMap<>())
.put(ability.id(), System.currentTimeMillis() + ability.cooldownMillis()); .put(ability.id(), System.currentTimeMillis() + cooldownMillis);
return CastResult.OK; return CastResult.OK;
} }
@@ -403,11 +433,6 @@ public final class AbilityService {
@Nonnull Ref<EntityStore> casterRef, @Nonnull Ref<EntityStore> casterRef,
@Nonnull UUID caster) { @Nonnull UUID caster) {
combatBuffs.grantRapidFire(caster, RAPID_FIRE_DURATION); combatBuffs.grantRapidFire(caster, RAPID_FIRE_DURATION);
world.execute(() -> {
Vector3d pos = position(store, casterRef);
CombatFx.vfx(store, VFX_BUFF, pos);
CombatFx.sound(store, SFX_ACTIVATE, pos, 1.0f, 1.4f);
});
} }
private void castVolley( private void castVolley(
@@ -419,13 +444,13 @@ public final class AbilityService {
// Bond en arrière. // Bond en arrière.
CombatFx.knockback(store, casterRef, new Vector3d( CombatFx.knockback(store, casterRef, new Vector3d(
-facing.x * VOLLEY_BACK_KNOCKBACK, VOLLEY_BACK_KNOCKBACK_Y, -facing.z * VOLLEY_BACK_KNOCKBACK)); -facing.x * VOLLEY_BACK_KNOCKBACK, VOLLEY_BACK_KNOCKBACK_Y, -facing.z * VOLLEY_BACK_KNOCKBACK));
Vector3d eye = eyePosition(store, casterRef); Vector3d launch = new Vector3d(position(store, casterRef)).add(0, 1.0, 0);
CombatFx.sound(store, SFX_BOW, eye, 1.0f, 1.1f); CombatFx.sound(store, SFX_BOW, launch, 1.0f, 1.1f);
double start = -(VOLLEY_ARROWS - 1) / 2.0; double start = -(VOLLEY_ARROWS - 1) / 2.0;
for (int i = 0; i < VOLLEY_ARROWS; i++) { for (int i = 0; i < VOLLEY_ARROWS; i++) {
Vector3d dir = rotatedHorizontal(facing, (start + i) * VOLLEY_SPREAD_DEG); Vector3d dir = rotatedHorizontal(facing, (start + i) * VOLLEY_SPREAD_DEG);
fireProjectile(store, casterRef, eye, dir, VOLLEY_RANGE, 1.2, fireProjectile(store, casterRef, launch, dir, VOLLEY_RANGE, VOLLEY_HIT_RADIUS,
VOLLEY_ARROW_DAMAGE, VFX_ARROW_TRAIL, VFX_ARROW_HIT); VOLLEY_ARROW_DAMAGE, VFX_ARROW_TRAIL, VFX_ARROW_HIT, DamageCause.PROJECTILE);
} }
}); });
} }
@@ -436,11 +461,6 @@ public final class AbilityService {
@Nonnull Ref<EntityStore> casterRef, @Nonnull Ref<EntityStore> casterRef,
@Nonnull UUID caster) { @Nonnull UUID caster) {
combatBuffs.chargePowerShot(caster); combatBuffs.chargePowerShot(caster);
world.execute(() -> {
Vector3d pos = position(store, casterRef);
CombatFx.vfx(store, VFX_BUFF, pos);
CombatFx.sound(store, SFX_ACTIVATE, pos, 1.0f, 0.8f);
});
} }
// ------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------
@@ -459,7 +479,7 @@ public final class AbilityService {
for (int i = 0; i < FIREBALL_COUNT; i++) { for (int i = 0; i < FIREBALL_COUNT; i++) {
Vector3d dir = rotatedHorizontal(facing, (start + i) * FIREBALL_SPREAD_DEG); Vector3d dir = rotatedHorizontal(facing, (start + i) * FIREBALL_SPREAD_DEG);
fireProjectile(store, casterRef, eye, dir, FIREBALL_RANGE, FIREBALL_HIT_RADIUS, fireProjectile(store, casterRef, eye, dir, FIREBALL_RANGE, FIREBALL_HIT_RADIUS,
FIREBALL_DAMAGE, VFX_FIRE_TRAIL, VFX_FIRE_HIT); FIREBALL_DAMAGE, VFX_FIRE_TRAIL, VFX_FIRE_HIT, DamageCause.PROJECTILE);
} }
}); });
} }
@@ -513,7 +533,7 @@ public final class AbilityService {
} }
CombatFx.knockback(store, target, new Vector3d( CombatFx.knockback(store, target, new Vector3d(
pull.x * TORNADO_PULL, 2.5, pull.z * TORNADO_PULL)); pull.x * TORNADO_PULL, 2.5, pull.z * TORNADO_PULL));
dealDamage(store, casterRef, target, TORNADO_DAMAGE); dealDamage(store, casterRef, target, TORNADO_DAMAGE, DamageCause.PROJECTILE);
CombatFx.vfx(store, VFX_TORNADO_HIT, tpos); CombatFx.vfx(store, VFX_TORNADO_HIT, tpos);
} }
}), 0L, TORNADO_PERIOD); }), 0L, TORNADO_PERIOD);
@@ -631,23 +651,49 @@ public final class AbilityService {
float damage, float damage,
@Nonnull String trailVfx, @Nonnull String trailVfx,
@Nonnull String hitVfx) { @Nonnull String hitVfx) {
fireProjectile(store, casterRef, origin, dir, range, hitRadius, damage, trailVfx, hitVfx, DamageCause.PHYSICAL);
}
private void fireProjectile(
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> casterRef,
@Nonnull Vector3d origin,
@Nonnull Vector3d dir,
double range,
double hitRadius,
float damage,
@Nonnull String trailVfx,
@Nonnull String hitVfx,
@Nonnull DamageCause cause) {
Vector3d dirNorm = new Vector3d(dir);
if (dirNorm.lengthSquared() < 1.0e-6) {
return;
}
dirNorm.normalize();
Vector3d point = new Vector3d(origin); Vector3d point = new Vector3d(origin);
Vector3d step = new Vector3d(dir).mul(1.0); Vector3d step = new Vector3d(dirNorm).mul(PROJECTILE_TRACE_STEP);
int steps = (int) Math.ceil(range); int steps = Math.max(1, (int) Math.ceil(range / PROJECTILE_TRACE_STEP));
for (int i = 0; i < steps; i++) { for (int i = 0; i < steps; i++) {
point.add(step); point.add(step);
CombatFx.vfxTimed(store, trailVfx, new Vector3d(point), 0.5f, 0.35f); CombatFx.vfxTimed(store, trailVfx, new Vector3d(point), 0.5f, 0.35f);
for (Ref<EntityStore> target : TargetUtil.getAllEntitiesInSphere(point, hitRadius, store)) { for (Ref<EntityStore> target : TargetUtil.getAllEntitiesInCylinder(
if (target.equals(casterRef)) { point, hitRadius, PROJECTILE_CYLINDER_HEIGHT, store)) {
if (target.equals(casterRef) || !isDamageable(store, target)) {
continue; continue;
} }
dealDamage(store, casterRef, target, damage); dealDamage(store, casterRef, target, damage, cause);
CombatFx.vfx(store, hitVfx, targetPosition(store, target, point)); CombatFx.vfx(store, hitVfx, targetPosition(store, target, point));
return; return;
} }
} }
} }
private static boolean isDamageable(
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref) {
return store.getComponent(ref, EntityStatMap.getComponentType()) != null;
}
@Nonnull @Nonnull
private Vector3d rotatedHorizontal(@Nonnull Vector3d base, double degrees) { private Vector3d rotatedHorizontal(@Nonnull Vector3d base, double degrees) {
Vector3d v = new Vector3d(base); Vector3d v = new Vector3d(base);
@@ -695,7 +741,16 @@ public final class AbilityService {
@Nonnull Ref<EntityStore> casterRef, @Nonnull Ref<EntityStore> casterRef,
@Nonnull Ref<EntityStore> target, @Nonnull Ref<EntityStore> target,
float amount) { float amount) {
Damage damage = new Damage(new Damage.EntitySource(casterRef), DamageCause.PHYSICAL, amount); dealDamage(store, casterRef, target, amount, DamageCause.PHYSICAL);
}
private void dealDamage(
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> casterRef,
@Nonnull Ref<EntityStore> target,
float amount,
@Nonnull DamageCause cause) {
Damage damage = new Damage(new Damage.EntitySource(casterRef), cause, amount);
DamageSystems.executeDamage(target, store, damage); DamageSystems.executeDamage(target, store, damage);
} }
@@ -0,0 +1,160 @@
package com.disklexar.mmorpg.combat;
import com.disklexar.mmorpg.player.PlayerSessionService;
import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.clazz.ClassCatalog;
import com.disklexar.mmorpg.rpg.clazz.ClassService;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.entity.InteractionContext;
import com.hypixel.hytale.server.core.inventory.InventoryComponent;
import com.hypixel.hytale.server.core.inventory.InventoryUtils;
import com.hypixel.hytale.server.core.inventory.ItemStack;
import com.hypixel.hytale.server.core.inventory.container.ItemContainer;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.RootInteraction;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Archer bow: one arrow in inventory is enough to shoot forever.
* Each left click fires instantly at max charge, with a 1s cooldown between shots.
*/
public final class ArcherBowService {
public enum ShootResult {
OK, ON_COOLDOWN, NO_ARROW, NOT_BOW, PROJECTILE_UNAVAILABLE
}
private static final String ARCHER_PROJECTILE_ROOT = "Root_Mmorpg_Archer_Projectile";
private static final long SHOT_COOLDOWN_MS = 1_000L;
/** Tir rapide: x4 cadence de tir (cooldown ÷ 4, ~4 tirs/s). */
private static final double RAPID_FIRE_SPEED_FACTOR = 4.0;
private final PlayerSessionService sessions;
private final ClassService classService;
private final CombatStateService combatState;
private final CombatBuffService combatBuffs;
private final Map<UUID, Long> nextShotAt = new ConcurrentHashMap<>();
public ArcherBowService(
@Nonnull PlayerSessionService sessions,
@Nonnull ClassService classService,
@Nonnull CombatStateService combatState,
@Nonnull CombatBuffService combatBuffs) {
this.sessions = sessions;
this.classService = classService;
this.combatState = combatState;
this.combatBuffs = combatBuffs;
}
public boolean isArcher(@Nonnull PlayerRef playerRef) {
PlayerProfile profile = sessions.getSession(playerRef.getUuid());
if (profile == null) {
return false;
}
return ClassCatalog.ARCHER.id().equals(profile.getClassId());
}
@Nonnull
public ShootResult tryShoot(
@Nonnull PlayerRef playerRef,
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull InteractionContext interactionContext) {
if (!isBowInHand(store, ref)) {
return ShootResult.NOT_BOW;
}
if (!hasArrow(store, ref)) {
playerRef.sendMessage(com.hypixel.hytale.server.core.Message.raw(
"Vous avez besoin d'au moins une flèche dans votre inventaire."));
return ShootResult.NO_ARROW;
}
UUID uuid = playerRef.getUuid();
long now = System.currentTimeMillis();
Long readyAt = nextShotAt.get(uuid);
if (readyAt != null && now < readyAt) {
return ShootResult.ON_COOLDOWN;
}
RootInteraction projectileRoot = RootInteraction.getAssetMap().getAsset(ARCHER_PROJECTILE_ROOT);
if (projectileRoot == null) {
return ShootResult.PROJECTILE_UNAVAILABLE;
}
nextShotAt.put(uuid, now + shotCooldownMs(uuid));
interactionContext.execute(projectileRoot);
combatState.markCombat(uuid);
return ShootResult.OK;
}
public void clear(@Nonnull UUID playerUuid) {
nextShotAt.remove(playerUuid);
}
private long shotCooldownMs(@Nonnull UUID playerUuid) {
if (combatBuffs.hasRapidFire(playerUuid)) {
return Math.max(200L, Math.round(SHOT_COOLDOWN_MS / RAPID_FIRE_SPEED_FACTOR));
}
return SHOT_COOLDOWN_MS;
}
private static boolean isBowItemId(@Nonnull String itemId) {
String lower = itemId.toLowerCase(Locale.ROOT);
return lower.contains("bow") && !lower.contains("crossbow");
}
private static boolean isBowInHand(@Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref) {
try {
ItemStack held = InventoryComponent.getItemInHand(store, ref);
if (ItemStack.isEmpty(held)) {
return false;
}
String itemId = held.getItemId();
return itemId != null && isBowItemId(itemId);
} catch (Throwable t) {
return false;
}
}
private static boolean hasArrow(@Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref) {
return countArrows(store, ref) > 0;
}
private static int countArrows(@Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref) {
int total = 0;
total += countArrowsInSection(store, ref, InventoryComponent.HOTBAR_SECTION_ID);
total += countArrowsInSection(store, ref, InventoryComponent.STORAGE_SECTION_ID);
total += countArrowsInSection(store, ref, InventoryComponent.UTILITY_SECTION_ID);
return total;
}
private static int countArrowsInSection(
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
int sectionId) {
ItemContainer container = InventoryUtils.getSectionById(ref, sectionId, store);
if (container == null) {
return 0;
}
int count = 0;
for (short slot = 0; slot < container.getCapacity(); slot++) {
ItemStack stack = container.getItemStack(slot);
if (ItemStack.isEmpty(stack)) {
continue;
}
String itemId = stack.getItemId();
if (itemId != null && itemId.toLowerCase(Locale.ROOT).contains("arrow")) {
count += stack.getQuantity();
}
}
return count;
}
}
@@ -10,8 +10,7 @@ import java.util.concurrent.ConcurrentHashMap;
* Tracks the temporary offensive buffs granted by class abilities and read back by the damage * Tracks the temporary offensive buffs granted by class abilities and read back by the damage
* system when the buffed player lands a basic attack: * system when the buffed player lands a basic attack:
* <ul> * <ul>
* <li>Archer "Tir rapide": a flat outgoing-damage bonus for a window (server-side proxy for * <li>Archer "Tir rapide": x4 bow shot rate (250 ms between clicks),</li>
* attack speed, which is otherwise client-authoritative),</li>
* <li>Archer "Tir puissant": a one-shot +200% multiplier on the next basic attack,</li> * <li>Archer "Tir puissant": a one-shot +200% multiplier on the next basic attack,</li>
* <li>Assassin "Lame Empoisonnée": a window during which every hit applies a stacking poison,</li> * <li>Assassin "Lame Empoisonnée": a window during which every hit applies a stacking poison,</li>
* <li>Assassin "Camouflage": bookkeeping of the current invisibility window.</li> * <li>Assassin "Camouflage": bookkeeping of the current invisibility window.</li>
@@ -2,11 +2,13 @@ package com.disklexar.mmorpg.combat;
import com.disklexar.mmorpg.MmorpgPlugin; import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.core.service.Service; import com.disklexar.mmorpg.core.service.Service;
import com.disklexar.mmorpg.player.CharacterService;
import com.disklexar.mmorpg.player.OnlinePlayer; import com.disklexar.mmorpg.player.OnlinePlayer;
import com.disklexar.mmorpg.player.OnlinePlayerRegistry; import com.disklexar.mmorpg.player.OnlinePlayerRegistry;
import com.disklexar.mmorpg.player.PlayerSessionService; import com.disklexar.mmorpg.player.PlayerSessionService;
import com.disklexar.mmorpg.player.model.PlayerProfile; import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.power.PowerCatalog; import com.disklexar.mmorpg.rpg.power.PowerCatalog;
import com.disklexar.mmorpg.rpg.stats.CharacterStatService;
import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.protocol.MovementSettings; import com.hypixel.hytale.protocol.MovementSettings;
@@ -29,13 +31,19 @@ import java.util.concurrent.ScheduledFuture;
public final class PassiveEffectService implements Service { public final class PassiveEffectService implements Service {
private static final long RECONCILE_PERIOD = 2_000L; private static final long RECONCILE_PERIOD = 2_000L;
private static final long INVENTORY_SAVE_PERIOD = 15_000L;
private final MmorpgScheduler scheduler; private final MmorpgScheduler scheduler;
private final OnlinePlayerRegistry online; private final OnlinePlayerRegistry online;
private final PlayerSessionService sessions; private final PlayerSessionService sessions;
private final CombatStateService combatState; private final CombatStateService combatState;
@Nullable
private final CharacterStatService characterStats;
@Nullable
private final CharacterService characters;
private ScheduledFuture<?> task; private ScheduledFuture<?> task;
private ScheduledFuture<?> inventoryTask;
public PassiveEffectService( public PassiveEffectService(
@Nonnull MmorpgPlugin plugin, @Nonnull MmorpgPlugin plugin,
@@ -43,15 +51,44 @@ public final class PassiveEffectService implements Service {
@Nonnull OnlinePlayerRegistry online, @Nonnull OnlinePlayerRegistry online,
@Nonnull PlayerSessionService sessions, @Nonnull PlayerSessionService sessions,
@Nonnull CombatStateService combatState) { @Nonnull CombatStateService combatState) {
this(plugin, scheduler, online, sessions, combatState, null, null);
}
public PassiveEffectService(
@Nonnull MmorpgPlugin plugin,
@Nonnull MmorpgScheduler scheduler,
@Nonnull OnlinePlayerRegistry online,
@Nonnull PlayerSessionService sessions,
@Nonnull CombatStateService combatState,
@Nullable CharacterStatService characterStats) {
this(plugin, scheduler, online, sessions, combatState, characterStats, null);
}
public PassiveEffectService(
@Nonnull MmorpgPlugin plugin,
@Nonnull MmorpgScheduler scheduler,
@Nonnull OnlinePlayerRegistry online,
@Nonnull PlayerSessionService sessions,
@Nonnull CombatStateService combatState,
@Nullable CharacterStatService characterStats,
@Nullable CharacterService characters) {
this.scheduler = scheduler; this.scheduler = scheduler;
this.online = online; this.online = online;
this.sessions = sessions; this.sessions = sessions;
this.combatState = combatState; this.combatState = combatState;
this.characterStats = characterStats;
this.characters = characters;
} }
@Override @Override
public void start() { public void start() {
task = scheduler.runRepeating(this::reconcileAll, RECONCILE_PERIOD, RECONCILE_PERIOD); task = scheduler.runRepeating(this::reconcileAll, RECONCILE_PERIOD, RECONCILE_PERIOD);
if (characters != null) {
inventoryTask = scheduler.runRepeating(
() -> characters.persistAllOnlineInventories(online),
INVENTORY_SAVE_PERIOD,
INVENTORY_SAVE_PERIOD);
}
} }
@Override @Override
@@ -60,6 +97,10 @@ public final class PassiveEffectService implements Service {
task.cancel(false); task.cancel(false);
task = null; task = null;
} }
if (inventoryTask != null) {
inventoryTask.cancel(false);
inventoryTask = null;
}
} }
private void reconcileAll() { private void reconcileAll() {
@@ -79,6 +120,10 @@ public final class PassiveEffectService implements Service {
if (ref == null) { if (ref == null) {
return; return;
} }
PlayerProfile profile = sessions.getSession(player.uuid());
if (profile == null) {
return;
}
try { try {
Store<EntityStore> store = player.world().getEntityStore().getStore(); Store<EntityStore> store = player.world().getEntityStore().getStore();
MovementManager movement = store.getComponent(ref, MovementManager.getComponentType()); MovementManager movement = store.getComponent(ref, MovementManager.getComponentType());
@@ -91,7 +136,10 @@ public final class PassiveEffectService implements Service {
return; return;
} }
float baseline = defaults.baseSpeed; float baseline = defaults.baseSpeed;
float target = buffed ? baseline * (float) (1.0 + PowerCatalog.SPRINTER_SPEED_BONUS) : baseline; float speedBonus = characterStats == null ? 0f : characterStats.movementSpeedBonus(profile);
float target = (buffed
? baseline * (float) (1.0 + PowerCatalog.SPRINTER_SPEED_BONUS)
: baseline) + speedBonus;
if (Math.abs(current.baseSpeed - target) > 0.0001f) { if (Math.abs(current.baseSpeed - target) > 0.0001f) {
current.baseSpeed = target; current.baseSpeed = target;
movement.update(player.playerRef().getPacketHandler()); movement.update(player.playerRef().getPacketHandler());
@@ -0,0 +1,82 @@
package com.disklexar.mmorpg.combat.system;
import com.disklexar.mmorpg.player.PlayerSessionService;
import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.stats.CharacterStatService;
import com.hypixel.hytale.component.ArchetypeChunk;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.dependency.Dependency;
import com.hypixel.hytale.component.dependency.Order;
import com.hypixel.hytale.component.dependency.SystemDependency;
import com.hypixel.hytale.component.query.Query;
import com.hypixel.hytale.component.system.tick.EntityTickingSystem;
import com.hypixel.hytale.protocol.InteractionType;
import com.hypixel.hytale.server.core.entity.InteractionManager;
import com.hypixel.hytale.server.core.modules.interaction.InteractionModule;
import com.hypixel.hytale.server.core.modules.interaction.system.InteractionSystems;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import java.util.Set;
/**
* Applies dexterity as real attack-speed acceleration on weapon interactions (Primary / Secondary).
* Runs before the vanilla interaction tick so {@link InteractionManager#setGlobalTimeShift} is picked up.
*/
public final class MmorpgAttackSpeedSystem extends EntityTickingSystem<EntityStore> {
private final PlayerSessionService sessions;
private final CharacterStatService characterStats;
private final ComponentType<EntityStore, InteractionManager> interactionManagerType;
public MmorpgAttackSpeedSystem(
@Nonnull PlayerSessionService sessions,
@Nonnull CharacterStatService characterStats) {
this.sessions = sessions;
this.characterStats = characterStats;
this.interactionManagerType = InteractionModule.get().getInteractionManagerComponent();
}
@Override
public Query<EntityStore> getQuery() {
return Query.and(interactionManagerType, PlayerRef.getComponentType());
}
@Override
public Set<Dependency<EntityStore>> getDependencies() {
return Set.of(new SystemDependency<>(
Order.BEFORE,
InteractionSystems.TickInteractionManagerSystem.class));
}
@Override
public void tick(
float dt,
int index,
@Nonnull ArchetypeChunk<EntityStore> chunk,
@Nonnull Store<EntityStore> store,
@Nonnull CommandBuffer<EntityStore> commandBuffer) {
PlayerRef playerRef = chunk.getComponent(index, PlayerRef.getComponentType());
if (playerRef == null) {
return;
}
PlayerProfile profile = sessions.getSession(playerRef.getUuid());
if (profile == null) {
return;
}
float shift = characterStats.attackSpeedTimeShift(profile);
if (shift <= 0f) {
return;
}
InteractionManager manager = chunk.getComponent(index, interactionManagerType);
if (manager == null) {
return;
}
manager.setGlobalTimeShift(InteractionType.Primary, shift);
manager.setGlobalTimeShift(InteractionType.Secondary, shift);
}
}
@@ -7,7 +7,9 @@ import com.disklexar.mmorpg.player.OnlinePlayer;
import com.disklexar.mmorpg.player.OnlinePlayerRegistry; import com.disklexar.mmorpg.player.OnlinePlayerRegistry;
import com.disklexar.mmorpg.player.PlayerSessionService; import com.disklexar.mmorpg.player.PlayerSessionService;
import com.disklexar.mmorpg.player.model.PlayerProfile; import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.clazz.ClassCatalog;
import com.disklexar.mmorpg.rpg.power.PowerCatalog; import com.disklexar.mmorpg.rpg.power.PowerCatalog;
import com.disklexar.mmorpg.rpg.stats.CharacterStatService;
import com.hypixel.hytale.component.ArchetypeChunk; import com.hypixel.hytale.component.ArchetypeChunk;
import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Ref;
@@ -15,6 +17,8 @@ import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.SystemGroup; import com.hypixel.hytale.component.SystemGroup;
import com.hypixel.hytale.component.query.Query; import com.hypixel.hytale.component.query.Query;
import com.hypixel.hytale.server.core.entity.UUIDComponent; import com.hypixel.hytale.server.core.entity.UUIDComponent;
import com.hypixel.hytale.server.core.inventory.InventoryComponent;
import com.hypixel.hytale.server.core.inventory.ItemStack;
import com.hypixel.hytale.server.core.modules.entity.damage.Damage; import com.hypixel.hytale.server.core.modules.entity.damage.Damage;
import com.hypixel.hytale.server.core.modules.entity.damage.DamageCause; import com.hypixel.hytale.server.core.modules.entity.damage.DamageCause;
import com.hypixel.hytale.server.core.modules.entity.damage.DamageEventSystem; import com.hypixel.hytale.server.core.modules.entity.damage.DamageEventSystem;
@@ -23,6 +27,8 @@ import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Locale;
import java.util.UUID; import java.util.UUID;
/** /**
@@ -37,8 +43,6 @@ public final class MmorpgDamageSystem extends DamageEventSystem {
private static final Query<EntityStore> QUERY = Query.any(); private static final Query<EntityStore> QUERY = Query.any();
private static final double RESISTANCE_DEBUFF_FACTOR = 1.20; private static final double RESISTANCE_DEBUFF_FACTOR = 1.20;
// Archer "Tir rapide": flat outgoing-damage bonus (server proxy for attack speed).
private static final double RAPID_FIRE_FACTOR = 1.20;
// Archer "Tir puissant": +200% on the next basic attack. // Archer "Tir puissant": +200% on the next basic attack.
private static final double POWER_SHOT_FACTOR = 3.0; private static final double POWER_SHOT_FACTOR = 3.0;
// Assassin "Lame Empoisonnée": stacking poison applied on each basic hit. // Assassin "Lame Empoisonnée": stacking poison applied on each basic hit.
@@ -51,6 +55,8 @@ public final class MmorpgDamageSystem extends DamageEventSystem {
private final CombatBuffService combatBuffs; private final CombatBuffService combatBuffs;
private final PoisonService poisonService; private final PoisonService poisonService;
private final OnlinePlayerRegistry onlinePlayers; private final OnlinePlayerRegistry onlinePlayers;
@Nullable
private final CharacterStatService characterStats;
public MmorpgDamageSystem( public MmorpgDamageSystem(
@Nonnull PlayerSessionService sessions, @Nonnull PlayerSessionService sessions,
@@ -58,11 +64,22 @@ public final class MmorpgDamageSystem extends DamageEventSystem {
@Nonnull CombatBuffService combatBuffs, @Nonnull CombatBuffService combatBuffs,
@Nonnull PoisonService poisonService, @Nonnull PoisonService poisonService,
@Nonnull OnlinePlayerRegistry onlinePlayers) { @Nonnull OnlinePlayerRegistry onlinePlayers) {
this(sessions, combatState, combatBuffs, poisonService, onlinePlayers, null);
}
public MmorpgDamageSystem(
@Nonnull PlayerSessionService sessions,
@Nonnull CombatStateService combatState,
@Nonnull CombatBuffService combatBuffs,
@Nonnull PoisonService poisonService,
@Nonnull OnlinePlayerRegistry onlinePlayers,
@Nullable CharacterStatService characterStats) {
this.sessions = sessions; this.sessions = sessions;
this.combatState = combatState; this.combatState = combatState;
this.combatBuffs = combatBuffs; this.combatBuffs = combatBuffs;
this.poisonService = poisonService; this.poisonService = poisonService;
this.onlinePlayers = onlinePlayers; this.onlinePlayers = onlinePlayers;
this.characterStats = characterStats;
} }
@Override @Override
@@ -72,7 +89,8 @@ public final class MmorpgDamageSystem extends DamageEventSystem {
@Override @Override
public SystemGroup<EntityStore> getGroup() { public SystemGroup<EntityStore> getGroup() {
return DamageModule.get().getInspectDamageGroup(); // gather → filter → apply : only the filter stage mutates damage before it hits health.
return DamageModule.get().getFilterDamageGroup();
} }
@Override @Override
@@ -93,9 +111,8 @@ public final class MmorpgDamageSystem extends DamageEventSystem {
if (damage.getSource() instanceof Damage.EntitySource source) { if (damage.getSource() instanceof Damage.EntitySource source) {
Ref<EntityStore> attackerRef = source.getRef(); Ref<EntityStore> attackerRef = source.getRef();
if (attackerRef != null) { if (attackerRef != null) {
PlayerRef attacker = store.getComponent(attackerRef, PlayerRef.getComponentType()); UUID attackerUuid = playerUuid(store, attackerRef);
if (attacker != null) { if (attackerUuid != null) {
UUID attackerUuid = attacker.getUuid();
combatState.markCombat(attackerUuid); combatState.markCombat(attackerUuid);
PlayerProfile profile = sessions.getSession(attackerUuid); PlayerProfile profile = sessions.getSession(attackerUuid);
if (profile != null if (profile != null
@@ -104,12 +121,12 @@ public final class MmorpgDamageSystem extends DamageEventSystem {
amount *= (1.0 + PowerCatalog.RESILIENT_DAMAGE_BONUS); amount *= (1.0 + PowerCatalog.RESILIENT_DAMAGE_BONUS);
} }
// Offensive class buffs only apply to direct physical hits, never to the boolean archerBowHit = isArcherBowHit(profile, store, attackerRef, damage.getCause());
// damage-over-time / spell ticks our own abilities deal (those are MAGIC), so a // Vanilla melee uses Bludgeoning / Slashing (children of Physical), not the
// single sword swing doesn't waste the archer's charged shot, etc. // PHYSICAL constant itself. Class ability DoT uses Elemental / Environment.
if (damage.getCause() == DamageCause.PHYSICAL) { if (isMeleePhysicalDamage(damage.getCause()) || archerBowHit) {
if (combatBuffs.hasRapidFire(attackerUuid)) { if (profile != null && characterStats != null) {
amount *= RAPID_FIRE_FACTOR; amount += characterStats.attackBonus(profile);
} }
if (combatBuffs.consumePowerShot(attackerUuid)) { if (combatBuffs.consumePowerShot(attackerUuid)) {
amount *= POWER_SHOT_FACTOR; amount *= POWER_SHOT_FACTOR;
@@ -136,8 +153,77 @@ public final class MmorpgDamageSystem extends DamageEventSystem {
} }
} }
private UUID uuid(@Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref) { @Nullable
private UUID playerUuid(@Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref) {
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
if (playerRef != null) {
return playerRef.getUuid();
}
UUIDComponent component = store.getComponent(ref, UUIDComponent.getComponentType()); UUIDComponent component = store.getComponent(ref, UUIDComponent.getComponentType());
return component == null ? null : component.getUuid(); return component == null ? null : component.getUuid();
} }
@Nullable
private UUID uuid(@Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref) {
return playerUuid(store, ref);
}
private static boolean isArcherBowHit(
@Nullable PlayerProfile profile,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> attackerRef,
@Nullable DamageCause cause) {
if (profile == null || !ClassCatalog.ARCHER.id().equals(profile.getClassId())) {
return false;
}
if (!isProjectileDamage(cause)) {
return false;
}
try {
ItemStack held = InventoryComponent.getItemInHand(store, attackerRef);
if (ItemStack.isEmpty(held)) {
return false;
}
String itemId = held.getItemId();
if (itemId == null) {
return false;
}
String lower = itemId.toLowerCase(Locale.ROOT);
return lower.contains("bow") && !lower.contains("crossbow");
} catch (Throwable t) {
return false;
}
}
private static boolean isProjectileDamage(@Nullable DamageCause cause) {
if (cause == null) {
return false;
}
String id = cause.getId();
return id != null && id.equalsIgnoreCase("Projectile");
}
/**
* True for fists, swords, axes, etc. Excludes projectiles and elemental ability ticks.
*/
private static boolean isMeleePhysicalDamage(@Nullable DamageCause cause) {
if (cause == null) {
return false;
}
if (cause == DamageCause.PHYSICAL) {
return true;
}
String id = cause.getId();
if (id != null) {
String lower = id.toLowerCase(Locale.ROOT);
if (lower.equals("projectile") || lower.equals("elemental") || lower.equals("environment")) {
return false;
}
if (lower.equals("bludgeoning") || lower.equals("slashing") || lower.equals("physical")) {
return true;
}
}
String inherits = cause.getInherits();
return inherits != null && inherits.equalsIgnoreCase("Physical");
}
} }
@@ -0,0 +1,63 @@
package com.disklexar.mmorpg.combat.system;
import com.disklexar.mmorpg.player.CharacterService;
import com.disklexar.mmorpg.player.PlayerSessionService;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.query.Query;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.modules.entity.damage.DeathComponent;
import com.hypixel.hytale.server.core.modules.entity.damage.DeathSystems;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import java.util.UUID;
/**
* Persists permanent character death when {@link DeathComponent} is added.
*/
public final class MmorpgPlayerDeathSystem extends DeathSystems.OnDeathSystem {
private static final Query<EntityStore> QUERY = Player.getComponentType();
private final PlayerSessionService sessions;
private final CharacterService characters;
public MmorpgPlayerDeathSystem(
@Nonnull PlayerSessionService sessions,
@Nonnull CharacterService characters) {
this.sessions = sessions;
this.characters = characters;
}
@Override
public Query<EntityStore> getQuery() {
return QUERY;
}
@Override
public void onComponentAdded(
@Nonnull Ref<EntityStore> deadRef,
@Nonnull DeathComponent death,
@Nonnull Store<EntityStore> store,
@Nonnull CommandBuffer<EntityStore> commandBuffer) {
PlayerRef playerRef = store.getComponent(deadRef, PlayerRef.getComponentType());
if (playerRef == null) {
return;
}
UUID accountUuid = playerRef.getUuid();
if (sessions.getSession(accountUuid) == null) {
return;
}
Player player = store.getComponent(deadRef, Player.getComponentType());
if (player == null) {
return;
}
characters.retireActiveCharacterOnDeath(playerRef, deadRef, store, accountUuid);
}
}
@@ -0,0 +1,60 @@
package com.disklexar.mmorpg.combat.system;
import com.disklexar.mmorpg.combat.MmorpgScheduler;
import com.disklexar.mmorpg.player.CharacterService;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.query.Query;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.modules.entity.damage.DeathComponent;
import com.hypixel.hytale.server.core.modules.entity.damage.RespawnSystems;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import java.util.UUID;
/**
* Opens the character selector after the vanilla respawn flow clears the death screen.
*/
public final class MmorpgPlayerRespawnSystem extends RespawnSystems.OnRespawnSystem {
private static final Query<EntityStore> QUERY = Player.getComponentType();
private final CharacterService characters;
private final MmorpgScheduler scheduler;
public MmorpgPlayerRespawnSystem(
@Nonnull CharacterService characters,
@Nonnull MmorpgScheduler scheduler) {
this.characters = characters;
this.scheduler = scheduler;
}
@Override
public Query<EntityStore> getQuery() {
return QUERY;
}
@Override
public void onComponentRemoved(
@Nonnull Ref<EntityStore> ref,
@Nonnull DeathComponent death,
@Nonnull Store<EntityStore> store,
@Nonnull CommandBuffer<EntityStore> commandBuffer) {
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
if (playerRef == null) {
return;
}
UUID accountUuid = playerRef.getUuid();
Player player = store.getComponent(ref, Player.getComponentType());
if (player == null) {
return;
}
scheduler.runLater(() -> player.getWorld().execute(() ->
characters.openCharacterSelectorAfterRespawn(playerRef, ref, store, accountUuid)), 500L);
}
}
@@ -1,13 +1,13 @@
package com.disklexar.mmorpg.command; package com.disklexar.mmorpg.command;
import com.disklexar.mmorpg.MmorpgPlugin; import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.player.OnlinePlayer;
import com.disklexar.mmorpg.player.OnlinePlayerRegistry;
import com.disklexar.mmorpg.player.model.PlayerProfile; import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.clazz.AbilityDefinition; import com.disklexar.mmorpg.rpg.clazz.AbilityDefinition;
import com.disklexar.mmorpg.ui.AbilityBarService; import com.disklexar.mmorpg.ui.AbilityBarService;
import com.disklexar.mmorpg.rpg.clazz.ClassCatalog; import com.disklexar.mmorpg.rpg.clazz.ClassCatalog;
import com.disklexar.mmorpg.rpg.clazz.ClassService; import com.disklexar.mmorpg.rpg.clazz.ClassService;
import com.disklexar.mmorpg.player.CharacterService;
import com.disklexar.mmorpg.rpg.clazz.ClassSelectionSupport;
import com.disklexar.mmorpg.rpg.clazz.PlayerClass; import com.disklexar.mmorpg.rpg.clazz.PlayerClass;
import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
@@ -31,6 +31,7 @@ public final class ClassCommand extends AbstractCommandCollection {
public ClassCommand(@Nonnull MmorpgPlugin plugin) { public ClassCommand(@Nonnull MmorpgPlugin plugin) {
super("class", "Gérer votre classe"); super("class", "Gérer votre classe");
addSubCommand(new Choose(plugin)); addSubCommand(new Choose(plugin));
addSubCommand(new Selector(plugin));
addSubCommand(new Leave(plugin)); addSubCommand(new Leave(plugin));
addSubCommand(new Info(plugin)); addSubCommand(new Info(plugin));
} }
@@ -54,17 +55,56 @@ public final class ClassCommand extends AbstractCommandCollection {
context.sendMessage(Message.raw("Profil indisponible.")); context.sendMessage(Message.raw("Profil indisponible."));
return; return;
} }
boolean firstClass = !classService.hasClass(profile);
String id = context.get(classArg); String id = context.get(classArg);
if (classService.choose(profile, id)) { PlayerClass chosen = ClassSelectionSupport.apply(plugin, store, ref, playerRef, profile, id);
PlayerClass chosen = classService.getClass(profile); if (chosen != null) {
context.sendMessage(Message.raw("Classe choisie : " + (chosen != null ? chosen.displayName() : id))); context.sendMessage(Message.raw("Classe choisie : " + chosen.displayName()));
syncAbilityBar(plugin, playerRef, profile); if (firstClass) {
CharacterService characters = CommandSupport.service(plugin, CharacterService.class);
if (characters != null) {
characters.welcomeAfterFirstClassChoice(playerRef, profile);
}
}
} else { } else {
context.sendMessage(Message.raw("Classe inconnue. Disponibles : " + classList())); context.sendMessage(Message.raw("Classe inconnue. Disponibles : " + classList()));
} }
} }
} }
private static final class Selector extends AbstractPlayerCommand {
private final MmorpgPlugin plugin;
Selector(@Nonnull MmorpgPlugin plugin) {
super("selecteur", "Ouvrir le sélecteur de classes");
this.plugin = plugin;
}
@Override
protected void execute(
@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world) {
PlayerProfile profile = CommandSupport.profile(plugin, store, ref);
ClassService classService = CommandSupport.service(plugin, ClassService.class);
CharacterService characters = CommandSupport.service(plugin, CharacterService.class);
if (profile == null || classService == null || characters == null) {
context.sendMessage(Message.raw("Profil indisponible."));
return;
}
world.execute(() -> characters.openClassSelector(
playerRef,
ref,
store,
profile,
!classService.hasClass(profile),
false));
}
}
private static final class Leave extends AbstractPlayerCommand { private static final class Leave extends AbstractPlayerCommand {
private final MmorpgPlugin plugin; private final MmorpgPlugin plugin;
@@ -123,21 +163,6 @@ public final class ClassCommand extends AbstractCommandCollection {
} }
} }
private static void syncAbilityBar(
@Nonnull MmorpgPlugin plugin,
@Nonnull PlayerRef playerRef,
@Nonnull PlayerProfile profile) {
AbilityBarService abilityBar = CommandSupport.service(plugin, AbilityBarService.class);
OnlinePlayerRegistry onlinePlayers = CommandSupport.service(plugin, OnlinePlayerRegistry.class);
if (abilityBar == null || onlinePlayers == null) {
return;
}
OnlinePlayer online = onlinePlayers.get(playerRef.getUuid());
if (online != null) {
abilityBar.sync(online, profile);
}
}
private static void dismissAbilityBar(@Nonnull MmorpgPlugin plugin, @Nonnull PlayerRef playerRef) { private static void dismissAbilityBar(@Nonnull MmorpgPlugin plugin, @Nonnull PlayerRef playerRef) {
AbilityBarService abilityBar = CommandSupport.service(plugin, AbilityBarService.class); AbilityBarService abilityBar = CommandSupport.service(plugin, AbilityBarService.class);
if (abilityBar != null) { if (abilityBar != null) {
@@ -1,11 +1,11 @@
package com.disklexar.mmorpg.command; package com.disklexar.mmorpg.command;
import com.disklexar.mmorpg.MmorpgPlugin; import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.player.OnlinePlayer; import com.disklexar.mmorpg.combat.AbilityService;
import com.disklexar.mmorpg.player.OnlinePlayerRegistry;
import com.disklexar.mmorpg.player.model.PlayerProfile; import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.clazz.AbilityDefinition;
import com.disklexar.mmorpg.rpg.clazz.ClassService; import com.disklexar.mmorpg.rpg.clazz.ClassService;
import com.disklexar.mmorpg.ui.AbilityBarService; import com.disklexar.mmorpg.rpg.clazz.PlayerClass;
import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.Message;
@@ -39,9 +39,8 @@ public final class HudCommand extends AbstractPlayerCommand {
} }
ClassService classService = CommandSupport.service(plugin, ClassService.class); ClassService classService = CommandSupport.service(plugin, ClassService.class);
AbilityBarService abilityBar = CommandSupport.service(plugin, AbilityBarService.class); AbilityService abilities = CommandSupport.service(plugin, AbilityService.class);
OnlinePlayerRegistry onlinePlayers = CommandSupport.service(plugin, OnlinePlayerRegistry.class); if (classService == null || abilities == null) {
if (classService == null || abilityBar == null || onlinePlayers == null) {
context.sendMessage(Message.raw("Service HUD indisponible.")); context.sendMessage(Message.raw("Service HUD indisponible."));
return; return;
} }
@@ -51,14 +50,19 @@ public final class HudCommand extends AbstractPlayerCommand {
return; return;
} }
OnlinePlayer online = onlinePlayers.get(playerRef.getUuid()); PlayerClass playerClass = classService.getClass(profile);
if (online == null) { if (playerClass == null) {
context.sendMessage(Message.raw("Joueur non enregistré. Reconnectez-vous.")); context.sendMessage(Message.raw("Classe introuvable."));
return; return;
} }
abilityBar.dismiss(online); context.sendMessage(Message.raw("=== Capacités (" + playerClass.displayName() + ") ==="));
abilityBar.sync(online, profile); for (AbilityDefinition ability : playerClass.abilities()) {
context.sendMessage(Message.raw("Barre de capacités renvoyée (colonne gauche).")); long remaining = abilities.cooldownRemaining(profile.getUuid(), ability.id());
String cooldown = remaining > 0 ? "" + (remaining / 1000 + 1) + "s" : " — prête";
context.sendMessage(Message.raw(
ability.slot() + ". " + ability.displayName() + cooldown));
}
context.sendMessage(Message.raw("Utilisez /mmorpg ability <1|2|3> ou les touches Use ability en jeu."));
} }
} }
@@ -5,6 +5,7 @@ import com.disklexar.mmorpg.player.PlayerInfoView;
import com.disklexar.mmorpg.player.model.PlayerProfile; import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.combat.AbilityService; import com.disklexar.mmorpg.combat.AbilityService;
import com.disklexar.mmorpg.progression.ProgressionService; import com.disklexar.mmorpg.progression.ProgressionService;
import com.disklexar.mmorpg.rpg.stats.CharacterStatService;
import com.disklexar.mmorpg.ui.PlayerMenuPage; import com.disklexar.mmorpg.ui.PlayerMenuPage;
import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
@@ -28,7 +29,7 @@ public final class MenuCommand extends AbstractPlayerCommand {
private final MmorpgPlugin plugin; private final MmorpgPlugin plugin;
public MenuCommand(@Nonnull MmorpgPlugin plugin) { public MenuCommand(@Nonnull MmorpgPlugin plugin) {
super("menu", "Ouvrir le menu joueur MMORPG (Personnage / Inventaire / Compétences)"); super("menu", "Ouvrir le menu joueur MMORPG (Personnage / Inventaire / Statistiques / Compétences)");
this.plugin = plugin; this.plugin = plugin;
} }
@@ -53,6 +54,7 @@ public final class MenuCommand extends AbstractPlayerCommand {
profile, profile,
CommandSupport.service(plugin, ProgressionService.class), CommandSupport.service(plugin, ProgressionService.class),
CommandSupport.service(plugin, AbilityService.class), CommandSupport.service(plugin, AbilityService.class),
CommandSupport.service(plugin, CharacterStatService.class),
PlayerMenuPage.Tab.CHARACTER)); PlayerMenuPage.Tab.CHARACTER));
} catch (Throwable t) { } catch (Throwable t) {
playerRef.sendMessage(Message.raw(PlayerInfoView.asText(profile))); playerRef.sendMessage(Message.raw(PlayerInfoView.asText(profile)));
@@ -25,10 +25,10 @@ public final class MmorpgHelpCommand extends AbstractAsyncCommand {
/mmorpg help — cette aide /mmorpg help — cette aide
/mmorpg profile — résumé court de votre profil /mmorpg profile — résumé court de votre profil
/mmorpg info — toutes vos informations (chat) /mmorpg info — toutes vos informations (chat)
/mmorpg menu — menu joueur (Personnage / Inventaire / Compétences) /mmorpg menu — menu joueur (Personnage / Inventaire / Statistiques / Compétences)
/mmorpg inventory — menu joueur, onglet Inventaire /mmorpg inventory — menu joueur, onglet Inventaire
/mmorpg menucustom — interface personnalisée (asset pack, test) /mmorpg menucustom — interface personnalisée (asset pack, test)
/mmorpg class <choisir|quitter|info> [classe] /mmorpg class <choisir|selecteur|quitter|info> [classe]
/mmorpg power <recevoir|retirer|info> [pouvoir] /mmorpg power <recevoir|retirer|info> [pouvoir]
/mmorpg job <rejoindre|quitter|info> [metier] /mmorpg job <rejoindre|quitter|info> [metier]
/mmorpg group <inviter|accepter|quitter|kick|info> [joueur] /mmorpg group <inviter|accepter|quitter|kick|info> [joueur]
@@ -2,13 +2,12 @@ package com.disklexar.mmorpg.events;
import com.disklexar.mmorpg.MmorpgPlugin; import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.bootstrap.Bootstrap; import com.disklexar.mmorpg.bootstrap.Bootstrap;
import com.disklexar.mmorpg.combat.ArcherBowService;
import com.disklexar.mmorpg.combat.MmorpgScheduler; import com.disklexar.mmorpg.combat.MmorpgScheduler;
import com.disklexar.mmorpg.player.CharacterService; import com.disklexar.mmorpg.player.CharacterService;
import com.disklexar.mmorpg.player.OnlinePlayer;
import com.disklexar.mmorpg.player.OnlinePlayerRegistry; import com.disklexar.mmorpg.player.OnlinePlayerRegistry;
import com.disklexar.mmorpg.player.PlayerSessionService; import com.disklexar.mmorpg.player.PlayerSessionService;
import com.disklexar.mmorpg.ui.AbilityBarService; import com.disklexar.mmorpg.ui.AbilityBarService;
import com.disklexar.mmorpg.ui.CharacterSelectPage;
import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.logger.HytaleLogger;
@@ -61,8 +60,8 @@ public final class PlayerConnectionHandler {
InventoryUtils.clear(entityRef, store); InventoryUtils.clear(entityRef, store);
if (universePlayerRef != null) { if (universePlayerRef != null) {
scheduler.runLater(() -> openCharacterSelector( scheduler.runLater(() -> player.getWorld().execute(() ->
player, entityRef, store, universePlayerRef, characters, uuid), 500L); characters.handleJoinSelection(universePlayerRef, entityRef, store, uuid)), 2_500L);
} }
} catch (SQLException e) { } catch (SQLException e) {
logger.at(Level.SEVERE).log( logger.at(Level.SEVERE).log(
@@ -86,49 +85,23 @@ public final class PlayerConnectionHandler {
UUID accountUuid = playerRef.getUuid(); UUID accountUuid = playerRef.getUuid();
OnlinePlayerRegistry onlinePlayers = bootstrap.getRegistry().require(OnlinePlayerRegistry.class); OnlinePlayerRegistry onlinePlayers = bootstrap.getRegistry().require(OnlinePlayerRegistry.class);
AbilityBarService abilityBar = bootstrap.getRegistry().require(AbilityBarService.class); AbilityBarService abilityBar = bootstrap.getRegistry().require(AbilityBarService.class);
ArcherBowService archerBow = bootstrap.getRegistry().require(ArcherBowService.class);
CharacterService characters = bootstrap.getRegistry().require(CharacterService.class); CharacterService characters = bootstrap.getRegistry().require(CharacterService.class);
PlayerSessionService sessions = bootstrap.getRegistry().require(PlayerSessionService.class); PlayerSessionService sessions = bootstrap.getRegistry().require(PlayerSessionService.class);
OnlinePlayer online = onlinePlayers.get(accountUuid);
PlayerSessionService.AccountSession session = sessions.getAccountSession(accountUuid); PlayerSessionService.AccountSession session = sessions.getAccountSession(accountUuid);
Ref<EntityStore> entityRef = playerRef.getReference(); if (session != null && session.activeCharacter() != null) {
if (session != null // Use PlayerRef (entity ref or holder) so inventory is saved even when the entity
&& session.activeCharacter() != null // is already torn down at disconnect time.
&& online != null characters.persistActiveCharacter(session, playerRef);
&& entityRef != null
&& entityRef.isValid()) {
Store<EntityStore> store = online.world().getEntityStore().getStore();
characters.persistActiveCharacter(session, store, entityRef);
} }
onlinePlayers.remove(accountUuid); onlinePlayers.remove(accountUuid);
abilityBar.dismiss(accountUuid); abilityBar.dismiss(accountUuid);
archerBow.clear(accountUuid);
sessions.unload(accountUuid); sessions.unload(accountUuid);
} }
private void openCharacterSelector(
@Nonnull Player player,
@Nonnull Ref<EntityStore> entityRef,
@Nonnull Store<EntityStore> store,
@Nonnull PlayerRef playerRef,
@Nonnull CharacterService characters,
@Nonnull UUID accountUuid) {
player.getWorld().execute(() -> {
try {
player.getPageManager().openCustomPage(entityRef, store, new CharacterSelectPage(
playerRef,
accountUuid,
characters,
plugin.getBootstrap().getConfig().getMaxCharactersPerAccount()));
} catch (Throwable t) {
logger.at(Level.WARNING).log(
"Failed to open character selector for %s: %s",
playerRef.getUsername(),
t.getMessage());
}
});
}
@Nonnull @Nonnull
private UUID resolveUuid( private UUID resolveUuid(
@Nonnull Store<EntityStore> store, @Nonnull Store<EntityStore> store,
@@ -0,0 +1,98 @@
package com.disklexar.mmorpg.interaction;
import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.combat.ArcherBowService;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.protocol.InteractionState;
import com.hypixel.hytale.protocol.InteractionType;
import com.hypixel.hytale.server.core.entity.InteractionContext;
import com.hypixel.hytale.server.core.modules.interaction.interaction.CooldownHandler;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.RootInteraction;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.SimpleInstantInteraction;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Replaces vanilla bow Primary for archers: one click = instant max-power shot, 1s cooldown between clicks.
*/
public final class MmorpgArcherShootInteraction extends SimpleInstantInteraction {
public static final String ID = "mmorpg_archer_shoot";
private static final String VANILLA_PRIMARY = "Root_Weapon_Shortbow_Primary_Shoot";
public static final BuilderCodec<MmorpgArcherShootInteraction> CODEC = BuilderCodec.builder(
MmorpgArcherShootInteraction.class,
MmorpgArcherShootInteraction::new,
SimpleInstantInteraction.CODEC)
.build();
public MmorpgArcherShootInteraction() {
}
public MmorpgArcherShootInteraction(@Nonnull String id) {
super(id);
}
@Override
protected void firstRun(
@Nonnull InteractionType interactionType,
@Nonnull InteractionContext interactionContext,
@Nonnull CooldownHandler cooldownHandler) {
Ref<EntityStore> entityRef = interactionContext.getEntity();
if (entityRef == null || !entityRef.isValid()) {
interactionContext.getState().state = InteractionState.Failed;
return;
}
Store<EntityStore> store = entityRef.getStore();
PlayerRef playerRef = store.getComponent(entityRef, PlayerRef.getComponentType());
if (playerRef == null) {
interactionContext.getState().state = InteractionState.Failed;
return;
}
ArcherBowService archerBow = resolveService();
if (archerBow == null) {
interactionContext.getState().state = InteractionState.Failed;
return;
}
if (!archerBow.isArcher(playerRef)) {
runVanillaPrimary(interactionContext);
return;
}
ArcherBowService.ShootResult result = archerBow.tryShoot(
playerRef,
entityRef,
store,
interactionContext);
interactionContext.getState().state = switch (result) {
case OK, ON_COOLDOWN -> InteractionState.Finished;
case NO_ARROW, NOT_BOW, PROJECTILE_UNAVAILABLE -> InteractionState.Failed;
};
}
private void runVanillaPrimary(@Nonnull InteractionContext interactionContext) {
RootInteraction vanilla = RootInteraction.getAssetMap().getAsset(VANILLA_PRIMARY);
if (vanilla != null) {
interactionContext.execute(vanilla);
}
interactionContext.getState().state = InteractionState.Finished;
}
@Nullable
private static ArcherBowService resolveService() {
MmorpgPlugin plugin = MmorpgPlugin.get();
if (plugin == null || plugin.getBootstrap() == null) {
return null;
}
return plugin.getBootstrap().getRegistry().get(ArcherBowService.class);
}
}
@@ -2,6 +2,7 @@ package com.disklexar.mmorpg.persistence.repository;
import com.disklexar.mmorpg.persistence.DatabaseManager; import com.disklexar.mmorpg.persistence.DatabaseManager;
import com.disklexar.mmorpg.player.model.PlayerProfile; import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.stats.CharacterStat;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
@@ -42,8 +43,15 @@ public final class CharacterRepository {
return findByQuery("WHERE account_uuid = ? ORDER BY date_creation ASC", accountUuid.toString()); return findByQuery("WHERE account_uuid = ? ORDER BY date_creation ASC", accountUuid.toString());
} }
@Nonnull
public List<PlayerProfile> findAliveByAccountUuid(@Nonnull UUID accountUuid) throws SQLException {
return findByQuery(
"WHERE account_uuid = ? AND death_time IS NULL ORDER BY date_creation ASC",
accountUuid.toString());
}
public int countByAccount(@Nonnull UUID accountUuid) throws SQLException { public int countByAccount(@Nonnull UUID accountUuid) throws SQLException {
String sql = "SELECT COUNT(*) FROM characters WHERE account_uuid = ?"; String sql = "SELECT COUNT(*) FROM characters WHERE account_uuid = ? AND death_time IS NULL";
try (PreparedStatement statement = connection().prepareStatement(sql)) { try (PreparedStatement statement = connection().prepareStatement(sql)) {
statement.setString(1, accountUuid.toString()); statement.setString(1, accountUuid.toString());
try (ResultSet resultSet = statement.executeQuery()) { try (ResultSet resultSet = statement.executeQuery()) {
@@ -72,8 +80,10 @@ public final class CharacterRepository {
String sql = """ String sql = """
INSERT INTO characters ( INSERT INTO characters (
id, account_uuid, name, level, experience, class_id, powers, jobs, id, account_uuid, name, level, experience, class_id, powers, jobs,
guild_id, group_id, money, race_id, date_creation, updated_at) guild_id, group_id, money, race_id, date_creation, updated_at,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) death_time, total_time_play, stat_points, stat_force, stat_vitality,
stat_dexterity, stat_speed)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET ON CONFLICT(id) DO UPDATE SET
name = excluded.name, name = excluded.name,
level = excluded.level, level = excluded.level,
@@ -85,7 +95,14 @@ public final class CharacterRepository {
group_id = excluded.group_id, group_id = excluded.group_id,
money = excluded.money, money = excluded.money,
race_id = excluded.race_id, race_id = excluded.race_id,
updated_at = excluded.updated_at updated_at = excluded.updated_at,
death_time = excluded.death_time,
total_time_play = excluded.total_time_play,
stat_points = excluded.stat_points,
stat_force = excluded.stat_force,
stat_vitality = excluded.stat_vitality,
stat_dexterity = excluded.stat_dexterity,
stat_speed = excluded.stat_speed
"""; """;
try (PreparedStatement statement = connection().prepareStatement(sql)) { try (PreparedStatement statement = connection().prepareStatement(sql)) {
@@ -103,6 +120,17 @@ public final class CharacterRepository {
statement.setString(12, profile.getRaceId()); statement.setString(12, profile.getRaceId());
statement.setLong(13, profile.getDateCreation()); statement.setLong(13, profile.getDateCreation());
statement.setLong(14, profile.getUpdatedAt()); statement.setLong(14, profile.getUpdatedAt());
if (profile.getDeathTime() != null) {
statement.setLong(15, profile.getDeathTime());
} else {
statement.setNull(15, java.sql.Types.INTEGER);
}
statement.setLong(16, profile.getTotalTimePlay());
statement.setInt(17, profile.getStatPoints());
statement.setInt(18, profile.getStat(CharacterStat.FORCE));
statement.setInt(19, profile.getStat(CharacterStat.VITALITY));
statement.setInt(20, profile.getStat(CharacterStat.DEXTERITY));
statement.setInt(21, profile.getStat(CharacterStat.SPEED));
statement.executeUpdate(); statement.executeUpdate();
} }
@@ -123,7 +151,9 @@ public final class CharacterRepository {
throws SQLException { throws SQLException {
String sql = """ String sql = """
SELECT id, account_uuid, name, level, experience, class_id, powers, jobs, SELECT id, account_uuid, name, level, experience, class_id, powers, jobs,
guild_id, group_id, money, race_id, date_creation, updated_at guild_id, group_id, money, race_id, date_creation, updated_at,
death_time, total_time_play, stat_points, stat_force, stat_vitality,
stat_dexterity, stat_speed
FROM characters FROM characters
""" + whereClause; """ + whereClause;
@@ -159,6 +189,16 @@ public final class CharacterRepository {
String raceId = resultSet.getString("race_id"); String raceId = resultSet.getString("race_id");
profile.setRaceId(raceId != null ? raceId : "human"); profile.setRaceId(raceId != null ? raceId : "human");
profile.setUpdatedAt(resultSet.getLong("updated_at")); profile.setUpdatedAt(resultSet.getLong("updated_at"));
long deathTime = resultSet.getLong("death_time");
if (!resultSet.wasNull()) {
profile.setDeathTime(deathTime);
}
profile.setTotalTimePlay(resultSet.getLong("total_time_play"));
profile.setStatPoints(resultSet.getInt("stat_points"));
profile.setStat(CharacterStat.FORCE, resultSet.getInt("stat_force"));
profile.setStat(CharacterStat.VITALITY, resultSet.getInt("stat_vitality"));
profile.setStat(CharacterStat.DEXTERITY, resultSet.getInt("stat_dexterity"));
profile.setStat(CharacterStat.SPEED, resultSet.getInt("stat_speed"));
return profile; return profile;
} }
@@ -27,7 +27,10 @@ public final class SchemaInitializer {
"001_init.sql", "001_init.sql",
"002_rpg.sql", "002_rpg.sql",
"003_web_password.sql", "003_web_password.sql",
"004_characters.sql"); "004_characters.sql",
"005_character_death_playtime.sql",
"006_character_stats.sql",
"007_trim_character_stats.sql");
private SchemaInitializer() { private SchemaInitializer() {
} }
@@ -2,6 +2,7 @@ package com.disklexar.mmorpg.player;
import com.disklexar.mmorpg.persistence.repository.CharacterInventoryRepository; import com.disklexar.mmorpg.persistence.repository.CharacterInventoryRepository;
import com.disklexar.mmorpg.persistence.repository.CharacterInventoryRepository.InventorySlotRecord; import com.disklexar.mmorpg.persistence.repository.CharacterInventoryRepository.InventorySlotRecord;
import com.hypixel.hytale.component.Holder;
import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.inventory.InventoryComponent; import com.hypixel.hytale.server.core.inventory.InventoryComponent;
@@ -9,6 +10,7 @@ import com.hypixel.hytale.server.core.inventory.InventoryUtils;
import com.hypixel.hytale.server.core.inventory.ItemStack; import com.hypixel.hytale.server.core.inventory.ItemStack;
import com.hypixel.hytale.server.core.inventory.container.ItemContainer; import com.hypixel.hytale.server.core.inventory.container.ItemContainer;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import org.bson.BsonDocument;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -26,7 +28,9 @@ public final class CharacterInventoryService {
InventoryComponent.STORAGE_SECTION_ID, InventoryComponent.STORAGE_SECTION_ID,
InventoryComponent.HOTBAR_SECTION_ID, InventoryComponent.HOTBAR_SECTION_ID,
InventoryComponent.ARMOR_SECTION_ID, InventoryComponent.ARMOR_SECTION_ID,
InventoryComponent.UTILITY_SECTION_ID InventoryComponent.UTILITY_SECTION_ID,
InventoryComponent.TOOLS_SECTION_ID,
InventoryComponent.BACKPACK_SECTION_ID,
}; };
private final CharacterInventoryRepository repository; private final CharacterInventoryRepository repository;
@@ -42,6 +46,12 @@ public final class CharacterInventoryService {
repository.replaceAll(characterId, captureFromPlayer(store, ref)); repository.replaceAll(characterId, captureFromPlayer(store, ref));
} }
public void saveFromHolder(
@Nonnull UUID characterId,
@Nonnull Holder<EntityStore> holder) throws SQLException {
repository.replaceAll(characterId, captureFromHolder(holder));
}
public void loadToPlayer( public void loadToPlayer(
@Nonnull UUID characterId, @Nonnull UUID characterId,
@Nonnull Store<EntityStore> store, @Nonnull Store<EntityStore> store,
@@ -55,10 +65,32 @@ public final class CharacterInventoryService {
@Nonnull Ref<EntityStore> ref) { @Nonnull Ref<EntityStore> ref) {
List<InventorySlotRecord> slots = new ArrayList<>(); List<InventorySlotRecord> slots = new ArrayList<>();
for (int sectionId : SECTION_IDS) { for (int sectionId : SECTION_IDS) {
ItemContainer container = sectionContainer(store, ref, sectionId); captureSection(slots, sectionId, sectionContainer(store, ref, sectionId));
if (container == null) { }
return slots;
}
@Nonnull
public List<InventorySlotRecord> captureFromHolder(@Nonnull Holder<EntityStore> holder) {
List<InventorySlotRecord> slots = new ArrayList<>();
for (int sectionId : SECTION_IDS) {
var type = InventoryComponent.getComponentTypeById(sectionId);
if (type == null) {
continue; continue;
} }
InventoryComponent component = holder.getComponent(type);
captureSection(slots, sectionId, component == null ? null : component.getInventory());
}
return slots;
}
private static void captureSection(
@Nonnull List<InventorySlotRecord> slots,
int sectionId,
@Nullable ItemContainer container) {
if (container == null) {
return;
}
for (short slot = 0; slot < container.getCapacity(); slot++) { for (short slot = 0; slot < container.getCapacity(); slot++) {
ItemStack stack = container.getItemStack(slot); ItemStack stack = container.getItemStack(slot);
if (ItemStack.isEmpty(stack)) { if (ItemStack.isEmpty(stack)) {
@@ -77,11 +109,9 @@ public final class CharacterInventoryService {
stack.getQuantity(), stack.getQuantity(),
durability, durability,
maxDurability, maxDurability,
null)); metadataJson(stack)));
} }
} }
return slots;
}
public void applyToPlayer( public void applyToPlayer(
@Nonnull List<InventorySlotRecord> slots, @Nonnull List<InventorySlotRecord> slots,
@@ -103,7 +133,10 @@ public final class CharacterInventoryService {
@Nonnull @Nonnull
private static ItemStack buildStack(@Nonnull InventorySlotRecord slot) { private static ItemStack buildStack(@Nonnull InventorySlotRecord slot) {
ItemStack stack = new ItemStack(slot.itemId(), slot.quantity()); BsonDocument metadata = parseMetadata(slot.metadataJson());
ItemStack stack = metadata == null
? new ItemStack(slot.itemId(), slot.quantity())
: new ItemStack(slot.itemId(), slot.quantity(), metadata);
if (slot.maxDurability() != null && slot.maxDurability() > 0) { if (slot.maxDurability() != null && slot.maxDurability() > 0) {
stack = stack.withMaxDurability(slot.maxDurability()); stack = stack.withMaxDurability(slot.maxDurability());
if (slot.durability() != null) { if (slot.durability() != null) {
@@ -113,16 +146,28 @@ public final class CharacterInventoryService {
return stack; return stack;
} }
@Nullable
private static String metadataJson(@Nonnull ItemStack stack) {
BsonDocument metadata = stack.getMetadata();
if (metadata == null || metadata.isEmpty()) {
return null;
}
return metadata.toJson();
}
@Nullable
private static BsonDocument parseMetadata(@Nullable String metadataJson) {
if (metadataJson == null || metadataJson.isBlank()) {
return null;
}
return BsonDocument.parse(metadataJson);
}
@Nullable @Nullable
private static ItemContainer sectionContainer( private static ItemContainer sectionContainer(
@Nonnull Store<EntityStore> store, @Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref, @Nonnull Ref<EntityStore> ref,
int sectionId) { int sectionId) {
var type = InventoryComponent.getComponentTypeById(sectionId); return InventoryUtils.getSectionById(ref, sectionId, store);
if (type == null) {
return null;
}
InventoryComponent component = store.getComponent(ref, type);
return component == null ? null : component.getInventory();
} }
} }
@@ -1,20 +1,24 @@
package com.disklexar.mmorpg.player; package com.disklexar.mmorpg.player;
import com.disklexar.mmorpg.MmorpgPlugin; import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.combat.MmorpgScheduler;
import com.disklexar.mmorpg.core.config.MmorpgConfig; import com.disklexar.mmorpg.core.config.MmorpgConfig;
import com.disklexar.mmorpg.persistence.repository.CharacterRepository; import com.disklexar.mmorpg.persistence.repository.CharacterRepository;
import com.disklexar.mmorpg.persistence.repository.PlayerAccountRepository; import com.disklexar.mmorpg.persistence.repository.PlayerAccountRepository;
import com.disklexar.mmorpg.player.model.PlayerAccount; import com.disklexar.mmorpg.player.model.PlayerAccount;
import com.disklexar.mmorpg.player.model.PlayerProfile; import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.clazz.ClassCatalog;
import com.disklexar.mmorpg.rpg.clazz.ClassService; import com.disklexar.mmorpg.rpg.clazz.ClassService;
import com.disklexar.mmorpg.rpg.clazz.ClassWeaponService;
import com.disklexar.mmorpg.rpg.clazz.PlayerClass;
import com.disklexar.mmorpg.rpg.race.RaceCatalog; import com.disklexar.mmorpg.rpg.race.RaceCatalog;
import com.disklexar.mmorpg.rpg.stats.CharacterStatService;
import com.disklexar.mmorpg.ui.AbilityBarService; import com.disklexar.mmorpg.ui.AbilityBarService;
import com.disklexar.mmorpg.ui.CharacterSelectPage;
import com.hypixel.hytale.component.Holder;
import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.protocol.packets.interface_.Page;
import com.hypixel.hytale.server.core.entity.entities.Player; import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.inventory.InventoryUtils; import com.hypixel.hytale.server.core.inventory.InventoryUtils;
import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.PlayerRef;
@@ -63,7 +67,7 @@ public final class CharacterService {
@Nonnull @Nonnull
public List<PlayerProfile> listCharacters(@Nonnull UUID accountUuid) throws SQLException { public List<PlayerProfile> listCharacters(@Nonnull UUID accountUuid) throws SQLException {
return characters.findByAccountUuid(accountUuid); return characters.findAliveByAccountUuid(accountUuid);
} }
@Nonnull @Nonnull
@@ -71,7 +75,7 @@ public final class CharacterService {
@Nonnull UUID accountUuid, @Nonnull UUID accountUuid,
@Nonnull String query) throws SQLException { @Nonnull String query) throws SQLException {
String trimmed = query.trim(); String trimmed = query.trim();
for (PlayerProfile profile : characters.findByAccountUuid(accountUuid)) { for (PlayerProfile profile : characters.findAliveByAccountUuid(accountUuid)) {
if (profile.getCharacterId().toString().equalsIgnoreCase(trimmed) if (profile.getCharacterId().toString().equalsIgnoreCase(trimmed)
|| profile.getDisplayName().equalsIgnoreCase(trimmed)) { || profile.getDisplayName().equalsIgnoreCase(trimmed)) {
return Optional.of(profile); return Optional.of(profile);
@@ -174,6 +178,65 @@ public final class CharacterService {
return CharacterDeletionResult.success(found.get().getDisplayName()); return CharacterDeletionResult.success(found.get().getDisplayName());
} }
/**
* Resolves character selection on join without Custom UI (asset pack not required).
*/
public void handleJoinSelection(
@Nonnull PlayerRef playerRef,
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull UUID accountUuid) {
try {
List<PlayerProfile> characters = listCharacters(accountUuid);
if (characters.isEmpty()) {
CharacterCreationResult created = createDefaultCharacter(
accountUuid, playerRef.getUsername());
if (created.ok() && created.profile() != null) {
activateCharacter(
playerRef, ref, store, accountUuid, created.profile().getCharacterId());
} else {
String error = created.errorMessage() != null
? created.errorMessage()
: "Création impossible.";
playerRef.sendMessage(Message.raw(error));
playerRef.sendMessage(Message.raw(
"Créez un personnage : /mmorpg character creer [nom]"));
}
return;
}
if (characters.size() == 1) {
activateCharacter(
playerRef, ref, store, accountUuid, characters.get(0).getCharacterId());
return;
}
PlayerSessionService.AccountSession session = sessions.getAccountSession(accountUuid);
UUID activeId = session == null ? null : session.account().getActiveCharacterId();
if (activeId != null) {
for (PlayerProfile profile : characters) {
if (profile.getCharacterId().equals(activeId)) {
activateCharacter(playerRef, ref, store, accountUuid, activeId);
return;
}
}
}
sessions.beginCharacterSelection(accountUuid);
showCharacterSelectorUi(playerRef, ref, store, accountUuid, null);
} catch (SQLException e) {
logger.at(Level.WARNING).log(
"Failed join character selection for %s: %s",
playerRef.getUsername(),
e.getMessage());
playerRef.sendMessage(Message.raw(
"Erreur lors du chargement de vos personnages."));
playerRef.sendMessage(Message.raw(
"Utilisez /mmorpg character liste puis /mmorpg character choisir <nom>"));
}
}
public void openCharacterSelector( public void openCharacterSelector(
@Nonnull PlayerRef playerRef, @Nonnull PlayerRef playerRef,
@Nonnull Ref<EntityStore> ref, @Nonnull Ref<EntityStore> ref,
@@ -190,15 +253,49 @@ public final class CharacterService {
bootstrap.getRegistry().require(AbilityBarService.class).dismiss(accountUuid); bootstrap.getRegistry().require(AbilityBarService.class).dismiss(accountUuid);
} }
showCharacterSelectorUi(playerRef, ref, store, accountUuid, null);
}
/**
* Prompts character selection via chat (after respawn or voluntary switch).
*/
public void showCharacterSelectorUi(
@Nonnull PlayerRef playerRef,
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull UUID accountUuid,
@Nullable String deathCharacterName) {
InventoryUtils.clear(ref, store); InventoryUtils.clear(ref, store);
if (deathCharacterName != null) {
playerRef.sendMessage(Message.raw(
"Votre personnage « " + deathCharacterName + " » est mort définitivement."));
playerRef.sendMessage(Message.raw("Choisissez un autre personnage ou créez-en un nouveau."));
}
Player player = store.getComponent(ref, Player.getComponentType()); Player player = store.getComponent(ref, Player.getComponentType());
if (player != null) { if (player != null) {
player.getPageManager().openCustomPage(ref, store, new com.disklexar.mmorpg.ui.CharacterSelectPage( try {
player.getPageManager().openCustomPage(ref, store, new CharacterSelectPage(
playerRef, playerRef,
accountUuid, accountUuid,
this, this,
config.getMaxCharactersPerAccount())); config.getMaxCharactersPerAccount()));
return;
} catch (Throwable t) {
logger.at(Level.WARNING).log(
"Character selector UI failed for %s: %s",
playerRef.getUsername(),
t.getMessage());
}
}
try {
sendCharacterSelectionPrompt(playerRef, listCharacters(accountUuid));
} catch (SQLException e) {
logger.at(Level.WARNING).log(
"Failed to list characters for %s: %s", playerRef.getUsername(), e.getMessage());
playerRef.sendMessage(Message.raw("Erreur lors du chargement de vos personnages."));
} }
} }
@@ -212,6 +309,9 @@ public final class CharacterService {
if (found.isEmpty() || !found.get().getAccountUuid().equals(accountUuid)) { if (found.isEmpty() || !found.get().getAccountUuid().equals(accountUuid)) {
throw new SQLException("Character not found for account: " + characterId); throw new SQLException("Character not found for account: " + characterId);
} }
if (found.get().isDead()) {
throw new SQLException("Character is dead: " + characterId);
}
PlayerSessionService.AccountSession session = sessions.requireSession(accountUuid); PlayerSessionService.AccountSession session = sessions.requireSession(accountUuid);
if (session.activeCharacter() != null if (session.activeCharacter() != null
@@ -227,6 +327,13 @@ public final class CharacterService {
InventoryUtils.clear(ref, store); InventoryUtils.clear(ref, store);
inventory.loadToPlayer(characterId, store, ref); inventory.loadToPlayer(characterId, store, ref);
PlayerClass playerClass = ClassCatalog.byId(profile.getClassId());
if (playerClass != null && ClassCatalog.ARCHER.id().equals(playerClass.id())) {
if (ClassWeaponService.ensureArcherArrowsIfMissing(store, ref)) {
inventory.saveFromPlayer(characterId, store, ref);
}
}
account.setActiveCharacterId(characterId); account.setActiveCharacterId(characterId);
account.touch(); account.touch();
accounts.setActiveCharacter(accountUuid, characterId); accounts.setActiveCharacter(accountUuid, characterId);
@@ -234,26 +341,159 @@ public final class CharacterService {
sessions.activateCharacter(accountUuid, profile); sessions.activateCharacter(accountUuid, profile);
completeLogin(playerRef, ref, store, profile); completeLogin(playerRef, ref, store, profile);
var bootstrap = plugin.getBootstrap();
if (bootstrap != null) {
CharacterStatService characterStats = bootstrap.getRegistry().require(CharacterStatService.class);
characterStats.applyToEntity(store, ref, profile);
}
} }
public void persistActiveCharacter( public void persistActiveCharacter(
@Nonnull PlayerSessionService.AccountSession session, @Nonnull PlayerSessionService.AccountSession session,
@Nonnull Store<EntityStore> store, @Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref) { @Nonnull Ref<EntityStore> ref) {
persistActiveCharacterInternal(session, characterId ->
inventory.saveFromPlayer(characterId, store, ref));
}
public void persistActiveCharacter(
@Nonnull PlayerSessionService.AccountSession session,
@Nonnull PlayerRef playerRef) {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null && ref.isValid()) {
persistActiveCharacter(session, ref.getStore(), ref);
return;
}
Holder<EntityStore> holder = playerRef.getHolder();
if (holder != null) {
persistActiveCharacterInternal(session, characterId ->
inventory.saveFromHolder(characterId, holder));
}
}
/**
* Saves every connected player's inventory to SQLite (called periodically while online).
*/
public void persistAllOnlineInventories(@Nonnull OnlinePlayerRegistry onlinePlayers) {
for (OnlinePlayer online : onlinePlayers.all()) {
PlayerSessionService.AccountSession session =
sessions.getAccountSession(online.playerRef().getUuid());
if (session == null || session.activeCharacter() == null) {
continue;
}
Ref<EntityStore> ref = online.entityRef();
if (ref == null || !ref.isValid()) {
continue;
}
online.world().execute(() -> persistActiveCharacter(
session, online.world().getEntityStore().getStore(), ref));
}
}
private void persistActiveCharacterInternal(
@Nonnull PlayerSessionService.AccountSession session,
@Nonnull InventorySaveAction saveAction) {
PlayerProfile active = session.activeCharacter(); PlayerProfile active = session.activeCharacter();
if (active == null) { if (active == null) {
return; return;
} }
try { try {
sessions.flushCharacterPlaytime(session.account().getUuid());
active.touch(); active.touch();
characters.save(active); characters.save(active);
inventory.saveFromPlayer(active.getCharacterId(), store, ref); saveAction.run(active.getCharacterId());
} catch (SQLException e) { } catch (SQLException e) {
logger.at(Level.WARNING).log( logger.at(Level.WARNING).log(
"Failed to persist character %s: %s", active.getCharacterId(), e.getMessage()); "Failed to persist character %s: %s", active.getCharacterId(), e.getMessage());
} }
} }
@FunctionalInterface
private interface InventorySaveAction {
void run(@Nonnull UUID characterId) throws SQLException;
}
/**
* Persists death data immediately; the selector opens after respawn.
*/
public void retireActiveCharacterOnDeath(
@Nonnull PlayerRef playerRef,
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull UUID accountUuid) {
PlayerSessionService.AccountSession session = sessions.getAccountSession(accountUuid);
if (session == null || session.activeCharacter() == null) {
return;
}
PlayerProfile active = session.activeCharacter();
try {
sessions.flushCharacterPlaytime(accountUuid);
active.touch();
active.markDead(System.currentTimeMillis());
inventory.saveFromPlayer(active.getCharacterId(), store, ref);
characters.save(active);
PlayerAccount account = session.account();
account.setActiveCharacterId(null);
account.touch();
accounts.setActiveCharacter(accountUuid, null);
accounts.save(account);
logger.at(Level.INFO).log(
"Character %s (%s) died permanently for account %s",
active.getDisplayName(),
active.getCharacterId(),
accountUuid);
var bootstrap = plugin.getBootstrap();
if (bootstrap != null) {
bootstrap.getRegistry().require(OnlinePlayerRegistry.class).remove(accountUuid);
bootstrap.getRegistry().require(AbilityBarService.class).dismiss(accountUuid);
}
sessions.beginCharacterSelection(accountUuid);
sessions.markSelectorAfterRespawn(accountUuid, active.getDisplayName());
} catch (SQLException e) {
logger.at(Level.WARNING).log(
"Failed to process character death for %s: %s", active.getCharacterId(), e.getMessage());
}
}
/**
* Opens the character selector once the player has respawned.
*/
public void openCharacterSelectorAfterRespawn(
@Nonnull PlayerRef playerRef,
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull UUID accountUuid) {
String characterName = sessions.consumeSelectorAfterRespawn(accountUuid);
if (characterName == null) {
return;
}
showCharacterSelectorUi(playerRef, ref, store, accountUuid, characterName);
}
public void openClassSelector(
@Nonnull PlayerRef playerRef,
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull PlayerProfile profile,
boolean mandatory,
boolean welcomeAfterPick) {
PlayerProfile active = sessions.getSession(playerRef.getUuid());
sendClassSelectionPrompt(playerRef, active != null ? active : profile);
}
public void welcomeAfterFirstClassChoice(
@Nonnull PlayerRef playerRef,
@Nonnull PlayerProfile profile) {
sendWelcomeMessages(playerRef, profile);
}
private void completeLogin( private void completeLogin(
@Nonnull PlayerRef playerRef, @Nonnull PlayerRef playerRef,
@Nonnull Ref<EntityStore> ref, @Nonnull Ref<EntityStore> ref,
@@ -269,7 +509,71 @@ public final class CharacterService {
if (player != null) { if (player != null) {
onlinePlayers.add(new OnlinePlayer( onlinePlayers.add(new OnlinePlayer(
profile.getUuid(), playerRef, ref, player.getWorld())); profile.getUuid(), playerRef, ref, player.getWorld()));
player.getPageManager().setPage(ref, store, Page.None); }
ClassService classService = bootstrap.getRegistry().require(ClassService.class);
OnlinePlayer online = onlinePlayers.get(profile.getUuid());
if (online != null) {
bootstrap.getRegistry().require(AbilityBarService.class).sync(online, profile);
}
if (!classService.hasClass(profile)) {
sendClassSelectionPrompt(playerRef, profile);
return;
}
sendWelcomeMessages(playerRef, profile);
}
private void sendCharacterSelectionPrompt(
@Nonnull PlayerRef playerRef,
@Nonnull List<PlayerProfile> profiles) {
playerRef.sendMessage(Message.raw("=== Sélection du personnage ==="));
if (profiles.isEmpty()) {
playerRef.sendMessage(Message.raw("Aucun personnage vivant."));
playerRef.sendMessage(Message.raw("Créez-en un : /mmorpg character creer [nom]"));
return;
}
for (int i = 0; i < profiles.size(); i++) {
PlayerProfile profile = profiles.get(i);
PlayerClass playerClass = ClassCatalog.byId(profile.getClassId());
String className = playerClass == null ? "Sans classe" : playerClass.displayName();
playerRef.sendMessage(Message.raw(String.format(
Locale.FRENCH,
"%d. %s — Nv.%d — %s",
i + 1,
profile.getDisplayName(),
profile.getLevel(),
className)));
}
playerRef.sendMessage(Message.raw("Choisissez : /mmorpg character choisir <nom>"));
playerRef.sendMessage(Message.raw("Nouveau : /mmorpg character creer [nom]"));
}
private void sendClassSelectionPrompt(
@Nonnull PlayerRef playerRef,
@Nonnull PlayerProfile profile) {
playerRef.sendMessage(Message.raw("=== Choisissez votre classe ==="));
playerRef.sendMessage(Message.raw("Personnage : " + profile.getDisplayName()));
for (PlayerClass playerClass : ClassCatalog.all()) {
playerRef.sendMessage(Message.raw(String.format(
Locale.FRENCH,
"• %s (%s) — %s",
playerClass.displayName(),
playerClass.id(),
playerClass.description())));
}
playerRef.sendMessage(Message.raw("Commande : /mmorpg class choisir <classe>"));
playerRef.sendMessage(Message.raw("Ex. : /mmorpg class choisir knight"));
}
private void sendWelcomeMessages(
@Nonnull PlayerRef playerRef,
@Nonnull PlayerProfile profile) {
var bootstrap = plugin.getBootstrap();
if (bootstrap == null) {
return;
} }
playerRef.sendMessage(Message.raw(String.format( playerRef.sendMessage(Message.raw(String.format(
@@ -278,13 +582,12 @@ public final class CharacterService {
profile.getLevel(), profile.getLevel(),
RaceCatalog.byId(profile.getRaceId()).displayName()))); RaceCatalog.byId(profile.getRaceId()).displayName())));
playerRef.sendMessage(Message.raw( playerRef.sendMessage(Message.raw(
"Commandes : /mmorpg menu | /mmorpg character | /mmorpg class info")); "Commandes : /mmorpg menu | /mmorpg character | /mmorpg class selecteur"));
ClassService classService = bootstrap.getRegistry().require(ClassService.class); ClassService classService = bootstrap.getRegistry().require(ClassService.class);
AbilityBarService abilityBar = bootstrap.getRegistry().require(AbilityBarService.class);
MmorpgScheduler scheduler = bootstrap.getRegistry().require(MmorpgScheduler.class);
if (classService.hasClass(profile)) { if (classService.hasClass(profile)) {
scheduler.runLater(() -> abilityBar.sync(profile.getUuid(), profile), 5_000L); playerRef.sendMessage(Message.raw(
"Capacités : touches Use ability 1/2/3 ou /mmorpg ability <1|2|3>"));
} }
} }
@@ -8,6 +8,8 @@ import com.disklexar.mmorpg.rpg.job.JobDefinition;
import com.disklexar.mmorpg.rpg.power.PowerCatalog; import com.disklexar.mmorpg.rpg.power.PowerCatalog;
import com.disklexar.mmorpg.rpg.power.PowerDefinition; import com.disklexar.mmorpg.rpg.power.PowerDefinition;
import com.disklexar.mmorpg.rpg.race.RaceCatalog; import com.disklexar.mmorpg.rpg.race.RaceCatalog;
import com.disklexar.mmorpg.rpg.stats.CharacterStat;
import com.disklexar.mmorpg.rpg.stats.CharacterStatService;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.time.Duration; import java.time.Duration;
@@ -42,6 +44,10 @@ public final class PlayerInfoView {
entries.add(new Entry("Argent", profile.getMoney() + " money")); entries.add(new Entry("Argent", profile.getMoney() + " money"));
entries.add(new Entry("Race", RaceCatalog.byId(profile.getRaceId()).displayName())); entries.add(new Entry("Race", RaceCatalog.byId(profile.getRaceId()).displayName()));
entries.add(new Entry("Classe", className(profile))); entries.add(new Entry("Classe", className(profile)));
entries.add(new Entry("Points de stats", String.valueOf(profile.getStatPoints())));
for (CharacterStat stat : CharacterStat.values()) {
entries.add(new Entry(stat.displayName(), String.valueOf(profile.getStat(stat))));
}
entries.add(new Entry("Pouvoirs", powerNames(profile))); entries.add(new Entry("Pouvoirs", powerNames(profile)));
entries.add(new Entry("Métiers", jobNames(profile))); entries.add(new Entry("Métiers", jobNames(profile)));
entries.add(new Entry("Groupe", profile.getGroupId() != null ? "Oui" : "Aucun")); entries.add(new Entry("Groupe", profile.getGroupId() != null ? "Oui" : "Aucun"));
@@ -14,6 +14,7 @@ import javax.annotation.Nullable;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level; import java.util.logging.Level;
@@ -46,6 +47,9 @@ public final class PlayerSessionService implements Service {
private final HytaleLogger logger; private final HytaleLogger logger;
private final Map<UUID, AccountSession> sessions = new ConcurrentHashMap<>(); private final Map<UUID, AccountSession> sessions = new ConcurrentHashMap<>();
private final Map<UUID, Long> connectedAt = new ConcurrentHashMap<>(); private final Map<UUID, Long> connectedAt = new ConcurrentHashMap<>();
private final Map<UUID, Long> characterActiveSince = new ConcurrentHashMap<>();
private final Set<UUID> pendingSelectorAfterRespawn = ConcurrentHashMap.newKeySet();
private final Map<UUID, String> pendingDeathCharacterNames = new ConcurrentHashMap<>();
public PlayerSessionService( public PlayerSessionService(
@Nonnull MmorpgPlugin plugin, @Nonnull MmorpgPlugin plugin,
@@ -82,6 +86,9 @@ public final class PlayerSessionService implements Service {
} }
sessions.clear(); sessions.clear();
connectedAt.clear(); connectedAt.clear();
characterActiveSince.clear();
pendingSelectorAfterRespawn.clear();
pendingDeathCharacterNames.clear();
logger.at(Level.INFO).log("Player session service stopped"); logger.at(Level.INFO).log("Player session service stopped");
} }
@@ -121,12 +128,14 @@ public final class PlayerSessionService implements Service {
profile.applyAccountSession(current.account()); profile.applyAccountSession(current.account());
AccountSession updated = new AccountSession(current.account(), profile, false); AccountSession updated = new AccountSession(current.account(), profile, false);
sessions.put(accountUuid, updated); sessions.put(accountUuid, updated);
characterActiveSince.put(accountUuid, System.currentTimeMillis());
} }
/** /**
* Puts the account back into character-selection mode (active character cleared from session). * Puts the account back into character-selection mode (active character cleared from session).
*/ */
public void beginCharacterSelection(@Nonnull UUID accountUuid) { public void beginCharacterSelection(@Nonnull UUID accountUuid) {
flushCharacterPlaytime(accountUuid);
AccountSession current = sessions.get(accountUuid); AccountSession current = sessions.get(accountUuid);
if (current == null) { if (current == null) {
return; return;
@@ -177,6 +186,19 @@ public final class PlayerSessionService implements Service {
} }
} }
public void markSelectorAfterRespawn(@Nonnull UUID accountUuid, @Nonnull String characterName) {
pendingSelectorAfterRespawn.add(accountUuid);
pendingDeathCharacterNames.put(accountUuid, characterName);
}
@Nullable
public String consumeSelectorAfterRespawn(@Nonnull UUID accountUuid) {
if (!pendingSelectorAfterRespawn.remove(accountUuid)) {
return null;
}
return pendingDeathCharacterNames.remove(accountUuid);
}
public void saveActiveCharacter(@Nonnull UUID accountUuid) { public void saveActiveCharacter(@Nonnull UUID accountUuid) {
AccountSession session = sessions.get(accountUuid); AccountSession session = sessions.get(accountUuid);
if (session == null || session.activeCharacter() == null) { if (session == null || session.activeCharacter() == null) {
@@ -185,16 +207,26 @@ public final class PlayerSessionService implements Service {
saveCharacterQuietly(session.activeCharacter()); saveCharacterQuietly(session.activeCharacter());
} }
private void accumulatePlaytime(@Nonnull UUID accountUuid) { /**
Long start = connectedAt.remove(accountUuid); * Flushes elapsed play time for the active character slice (not account total).
*/
public void flushCharacterPlaytime(@Nonnull UUID accountUuid) {
Long characterStart = characterActiveSince.remove(accountUuid);
AccountSession session = sessions.get(accountUuid); AccountSession session = sessions.get(accountUuid);
if (start == null || session == null) { if (characterStart == null || session == null || session.activeCharacter() == null) {
return; return;
} }
session.account().addTimePlay(System.currentTimeMillis() - start); session.activeCharacter().addTimePlay(System.currentTimeMillis() - characterStart);
if (session.activeCharacter() != null) {
session.activeCharacter().applyAccountSession(session.account());
} }
private void accumulatePlaytime(@Nonnull UUID accountUuid) {
flushCharacterPlaytime(accountUuid);
Long accountStart = connectedAt.remove(accountUuid);
AccountSession session = sessions.get(accountUuid);
if (accountStart == null || session == null) {
return;
}
session.account().addTimePlay(System.currentTimeMillis() - accountStart);
} }
private void saveAccountQuietly(@Nonnull PlayerAccount account) { private void saveAccountQuietly(@Nonnull PlayerAccount account) {
@@ -1,5 +1,7 @@
package com.disklexar.mmorpg.player.model; package com.disklexar.mmorpg.player.model;
import com.disklexar.mmorpg.rpg.stats.CharacterStat;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@@ -31,6 +33,15 @@ public final class PlayerProfile {
private long money; private long money;
private String raceId = "human"; private String raceId = "human";
@Nullable
private Long deathTime;
private int statPoints;
private int statForce;
private int statVitality;
private int statDexterity;
private int statSpeed;
private final long dateCreation; private final long dateCreation;
private long updatedAt; private long updatedAt;
@@ -169,6 +180,56 @@ public final class PlayerProfile {
this.raceId = raceId; this.raceId = raceId;
} }
public boolean isDead() {
return deathTime != null;
}
@Nullable
public Long getDeathTime() {
return deathTime;
}
public void setDeathTime(@Nullable Long deathTime) {
this.deathTime = deathTime;
}
public void markDead(long when) {
this.deathTime = when;
}
public int getStatPoints() {
return statPoints;
}
public void setStatPoints(int statPoints) {
this.statPoints = Math.max(0, statPoints);
}
public void addStatPoints(int amount) {
if (amount > 0) {
this.statPoints += amount;
}
}
public int getStat(@Nonnull CharacterStat stat) {
return switch (stat) {
case FORCE -> statForce;
case VITALITY -> statVitality;
case DEXTERITY -> statDexterity;
case SPEED -> statSpeed;
};
}
public void setStat(@Nonnull CharacterStat stat, int value) {
int clamped = Math.max(0, value);
switch (stat) {
case FORCE -> statForce = clamped;
case VITALITY -> statVitality = clamped;
case DEXTERITY -> statDexterity = clamped;
case SPEED -> statSpeed = clamped;
}
}
public long getDateCreation() { public long getDateCreation() {
return dateCreation; return dateCreation;
} }
@@ -184,7 +245,6 @@ public final class PlayerProfile {
/** Copies account-level session fields onto this character view. */ /** Copies account-level session fields onto this character view. */
public void applyAccountSession(@Nonnull PlayerAccount account) { public void applyAccountSession(@Nonnull PlayerAccount account) {
setConnected(account.isConnected()); setConnected(account.isConnected());
setTotalTimePlay(account.getTotalTimePlay());
setLastDateConnected(account.getLastDateConnected()); setLastDateConnected(account.getLastDateConnected());
} }
@@ -3,6 +3,7 @@ package com.disklexar.mmorpg.progression;
import com.disklexar.mmorpg.player.model.PlayerProfile; import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.race.RaceCatalog; import com.disklexar.mmorpg.rpg.race.RaceCatalog;
import com.disklexar.mmorpg.rpg.race.RaceDefinition; import com.disklexar.mmorpg.rpg.race.RaceDefinition;
import com.disklexar.mmorpg.rpg.stats.CharacterStatService;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@@ -43,11 +44,16 @@ public final class ProgressionService {
} }
private void applyLevelUps(@Nonnull PlayerProfile profile) { private void applyLevelUps(@Nonnull PlayerProfile profile) {
int previousLevel = profile.getLevel();
long needed = xpForLevel(profile.getLevel()); long needed = xpForLevel(profile.getLevel());
while (profile.getExperience() >= needed) { while (profile.getExperience() >= needed) {
profile.setExperience(profile.getExperience() - needed); profile.setExperience(profile.getExperience() - needed);
profile.setLevel(profile.getLevel() + 1); profile.setLevel(profile.getLevel() + 1);
needed = xpForLevel(profile.getLevel()); needed = xpForLevel(profile.getLevel());
} }
int levelsGained = profile.getLevel() - previousLevel;
if (levelsGained > 0) {
profile.addStatPoints(levelsGained * CharacterStatService.POINTS_PER_LEVEL);
}
} }
} }
@@ -55,7 +55,7 @@ public final class ClassCatalog {
List.of(WEAPON_BOW, WEAPON_CROSSBOW), List.of(WEAPON_BOW, WEAPON_CROSSBOW),
List.of( List.of(
new AbilityDefinition(ABILITY_ARCHER_RAPIDFIRE, 1, "Tir rapide", new AbilityDefinition(ABILITY_ARCHER_RAPIDFIRE, 1, "Tir rapide",
"Augmente votre cadence de tir (+20% de dégâts) pendant 10s.", 12_000L), "Quadruple la cadence de tir de l'arc (4 tirs/s) pendant 10s.", 12_000L),
new AbilityDefinition(ABILITY_ARCHER_VOLLEY, 2, "Salve", new AbilityDefinition(ABILITY_ARCHER_VOLLEY, 2, "Salve",
"Bond en arrière puis décoche instantanément 6 flèches devant vous.", 10_000L), "Bond en arrière puis décoche instantanément 6 flèches devant vous.", 10_000L),
new AbilityDefinition(ABILITY_ARCHER_POWERSHOT, 3, "Tir puissant", new AbilityDefinition(ABILITY_ARCHER_POWERSHOT, 3, "Tir puissant",
@@ -88,6 +88,14 @@ public final class ClassCatalog {
"Pendant 10s, chaque coup applique un poison (dégâts magiques/seconde pendant 5s, cumulable).", "Pendant 10s, chaque coup applique un poison (dégâts magiques/seconde pendant 5s, cumulable).",
13_000L))); 13_000L)));
private static final Map<String, String> WEAPON_ITEM_IDS = Map.of(
WEAPON_LONGSWORD, "Weapon_Longsword_Mithril",
WEAPON_BATTLEAXE, "Weapon_Battleaxe_Mithril",
WEAPON_BOW, "Weapon_Shortbow_Cobalt",
WEAPON_CROSSBOW, "Weapon_Crossbow_Ancient_Steel",
WEAPON_STAFF, "Weapon_Staff_Mithril",
WEAPON_DAGGERS, "Weapon_Daggers_Mithril");
private static final Map<String, PlayerClass> BY_ID = new LinkedHashMap<>(); private static final Map<String, PlayerClass> BY_ID = new LinkedHashMap<>();
static { static {
@@ -116,4 +124,9 @@ public final class ClassCatalog {
public static List<PlayerClass> all() { public static List<PlayerClass> all() {
return List.copyOf(BY_ID.values()); return List.copyOf(BY_ID.values());
} }
@Nullable
public static String weaponItemId(@Nonnull String weaponKey) {
return WEAPON_ITEM_IDS.get(weaponKey);
}
} }
@@ -0,0 +1,73 @@
package com.disklexar.mmorpg.rpg.clazz;
import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.bootstrap.Bootstrap;
import com.disklexar.mmorpg.player.CharacterService;
import com.disklexar.mmorpg.player.OnlinePlayer;
import com.disklexar.mmorpg.player.OnlinePlayerRegistry;
import com.disklexar.mmorpg.player.PlayerSessionService;
import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.ui.AbilityBarService;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Shared logic for applying a class choice (command or UI).
*/
public final class ClassSelectionSupport {
private ClassSelectionSupport() {
}
/**
* Assigns the class, equips the starter weapon, syncs the ability bar and persists data.
*
* @return the chosen class, or {@code null} on failure.
*/
@Nullable
public static PlayerClass apply(
@Nonnull MmorpgPlugin plugin,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull PlayerProfile profile,
@Nonnull String classId) {
Bootstrap bootstrap = plugin.getBootstrap();
if (bootstrap == null) {
return null;
}
ClassService classService = bootstrap.getRegistry().require(ClassService.class);
if (!classService.choose(profile, classId)) {
return null;
}
PlayerClass chosen = classService.getClass(profile);
if (chosen != null) {
ClassWeaponService.giveStarterWeapon(store, ref, chosen);
}
AbilityBarService abilityBar = bootstrap.getRegistry().require(AbilityBarService.class);
OnlinePlayerRegistry onlinePlayers = bootstrap.getRegistry().require(OnlinePlayerRegistry.class);
OnlinePlayer online = onlinePlayers.get(playerRef.getUuid());
if (online != null) {
abilityBar.sync(online, profile);
}
CharacterService characters = bootstrap.getRegistry().require(CharacterService.class);
PlayerSessionService sessions = bootstrap.getRegistry().require(PlayerSessionService.class);
PlayerSessionService.AccountSession session = sessions.getAccountSession(playerRef.getUuid());
if (session != null) {
characters.persistActiveCharacter(session, store, ref);
} else {
sessions.saveActiveCharacter(playerRef.getUuid());
}
return chosen;
}
}
@@ -0,0 +1,110 @@
package com.disklexar.mmorpg.rpg.clazz;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.inventory.InventoryComponent;
import com.hypixel.hytale.server.core.inventory.InventoryUtils;
import com.hypixel.hytale.server.core.inventory.ItemStack;
import com.hypixel.hytale.server.core.inventory.container.ItemContainer;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Locale;
/**
* Equips the starter weapon for a chosen class.
*/
public final class ClassWeaponService {
private static final byte HOTBAR_WEAPON_SLOT = 0;
private static final byte HOTBAR_AMMO_SLOT = 1;
private static final String ARCHER_ARROW_ITEM = "Weapon_Arrow_Crude";
private static final int ARCHER_ARROW_QUANTITY = 64;
private ClassWeaponService() {
}
/**
* Places the class starter weapon in the hotbar and selects it.
* Archers also receive a stack of arrows in the adjacent hotbar slot.
*
* @return the item id given, or {@code null} when no weapon could be equipped.
*/
@Nullable
public static String giveStarterWeapon(
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerClass playerClass) {
String itemId = playerClass.starterWeaponItemId();
if (itemId == null || itemId.isBlank()) {
return null;
}
ItemContainer hotbar = InventoryUtils.getSectionById(
ref, InventoryComponent.HOTBAR_SECTION_ID, store);
if (hotbar == null) {
return null;
}
hotbar.setItemStackForSlot(HOTBAR_WEAPON_SLOT, new ItemStack(itemId, 1));
if (ClassCatalog.ARCHER.id().equals(playerClass.id())) {
ensureArcherArrowsIfMissing(store, ref);
}
InventoryUtils.setActiveSlot(
ref, InventoryComponent.HOTBAR_SECTION_ID, HOTBAR_WEAPON_SLOT, store);
return itemId;
}
/**
* Gives archer starter arrows when none are present (hotbar, storage or utility).
*
* @return {@code true} when a stack was added.
*/
public static boolean ensureArcherArrowsIfMissing(
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref) {
if (countArrows(store, ref) > 0) {
return false;
}
ItemContainer hotbar = InventoryUtils.getSectionById(
ref, InventoryComponent.HOTBAR_SECTION_ID, store);
if (hotbar == null) {
return false;
}
hotbar.setItemStackForSlot(HOTBAR_AMMO_SLOT, new ItemStack(ARCHER_ARROW_ITEM, ARCHER_ARROW_QUANTITY));
return true;
}
public static int countArrows(
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref) {
int total = 0;
total += countArrowsInSection(store, ref, InventoryComponent.HOTBAR_SECTION_ID);
total += countArrowsInSection(store, ref, InventoryComponent.STORAGE_SECTION_ID);
total += countArrowsInSection(store, ref, InventoryComponent.UTILITY_SECTION_ID);
return total;
}
private static int countArrowsInSection(
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
int sectionId) {
ItemContainer container = InventoryUtils.getSectionById(ref, sectionId, store);
if (container == null) {
return 0;
}
int count = 0;
for (short slot = 0; slot < container.getCapacity(); slot++) {
ItemStack stack = container.getItemStack(slot);
if (ItemStack.isEmpty(stack)) {
continue;
}
String itemId = stack.getItemId();
if (itemId != null && itemId.toLowerCase(Locale.ROOT).contains("arrow")) {
count += stack.getQuantity();
}
}
return count;
}
}
@@ -1,6 +1,7 @@
package com.disklexar.mmorpg.rpg.clazz; package com.disklexar.mmorpg.rpg.clazz;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List; import java.util.List;
/** /**
@@ -21,4 +22,13 @@ public record PlayerClass(
} }
return false; return false;
} }
/** Starter weapon item id given when the player picks this class. */
@Nullable
public String starterWeaponItemId() {
if (weapons.isEmpty()) {
return null;
}
return ClassCatalog.weaponItemId(weapons.get(0));
}
} }
@@ -0,0 +1,43 @@
package com.disklexar.mmorpg.rpg.stats;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Allocatable character attributes. Each point grants a fixed combat bonus (see {@link CharacterStatService}).
*/
public enum CharacterStat {
FORCE("force", "Force"),
VITALITY("vitality", "Vitalité"),
DEXTERITY("dexterity", "Dextérité"),
SPEED("speed", "Vitesse");
private final String id;
private final String displayName;
CharacterStat(@Nonnull String id, @Nonnull String displayName) {
this.id = id;
this.displayName = displayName;
}
@Nonnull
public String id() {
return id;
}
@Nonnull
public String displayName() {
return displayName;
}
@Nullable
public static CharacterStat byId(@Nonnull String raw) {
String normalized = raw.trim().toLowerCase();
for (CharacterStat stat : values()) {
if (stat.id.equals(normalized)) {
return stat;
}
}
return null;
}
}
@@ -0,0 +1,161 @@
package com.disklexar.mmorpg.rpg.stats;
import com.disklexar.mmorpg.player.PlayerSessionService;
import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.protocol.MovementSettings;
import com.hypixel.hytale.server.core.entity.entities.player.movement.MovementManager;
import com.hypixel.hytale.server.core.modules.entitystats.EntityStatMap;
import com.hypixel.hytale.server.core.modules.entitystats.asset.DefaultEntityStatTypes;
import com.hypixel.hytale.server.core.modules.entitystats.modifier.Modifier;
import com.hypixel.hytale.server.core.modules.entitystats.modifier.StaticModifier;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import java.util.UUID;
/**
* Applies and persists character stat bonuses:
* <ul>
* <li>Force — +2 flat physical damage per point</li>
* <li>Vitalité — +10 max health per point</li>
* <li>Dextérité — +1% attack speed per point (weapon swing rate + shorter ability cooldowns)</li>
* <li>Vitesse — +2 movement speed per point</li>
* </ul>
* Players receive {@link #POINTS_PER_LEVEL} unspent points each time they level up.
*/
public final class CharacterStatService {
public static final int POINTS_PER_LEVEL = 10;
private static final float FORCE_ATTACK_PER_POINT = 2f;
private static final float VITALITY_HEALTH_PER_POINT = 10f;
private static final double DEXTERITY_ATTACK_SPEED_PER_POINT = 0.01;
private static final float SPEED_MOVEMENT_PER_POINT = 2f;
private static final String HEALTH_MODIFIER_KEY = "mmorpg_vitality";
private final PlayerSessionService sessions;
public CharacterStatService(@Nonnull PlayerSessionService sessions) {
this.sessions = sessions;
}
public boolean allocatePoint(
@Nonnull UUID accountUuid,
@Nonnull PlayerProfile profile,
@Nonnull CharacterStat stat,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref) {
if (profile.getStatPoints() <= 0) {
return false;
}
profile.setStatPoints(profile.getStatPoints() - 1);
profile.setStat(stat, profile.getStat(stat) + 1);
profile.touch();
sessions.saveActiveCharacter(accountUuid);
applyToEntity(store, ref, profile);
return true;
}
public void applyToEntity(
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerProfile profile) {
applyHealthBonus(store, ref, profile);
applyMovementSpeed(store, ref, profile);
}
public float attackBonus(@Nonnull PlayerProfile profile) {
return profile.getStat(CharacterStat.FORCE) * FORCE_ATTACK_PER_POINT;
}
public double attackSpeedFactor(@Nonnull PlayerProfile profile) {
return 1.0 + profile.getStat(CharacterStat.DEXTERITY) * DEXTERITY_ATTACK_SPEED_PER_POINT;
}
/**
* Extra interaction time shift applied each tick before the vanilla interaction manager runs.
* {@code 1} dexterity point = {@value #DEXTERITY_ATTACK_SPEED_PER_POINT} (1%) faster weapon swings.
*/
public float attackSpeedTimeShift(@Nonnull PlayerProfile profile) {
return (float) (profile.getStat(CharacterStat.DEXTERITY) * DEXTERITY_ATTACK_SPEED_PER_POINT);
}
public long cooldownMillis(@Nonnull PlayerProfile profile, long baseCooldownMillis) {
if (baseCooldownMillis <= 0L) {
return baseCooldownMillis;
}
double factor = attackSpeedFactor(profile);
return Math.max(1L, Math.round(baseCooldownMillis / factor));
}
public float movementSpeedBonus(@Nonnull PlayerProfile profile) {
return profile.getStat(CharacterStat.SPEED) * SPEED_MOVEMENT_PER_POINT;
}
@Nonnull
public static String formatStatLine(@Nonnull PlayerProfile profile, @Nonnull CharacterStat stat) {
int value = profile.getStat(stat);
return switch (stat) {
case FORCE -> stat.displayName() + " " + value + " (+" + Math.round(value * FORCE_ATTACK_PER_POINT) + " attaque)";
case VITALITY -> stat.displayName() + " " + value + " (+" + Math.round(value * VITALITY_HEALTH_PER_POINT) + " vie)";
case DEXTERITY -> stat.displayName() + " " + value + " (+" + value + "% vitesse d'attaque)";
case SPEED -> stat.displayName() + " " + value + " (+" + Math.round(value * SPEED_MOVEMENT_PER_POINT) + " déplacement)";
};
}
@Nonnull
public static String formatBonusLine(@Nonnull CharacterStat stat) {
return switch (stat) {
case FORCE -> "+" + (int) FORCE_ATTACK_PER_POINT + " attaque par point";
case VITALITY -> "+" + (int) VITALITY_HEALTH_PER_POINT + " vie par point";
case DEXTERITY -> "+1% vitesse d'attaque par point";
case SPEED -> "+" + (int) SPEED_MOVEMENT_PER_POINT + " déplacement par point";
};
}
private void applyHealthBonus(
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerProfile profile) {
EntityStatMap statMap = store.getComponent(ref, EntityStatMap.getComponentType());
if (statMap == null) {
return;
}
float bonus = profile.getStat(CharacterStat.VITALITY) * VITALITY_HEALTH_PER_POINT;
statMap.putModifier(
DefaultEntityStatTypes.getHealth(),
HEALTH_MODIFIER_KEY,
new StaticModifier(
Modifier.ModifierTarget.MAX,
StaticModifier.CalculationType.ADDITIVE,
bonus));
statMap.update();
}
private void applyMovementSpeed(
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerProfile profile) {
MovementManager movement = store.getComponent(ref, MovementManager.getComponentType());
if (movement == null) {
return;
}
MovementSettings defaults = movement.getDefaultSettings();
MovementSettings current = movement.getSettings();
if (defaults == null || current == null) {
return;
}
float target = defaults.baseSpeed + movementSpeedBonus(profile);
if (Math.abs(current.baseSpeed - target) > 0.0001f) {
current.baseSpeed = target;
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
if (playerRef != null) {
movement.update(playerRef.getPacketHandler());
}
}
}
}
@@ -32,6 +32,8 @@ import java.util.logging.Level;
*/ */
public final class AbilityBarService implements Service { public final class AbilityBarService implements Service {
private static final boolean CUSTOM_HUD_ENABLED = true;
private final HytaleLogger logger; private final HytaleLogger logger;
private final ClassService classService; private final ClassService classService;
private final AbilityService abilityService; private final AbilityService abilityService;
@@ -62,8 +64,10 @@ public final class AbilityBarService implements Service {
@Override @Override
public void start() { public void start() {
if (CUSTOM_HUD_ENABLED) {
refreshTask = scheduler.runRepeating(this::refreshAll, 5_000L, 1_000L); refreshTask = scheduler.runRepeating(this::refreshAll, 5_000L, 1_000L);
} }
}
@Override @Override
public void shutdown() { public void shutdown() {
@@ -75,6 +79,9 @@ public final class AbilityBarService implements Service {
} }
public void sync(@Nonnull UUID playerUuid, @Nonnull PlayerProfile profile) { public void sync(@Nonnull UUID playerUuid, @Nonnull PlayerProfile profile) {
if (!CUSTOM_HUD_ENABLED) {
return;
}
OnlinePlayer online = onlinePlayers.get(playerUuid); OnlinePlayer online = onlinePlayers.get(playerUuid);
if (online == null) { if (online == null) {
return; return;
@@ -83,6 +90,9 @@ public final class AbilityBarService implements Service {
} }
public void sync(@Nonnull OnlinePlayer online, @Nonnull PlayerProfile profile) { public void sync(@Nonnull OnlinePlayer online, @Nonnull PlayerProfile profile) {
if (!CUSTOM_HUD_ENABLED) {
return;
}
PlayerClass playerClass = classService.getClass(profile); PlayerClass playerClass = classService.getClass(profile);
if (playerClass == null) { if (playerClass == null) {
dismiss(online.uuid()); dismiss(online.uuid());
@@ -93,6 +103,10 @@ public final class AbilityBarService implements Service {
} }
public void dismiss(@Nonnull UUID playerUuid) { public void dismiss(@Nonnull UUID playerUuid) {
if (!CUSTOM_HUD_ENABLED) {
activeHuds.remove(playerUuid);
return;
}
OnlinePlayer online = onlinePlayers.get(playerUuid); OnlinePlayer online = onlinePlayers.get(playerUuid);
if (online == null) { if (online == null) {
activeHuds.remove(playerUuid); activeHuds.remove(playerUuid);
@@ -102,6 +116,10 @@ public final class AbilityBarService implements Service {
} }
public void dismiss(@Nonnull OnlinePlayer online) { public void dismiss(@Nonnull OnlinePlayer online) {
if (!CUSTOM_HUD_ENABLED) {
activeHuds.remove(online.uuid());
return;
}
AbilityBarHud hud = activeHuds.remove(online.uuid()); AbilityBarHud hud = activeHuds.remove(online.uuid());
if (hud == null) { if (hud == null) {
return; return;
@@ -12,6 +12,8 @@ import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
import com.hypixel.hytale.protocol.packets.interface_.Page;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage;
import com.hypixel.hytale.server.core.ui.builder.EventData; import com.hypixel.hytale.server.core.ui.builder.EventData;
import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
@@ -29,6 +31,7 @@ import java.util.logging.Logger;
/** /**
* Mandatory character selector shown on join before gameplay begins. * Mandatory character selector shown on join before gameplay begins.
* Uses the plugin asset pack document {@code Pages/MmorpgCharacterSelect.ui}.
*/ */
public final class CharacterSelectPage extends InteractiveCustomUIPage<CharacterSelectPage.UiEvent> { public final class CharacterSelectPage extends InteractiveCustomUIPage<CharacterSelectPage.UiEvent> {
@@ -142,6 +145,10 @@ public final class CharacterSelectPage extends InteractiveCustomUIPage<Character
@Nonnull UUID characterId) { @Nonnull UUID characterId) {
try { try {
characters.activateCharacter(playerRef, ref, store, accountUuid, characterId); characters.activateCharacter(playerRef, ref, store, accountUuid, characterId);
Player player = store.getComponent(ref, Player.getComponentType());
if (player != null) {
player.getPageManager().setPage(ref, store, Page.None);
}
} catch (SQLException e) { } catch (SQLException e) {
LOGGER.log(Level.WARNING, "Character selection failed", e); LOGGER.log(Level.WARNING, "Character selection failed", e);
setStatus(ref, store, "Impossible de charger ce personnage."); setStatus(ref, store, "Impossible de charger ce personnage.");
@@ -0,0 +1,183 @@
package com.disklexar.mmorpg.ui;
import com.disklexar.mmorpg.MmorpgPlugin;
import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.rpg.clazz.ClassCatalog;
import com.disklexar.mmorpg.rpg.clazz.ClassSelectionSupport;
import com.disklexar.mmorpg.rpg.clazz.PlayerClass;
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
import com.hypixel.hytale.protocol.packets.interface_.Page;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage;
import com.hypixel.hytale.server.core.ui.builder.EventData;
import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
import java.util.function.Consumer;
/**
* Class picker — mandatory on first character play, optional via command.
*/
public final class ClassSelectPage extends InteractiveCustomUIPage<ClassSelectPage.UiEvent> {
private static final String DOCUMENT = "Pages/MmorpgClassSelect.ui";
private static final int CARD_COUNT = 4;
private final MmorpgPlugin plugin;
private final PlayerRef playerRef;
private final PlayerProfile profile;
private final boolean welcomeAfterPick;
@Nullable
private final Consumer<PlayerProfile> onChosen;
public ClassSelectPage(
@Nonnull MmorpgPlugin plugin,
@Nonnull PlayerRef playerRef,
@Nonnull PlayerProfile profile,
boolean mandatory,
boolean welcomeAfterPick,
@Nullable Consumer<PlayerProfile> onChosen) {
super(playerRef, mandatory ? CustomPageLifetime.CantClose : CustomPageLifetime.CanDismiss, UiEvent.CODEC);
this.plugin = plugin;
this.playerRef = playerRef;
this.profile = profile;
this.welcomeAfterPick = welcomeAfterPick;
this.onChosen = onChosen;
}
@Override
public void build(
@Nonnull Ref<EntityStore> ref,
@Nonnull UICommandBuilder ui,
@Nonnull UIEventBuilder events,
@Nonnull Store<EntityStore> store) {
ui.append(DOCUMENT);
ui.set("#Title.Text", escape("Choisissez votre classe"));
ui.set("#Subtitle.Text", escape("Personnage : " + profile.getDisplayName()));
renderClassCards(ui, events, footerHint());
}
@Override
public void handleDataEvent(
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull UiEvent data) {
super.handleDataEvent(ref, store, data);
if (data.action == null || !"select".equalsIgnoreCase(data.action) || data.classId == null) {
return;
}
Player player = store.getComponent(ref, Player.getComponentType());
if (player == null) {
return;
}
player.getWorld().execute(() -> applyClassChoice(ref, store, data.classId));
}
private void applyClassChoice(
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull String classId) {
PlayerClass chosen = ClassSelectionSupport.apply(
plugin, store, ref, playerRef, profile, classId);
if (chosen == null) {
setStatus("Classe inconnue.");
return;
}
Player player = store.getComponent(ref, Player.getComponentType());
if (player != null) {
player.getPageManager().setPage(ref, store, Page.None);
}
playerRef.sendMessage(Message.raw("Classe choisie : " + chosen.displayName()));
if (onChosen != null) {
onChosen.accept(profile);
} else if (welcomeAfterPick) {
playerRef.sendMessage(Message.raw(
"Votre arme de classe a été équipée. Bon jeu !"));
}
}
private void setStatus(@Nonnull String message) {
UICommandBuilder ui = new UICommandBuilder();
UIEventBuilder events = new UIEventBuilder();
renderClassCards(ui, events, message);
sendUpdate(ui, events, false);
}
@Nonnull
private String footerHint() {
return profile.getClassId() == null
? "Cliquez sur Choisir pour valider une classe"
: "Cliquez sur Choisir pour changer de classe";
}
private void renderClassCards(
@Nonnull UICommandBuilder ui,
@Nonnull UIEventBuilder events,
@Nonnull String footer) {
for (int index = 0; index < CARD_COUNT; index++) {
ui.set("#ClassCard" + index + ".Visible", false);
}
List<PlayerClass> classes = ClassCatalog.all();
for (int index = 0; index < Math.min(classes.size(), CARD_COUNT); index++) {
PlayerClass playerClass = classes.get(index);
ui.set("#ClassCard" + index + ".Visible", true);
ui.set("#ClassName" + index + ".Text", escape(playerClass.displayName()));
ui.set(
"#ClassDesc" + index + ".Text",
escape(playerClass.description() + " — Armes : " + String.join(", ", playerClass.weapons())));
ui.set("#ClassPick" + index + ".Text", escape("Choisir " + playerClass.displayName()));
EventData selectEvent = EventData.of("Action", "select")
.append("ClassId", playerClass.id());
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#ClassPick" + index,
selectEvent,
false);
}
ui.set("#FooterHint.Text", escape(footer));
}
@Nonnull
private static String escape(@Nonnull String text) {
return text.replace("\\", "\\\\").replace("\"", "\\\"");
}
public static final class UiEvent {
public static final BuilderCodec<UiEvent> CODEC = BuilderCodec.builder(UiEvent.class, UiEvent::new)
.append(new KeyedCodec<>("Action", Codec.STRING),
(UiEvent event, String value) -> event.action = value,
event -> event.action)
.add()
.append(new KeyedCodec<>("ClassId", Codec.STRING),
(UiEvent event, String value) -> event.classId = value,
event -> event.classId)
.add()
.build();
private String action;
private String classId;
public UiEvent() {
}
}
}
@@ -3,6 +3,7 @@ package com.disklexar.mmorpg.ui;
import com.disklexar.mmorpg.combat.AbilityService; import com.disklexar.mmorpg.combat.AbilityService;
import com.disklexar.mmorpg.player.model.PlayerProfile; import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.progression.ProgressionService; import com.disklexar.mmorpg.progression.ProgressionService;
import com.disklexar.mmorpg.rpg.stats.CharacterStatService;
import com.disklexar.mmorpg.rpg.clazz.ClassCatalog; import com.disklexar.mmorpg.rpg.clazz.ClassCatalog;
import com.disklexar.mmorpg.rpg.clazz.PlayerClass; import com.disklexar.mmorpg.rpg.clazz.PlayerClass;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
@@ -36,6 +37,8 @@ public final class PlayerInventoryPage extends InteractiveCustomUIPage<PlayerInv
private final PlayerProfile profile; private final PlayerProfile profile;
private final ProgressionService progression; private final ProgressionService progression;
private final AbilityService abilities; private final AbilityService abilities;
@Nullable
private final CharacterStatService characterStats;
@Nullable @Nullable
private PlayerInventorySlotView.SlotRef pendingPick; private PlayerInventorySlotView.SlotRef pendingPick;
@@ -45,11 +48,21 @@ public final class PlayerInventoryPage extends InteractiveCustomUIPage<PlayerInv
@Nonnull PlayerProfile profile, @Nonnull PlayerProfile profile,
@Nullable ProgressionService progression, @Nullable ProgressionService progression,
@Nullable AbilityService abilities) { @Nullable AbilityService abilities) {
this(playerRef, profile, progression, abilities, null);
}
public PlayerInventoryPage(
@Nonnull PlayerRef playerRef,
@Nonnull PlayerProfile profile,
@Nullable ProgressionService progression,
@Nullable AbilityService abilities,
@Nullable CharacterStatService characterStats) {
super(playerRef, CustomPageLifetime.CanDismiss, UiEvent.CODEC); super(playerRef, CustomPageLifetime.CanDismiss, UiEvent.CODEC);
this.playerRef = playerRef; this.playerRef = playerRef;
this.profile = profile; this.profile = profile;
this.progression = progression; this.progression = progression;
this.abilities = abilities; this.abilities = abilities;
this.characterStats = characterStats;
} }
@Override @Override
@@ -83,6 +96,7 @@ public final class PlayerInventoryPage extends InteractiveCustomUIPage<PlayerInv
profile, profile,
progression, progression,
abilities, abilities,
characterStats,
tab)); tab));
} }
return; return;
@@ -166,9 +180,11 @@ public final class PlayerInventoryPage extends InteractiveCustomUIPage<PlayerInv
private void applyTabHighlight(@Nonnull UICommandBuilder ui) { private void applyTabHighlight(@Nonnull UICommandBuilder ui) {
ui.set("#TabCharacterIndicator.Visible", false); ui.set("#TabCharacterIndicator.Visible", false);
ui.set("#TabInventoryIndicator.Visible", true); ui.set("#TabInventoryIndicator.Visible", true);
ui.set("#TabStatsIndicator.Visible", false);
ui.set("#TabSkillsIndicator.Visible", false); ui.set("#TabSkillsIndicator.Visible", false);
ui.set("#TabCharacterWrap.Background", "#00000000"); ui.set("#TabCharacterWrap.Background", "#00000000");
ui.set("#TabInventoryWrap.Background", "#3a3f4a"); ui.set("#TabInventoryWrap.Background", "#3a3f4a");
ui.set("#TabStatsWrap.Background", "#00000000");
ui.set("#TabSkillsWrap.Background", "#00000000"); ui.set("#TabSkillsWrap.Background", "#00000000");
} }
@@ -228,6 +244,11 @@ public final class PlayerInventoryPage extends InteractiveCustomUIPage<PlayerInv
"#TabInventory", "#TabInventory",
EventData.of("Tab", "inventory"), EventData.of("Tab", "inventory"),
false); false);
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#TabStats",
EventData.of("Tab", "stats"),
false);
events.addEventBinding( events.addEventBinding(
CustomUIEventBindingType.Activating, CustomUIEventBindingType.Activating,
"#TabSkills", "#TabSkills",
@@ -252,6 +273,7 @@ public final class PlayerInventoryPage extends InteractiveCustomUIPage<PlayerInv
return switch (value) { return switch (value) {
case "character", "personnage" -> PlayerMenuPage.Tab.CHARACTER; case "character", "personnage" -> PlayerMenuPage.Tab.CHARACTER;
case "inventory", "inventaire" -> PlayerMenuPage.Tab.INVENTORY; case "inventory", "inventaire" -> PlayerMenuPage.Tab.INVENTORY;
case "stats", "statistiques", "statistics" -> PlayerMenuPage.Tab.STATS;
case "skills", "competences", "compétences" -> PlayerMenuPage.Tab.SKILLS; case "skills", "competences", "compétences" -> PlayerMenuPage.Tab.SKILLS;
default -> null; default -> null;
}; };
@@ -11,6 +11,8 @@ import com.disklexar.mmorpg.rpg.job.JobDefinition;
import com.disklexar.mmorpg.rpg.power.PowerCatalog; import com.disklexar.mmorpg.rpg.power.PowerCatalog;
import com.disklexar.mmorpg.rpg.power.PowerDefinition; import com.disklexar.mmorpg.rpg.power.PowerDefinition;
import com.disklexar.mmorpg.rpg.race.RaceCatalog; import com.disklexar.mmorpg.rpg.race.RaceCatalog;
import com.disklexar.mmorpg.rpg.stats.CharacterStat;
import com.disklexar.mmorpg.rpg.stats.CharacterStatService;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import com.hypixel.hytale.codec.builder.BuilderCodec; import com.hypixel.hytale.codec.builder.BuilderCodec;
@@ -36,15 +38,16 @@ import java.util.StringJoiner;
import java.util.UUID; import java.util.UUID;
/** /**
* MMORPG player menu with tabs: Personnage, Inventaire, Compétences. * MMORPG player menu with tabs: Personnage, Inventaire, Statistiques, Compétences.
* <p> * <p>
* Inventaire uses {@code MmorpgInventoryPage.ui} as the root document with {@code ItemSlot} icons. * Inventaire uses {@code MmorpgInventoryPage.ui} as the root document with {@code ItemSlot} icons.
* Personnage / Compétences use {@code MmorpgPlayerShell.ui}. * Personnage / Statistiques / Compétences use {@code MmorpgPlayerShell.ui}.
*/ */
public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage.UiEvent> { public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage.UiEvent> {
private static final String SHELL = "Pages/MmorpgPlayerShell.ui"; private static final String SHELL = "Pages/MmorpgPlayerShell.ui";
private static final String CHARACTER_TAB = "Pages/MmorpgCharacterTab.ui"; private static final String CHARACTER_TAB = "Pages/MmorpgCharacterTab.ui";
private static final String STATS_TAB = "Pages/MmorpgStatsTab.ui";
private static final String SKILLS_TAB = "Pages/MmorpgSkillsTab.ui"; private static final String SKILLS_TAB = "Pages/MmorpgSkillsTab.ui";
private static final DateTimeFormatter DATE_FORMAT = private static final DateTimeFormatter DATE_FORMAT =
@@ -53,6 +56,7 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
public enum Tab { public enum Tab {
CHARACTER, CHARACTER,
INVENTORY, INVENTORY,
STATS,
SKILLS SKILLS
} }
@@ -60,6 +64,8 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
private final PlayerProfile profile; private final PlayerProfile profile;
private final ProgressionService progression; private final ProgressionService progression;
private final AbilityService abilities; private final AbilityService abilities;
@Nullable
private final CharacterStatService characterStats;
private Tab activeTab; private Tab activeTab;
public PlayerMenuPage( public PlayerMenuPage(
@@ -68,11 +74,22 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
@Nullable ProgressionService progression, @Nullable ProgressionService progression,
@Nullable AbilityService abilities, @Nullable AbilityService abilities,
@Nonnull Tab initialTab) { @Nonnull Tab initialTab) {
this(playerRef, profile, progression, abilities, null, initialTab);
}
public PlayerMenuPage(
@Nonnull PlayerRef playerRef,
@Nonnull PlayerProfile profile,
@Nullable ProgressionService progression,
@Nullable AbilityService abilities,
@Nullable CharacterStatService characterStats,
@Nonnull Tab initialTab) {
super(playerRef, CustomPageLifetime.CanDismiss, UiEvent.CODEC); super(playerRef, CustomPageLifetime.CanDismiss, UiEvent.CODEC);
this.playerRef = playerRef; this.playerRef = playerRef;
this.profile = profile; this.profile = profile;
this.progression = progression; this.progression = progression;
this.abilities = abilities; this.abilities = abilities;
this.characterStats = characterStats;
this.activeTab = initialTab; this.activeTab = initialTab;
} }
@@ -90,6 +107,18 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
@Nonnull Ref<EntityStore> ref, @Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store, @Nonnull Store<EntityStore> store,
@Nonnull String raw) { @Nonnull String raw) {
CharacterStat stat = parseStat(raw);
if (stat != null && characterStats != null) {
if (characterStats.allocatePoint(profile.getUuid(), profile, stat, store, ref)) {
UICommandBuilder ui = new UICommandBuilder();
UIEventBuilder events = new UIEventBuilder();
bindTabButtons(events);
loadShellTab(ui, events, store, ref, activeTab);
sendUpdate(ui, events, false);
}
return;
}
Tab tab = parseTab(raw); Tab tab = parseTab(raw);
if (tab != null && tab != activeTab) { if (tab != null && tab != activeTab) {
if (tab == Tab.INVENTORY) { if (tab == Tab.INVENTORY) {
@@ -138,9 +167,14 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
switch (tab) { switch (tab) {
case CHARACTER -> { case CHARACTER -> {
ui.append("#DynamicTabHost", CHARACTER_TAB); ui.append("#DynamicTabHost", CHARACTER_TAB);
applyCharacterTab(ui, store, ref); applyCharacterTab(ui, events, store, ref);
ui.set("#FooterHint.Text", escape("Échap pour fermer — onglet Personnage")); ui.set("#FooterHint.Text", escape("Échap pour fermer — onglet Personnage"));
} }
case STATS -> {
ui.append("#DynamicTabHost", STATS_TAB);
applyStatsTab(ui, events);
ui.set("#FooterHint.Text", escape("Échap pour fermer — onglet Statistiques"));
}
case SKILLS -> { case SKILLS -> {
ui.append("#DynamicTabHost", SKILLS_TAB); ui.append("#DynamicTabHost", SKILLS_TAB);
applySkillsTab(ui); applySkillsTab(ui);
@@ -155,9 +189,11 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
private void applyTabHighlight(@Nonnull UICommandBuilder ui, @Nonnull Tab tab) { private void applyTabHighlight(@Nonnull UICommandBuilder ui, @Nonnull Tab tab) {
ui.set("#TabCharacterIndicator.Visible", tab == Tab.CHARACTER); ui.set("#TabCharacterIndicator.Visible", tab == Tab.CHARACTER);
ui.set("#TabInventoryIndicator.Visible", tab == Tab.INVENTORY); ui.set("#TabInventoryIndicator.Visible", tab == Tab.INVENTORY);
ui.set("#TabStatsIndicator.Visible", tab == Tab.STATS);
ui.set("#TabSkillsIndicator.Visible", tab == Tab.SKILLS); ui.set("#TabSkillsIndicator.Visible", tab == Tab.SKILLS);
ui.set("#TabCharacterWrap.Background", tab == Tab.CHARACTER ? "#3a3f4a" : "#00000000"); ui.set("#TabCharacterWrap.Background", tab == Tab.CHARACTER ? "#3a3f4a" : "#00000000");
ui.set("#TabInventoryWrap.Background", tab == Tab.INVENTORY ? "#3a3f4a" : "#00000000"); ui.set("#TabInventoryWrap.Background", tab == Tab.INVENTORY ? "#3a3f4a" : "#00000000");
ui.set("#TabStatsWrap.Background", tab == Tab.STATS ? "#3a3f4a" : "#00000000");
ui.set("#TabSkillsWrap.Background", tab == Tab.SKILLS ? "#3a3f4a" : "#00000000"); ui.set("#TabSkillsWrap.Background", tab == Tab.SKILLS ? "#3a3f4a" : "#00000000");
} }
@@ -169,6 +205,7 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
private void applyCharacterTab( private void applyCharacterTab(
@Nonnull UICommandBuilder ui, @Nonnull UICommandBuilder ui,
@Nonnull UIEventBuilder events,
@Nonnull Store<EntityStore> store, @Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref) { @Nonnull Ref<EntityStore> ref) {
PlayerClass playerClass = ClassCatalog.byId(profile.getClassId()); PlayerClass playerClass = ClassCatalog.byId(profile.getClassId());
@@ -194,11 +231,6 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
ui.set("#StaminaValue.Text", escape("Endurance : —")); ui.set("#StaminaValue.Text", escape("Endurance : —"));
} }
int level = profile.getLevel();
ui.set("#StatStr.Text", escape("FOR " + (10 + level)));
ui.set("#StatAgi.Text", escape("AGI " + (8 + level)));
ui.set("#StatInt.Text", escape("INT " + (6 + level)));
ui.set("#PowersList.Text", escape(formatPowers())); ui.set("#PowersList.Text", escape(formatPowers()));
ui.set("#JobsList.Text", escape(formatJobs())); ui.set("#JobsList.Text", escape(formatJobs()));
ui.set("#GroupLabel.Text", escape( ui.set("#GroupLabel.Text", escape(
@@ -211,6 +243,62 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
ui.set("#CreatedLabel.Text", escape("Création : " + formatDate(profile.getDateCreation()))); ui.set("#CreatedLabel.Text", escape("Création : " + formatDate(profile.getDateCreation())));
} }
private void applyStatsTab(@Nonnull UICommandBuilder ui, @Nonnull UIEventBuilder events) {
ui.set("#StatPointsLabel.Text", escape(
"Points à attribuer : " + profile.getStatPoints() + " (+"
+ CharacterStatService.POINTS_PER_LEVEL + " par niveau)"));
for (CharacterStat stat : CharacterStat.values()) {
ui.set(statBonusSelector(stat) + ".Text", escape(
stat.displayName() + " : " + CharacterStatService.formatBonusLine(stat)));
}
applyStatAllocation(ui, events);
}
private void applyStatAllocation(@Nonnull UICommandBuilder ui, @Nonnull UIEventBuilder events) {
boolean canAllocate = profile.getStatPoints() > 0 && characterStats != null;
for (CharacterStat stat : CharacterStat.values()) {
ui.set(statLabelSelector(stat) + ".Text", escape(CharacterStatService.formatStatLine(profile, stat)));
ui.set(statAddSelector(stat) + ".Visible", canAllocate);
if (canAllocate) {
events.addEventBinding(
CustomUIEventBindingType.Activating,
statAddSelector(stat),
EventData.of("Stat", stat.id()),
false);
}
}
}
@Nonnull
private static String statBonusSelector(@Nonnull CharacterStat stat) {
return switch (stat) {
case FORCE -> "#StatForceBonus";
case VITALITY -> "#StatVitalityBonus";
case DEXTERITY -> "#StatDexterityBonus";
case SPEED -> "#StatSpeedBonus";
};
}
@Nonnull
private static String statLabelSelector(@Nonnull CharacterStat stat) {
return switch (stat) {
case FORCE -> "#StatForceLabel";
case VITALITY -> "#StatVitalityLabel";
case DEXTERITY -> "#StatDexterityLabel";
case SPEED -> "#StatSpeedLabel";
};
}
@Nonnull
private static String statAddSelector(@Nonnull CharacterStat stat) {
return switch (stat) {
case FORCE -> "#StatForceAdd";
case VITALITY -> "#StatVitalityAdd";
case DEXTERITY -> "#StatDexterityAdd";
case SPEED -> "#StatSpeedAdd";
};
}
private void applySkillsTab(@Nonnull UICommandBuilder ui) { private void applySkillsTab(@Nonnull UICommandBuilder ui) {
PlayerClass playerClass = ClassCatalog.byId(profile.getClassId()); PlayerClass playerClass = ClassCatalog.byId(profile.getClassId());
if (playerClass == null) { if (playerClass == null) {
@@ -253,6 +341,11 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
"#TabInventory", "#TabInventory",
EventData.of("Tab", "inventory"), EventData.of("Tab", "inventory"),
false); false);
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#TabStats",
EventData.of("Tab", "stats"),
false);
events.addEventBinding( events.addEventBinding(
CustomUIEventBindingType.Activating, CustomUIEventBindingType.Activating,
"#TabSkills", "#TabSkills",
@@ -315,6 +408,7 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
return switch (tab) { return switch (tab) {
case CHARACTER -> "Personnage"; case CHARACTER -> "Personnage";
case INVENTORY -> "Inventaire"; case INVENTORY -> "Inventaire";
case STATS -> "Statistiques";
case SKILLS -> "Compétences"; case SKILLS -> "Compétences";
}; };
} }
@@ -323,6 +417,9 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
private static Tab parseTab(@Nonnull String raw) { private static Tab parseTab(@Nonnull String raw) {
try { try {
JsonObject json = JsonParser.parseString(raw).getAsJsonObject(); JsonObject json = JsonParser.parseString(raw).getAsJsonObject();
if (json.has("Stat")) {
return null;
}
if (!json.has("Tab")) { if (!json.has("Tab")) {
return null; return null;
} }
@@ -330,6 +427,7 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
return switch (value) { return switch (value) {
case "character", "personnage" -> Tab.CHARACTER; case "character", "personnage" -> Tab.CHARACTER;
case "inventory", "inventaire" -> Tab.INVENTORY; case "inventory", "inventaire" -> Tab.INVENTORY;
case "stats", "statistiques", "statistics" -> Tab.STATS;
case "skills", "competences", "compétences" -> Tab.SKILLS; case "skills", "competences", "compétences" -> Tab.SKILLS;
default -> null; default -> null;
}; };
@@ -338,6 +436,19 @@ public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage
} }
} }
@Nullable
private static CharacterStat parseStat(@Nonnull String raw) {
try {
JsonObject json = JsonParser.parseString(raw).getAsJsonObject();
if (!json.has("Stat")) {
return null;
}
return CharacterStat.byId(json.get("Stat").getAsString());
} catch (RuntimeException ignored) {
return null;
}
}
@Nonnull @Nonnull
private static String formatDuration(long millis) { private static String formatDuration(long millis) {
if (millis <= 0) { if (millis <= 0) {
@@ -3,7 +3,6 @@
@MmorpgAccent = #f5c842; @MmorpgAccent = #f5c842;
@MmorpgText = #e8e8ec; @MmorpgText = #e8e8ec;
@MmorpgMuted = #878e9c; @MmorpgMuted = #878e9c;
@MmorpgDanger = #c9302c;
Group { Group {
LayoutMode: Center; LayoutMode: Center;
@@ -38,119 +37,30 @@ Group {
Visible: false; Visible: false;
} }
Group #CharRow0 { Group #CharRow0 { LayoutMode: Left; Padding: (Vertical: 6); Anchor: (Bottom: 4); Visible: false;
LayoutMode: Left; Label #CharName0 { Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true); Text: "Perso 1"; FlexWeight: 1; }
Padding: (Vertical: 6); TextButton #CharDelete0 { Text: "Suppr."; Anchor: (Width: 72, Right: 6); }
Anchor: (Bottom: 4); TextButton #CharSelect0 { Text: "Jouer"; Anchor: (Width: 88); }
Visible: false;
Label #CharName0 {
Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true);
Text: "Perso 1";
FlexWeight: 1;
} }
Group #CharRow1 { LayoutMode: Left; Padding: (Vertical: 6); Anchor: (Bottom: 4); Visible: false;
TextButton #CharDelete0 { Label #CharName1 { Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true); Text: "Perso 2"; FlexWeight: 1; }
Text: "Suppr."; TextButton #CharDelete1 { Text: "Suppr."; Anchor: (Width: 72, Right: 6); }
Anchor: (Width: 72, Right: 6); TextButton #CharSelect1 { Text: "Jouer"; Anchor: (Width: 88); }
} }
Group #CharRow2 { LayoutMode: Left; Padding: (Vertical: 6); Anchor: (Bottom: 4); Visible: false;
TextButton #CharSelect0 { Label #CharName2 { Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true); Text: "Perso 3"; FlexWeight: 1; }
Text: "Jouer"; TextButton #CharDelete2 { Text: "Suppr."; Anchor: (Width: 72, Right: 6); }
Anchor: (Width: 88); TextButton #CharSelect2 { Text: "Jouer"; Anchor: (Width: 88); }
} }
Group #CharRow3 { LayoutMode: Left; Padding: (Vertical: 6); Anchor: (Bottom: 4); Visible: false;
Label #CharName3 { Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true); Text: "Perso 4"; FlexWeight: 1; }
TextButton #CharDelete3 { Text: "Suppr."; Anchor: (Width: 72, Right: 6); }
TextButton #CharSelect3 { Text: "Jouer"; Anchor: (Width: 88); }
} }
Group #CharRow4 { LayoutMode: Left; Padding: (Vertical: 6); Anchor: (Bottom: 4); Visible: false;
Group #CharRow1 { Label #CharName4 { Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true); Text: "Perso 5"; FlexWeight: 1; }
LayoutMode: Left; TextButton #CharDelete4 { Text: "Suppr."; Anchor: (Width: 72, Right: 6); }
Padding: (Vertical: 6); TextButton #CharSelect4 { Text: "Jouer"; Anchor: (Width: 88); }
Anchor: (Bottom: 4);
Visible: false;
Label #CharName1 {
Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true);
Text: "Perso 2";
FlexWeight: 1;
}
TextButton #CharDelete1 {
Text: "Suppr.";
Anchor: (Width: 72, Right: 6);
}
TextButton #CharSelect1 {
Text: "Jouer";
Anchor: (Width: 88);
}
}
Group #CharRow2 {
LayoutMode: Left;
Padding: (Vertical: 6);
Anchor: (Bottom: 4);
Visible: false;
Label #CharName2 {
Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true);
Text: "Perso 3";
FlexWeight: 1;
}
TextButton #CharDelete2 {
Text: "Suppr.";
Anchor: (Width: 72, Right: 6);
}
TextButton #CharSelect2 {
Text: "Jouer";
Anchor: (Width: 88);
}
}
Group #CharRow3 {
LayoutMode: Left;
Padding: (Vertical: 6);
Anchor: (Bottom: 4);
Visible: false;
Label #CharName3 {
Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true);
Text: "Perso 4";
FlexWeight: 1;
}
TextButton #CharDelete3 {
Text: "Suppr.";
Anchor: (Width: 72, Right: 6);
}
TextButton #CharSelect3 {
Text: "Jouer";
Anchor: (Width: 88);
}
}
Group #CharRow4 {
LayoutMode: Left;
Padding: (Vertical: 6);
Anchor: (Bottom: 4);
Visible: false;
Label #CharName4 {
Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true);
Text: "Perso 5";
FlexWeight: 1;
}
TextButton #CharDelete4 {
Text: "Suppr.";
Anchor: (Width: 72, Right: 6);
}
TextButton #CharSelect4 {
Text: "Jouer";
Anchor: (Width: 88);
}
} }
} }
@@ -169,34 +79,16 @@ Group {
Group #ConfirmActions { Group #ConfirmActions {
LayoutMode: Left; LayoutMode: Left;
TextButton #ConfirmDeleteButton { Text: "Confirmer la suppression"; FlexWeight: 1; Anchor: (Right: 8); }
TextButton #ConfirmDeleteButton { TextButton #CancelDeleteButton { Text: "Annuler"; FlexWeight: 1; }
Text: "Confirmer la suppression";
FlexWeight: 1;
Anchor: (Right: 8);
}
TextButton #CancelDeleteButton {
Text: "Annuler";
FlexWeight: 1;
}
} }
} }
Group #Actions { Group #Actions {
LayoutMode: Left; LayoutMode: Left;
Anchor: (Top: 12); Anchor: (Top: 12);
TextButton #CreateCharacterButton { Text: "Nouveau personnage"; FlexWeight: 1; Anchor: (Right: 8); }
TextButton #CreateCharacterButton { TextButton #RefreshButton { Text: "Actualiser"; FlexWeight: 1; }
Text: "Nouveau personnage";
FlexWeight: 1;
Anchor: (Right: 8);
}
TextButton #RefreshButton {
Text: "Actualiser";
FlexWeight: 1;
}
} }
Label #FooterHint { Label #FooterHint {
@@ -10,6 +10,9 @@
@XpBg = PatchStyle(Color: #152030); @XpBg = PatchStyle(Color: #152030);
Group { Group {
LayoutMode: Top;
Group {
LayoutMode: Left; LayoutMode: Left;
Group #LeftColumn { Group #LeftColumn {
@@ -93,45 +96,6 @@ Group {
Value: 1.0; Value: 1.0;
Direction: End; Direction: End;
} }
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "ATTRIBUTS";
Anchor: (Bottom: 6);
}
Group {
LayoutMode: Left;
Anchor: (Bottom: 4);
Label #StatStr {
Style: (FontSize: 12, TextColor: @MmorpgText);
Text: "FOR 10";
Padding: (Horizontal: 10, Vertical: 4);
Background: #2a3140;
Anchor: (Right: 6);
}
Label #StatAgi {
Style: (FontSize: 12, TextColor: @MmorpgText);
Text: "AGI 10";
Padding: (Horizontal: 10, Vertical: 4);
Background: #2a3140;
Anchor: (Right: 6);
}
Label #StatInt {
Style: (FontSize: 12, TextColor: @MmorpgText);
Text: "INT 10";
Padding: (Horizontal: 10, Vertical: 4);
Background: #2a3140;
}
}
Label #StatsHint {
Style: (FontSize: 9, TextColor: @MmorpgMuted);
Text: "Attributs dérivés du niveau (système M1 à venir)";
}
} }
Group #RightColumn { Group #RightColumn {
@@ -182,7 +146,7 @@ Group {
Label { Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "STATISTIQUES"; Text: "ACTIVITÉ";
Anchor: (Bottom: 4); Anchor: (Bottom: 4);
} }
@@ -203,4 +167,5 @@ Group {
Text: "Création : —"; Text: "Création : —";
} }
} }
}
} }
@@ -0,0 +1,95 @@
@MmorpgBg = #0d0f14(0.98);
@MmorpgPanel = #1a1c20;
@MmorpgSlot = #2a3140;
@MmorpgAccent = #f5c842;
@MmorpgText = #e8e8ec;
@MmorpgMuted = #878e9c;
Group {
LayoutMode: Center;
Group #ClassSelectRoot {
LayoutMode: Top;
Anchor: (Width: 720, Height: 520);
Background: @MmorpgBg;
Padding: (Full: 20);
Label #Title {
Style: (FontSize: 22, RenderBold: true, TextColor: @MmorpgAccent, HorizontalAlignment: Center);
Text: "Choisissez votre classe";
Anchor: (Bottom: 6);
}
Label #Subtitle {
Style: (FontSize: 14, TextColor: @MmorpgText, HorizontalAlignment: Center);
Text: "Cliquez sur Choisir pour valider";
Anchor: (Bottom: 16);
}
Group #ClassGrid {
LayoutMode: Top;
FlexWeight: 1;
Background: @MmorpgPanel;
Padding: (Full: 14);
Group #ClassRow0 {
LayoutMode: Left;
Anchor: (Bottom: 12);
Group #ClassCard0 {
LayoutMode: Top;
Padding: (Full: 12);
Background: @MmorpgSlot;
Anchor: (Width: 320, Right: 12);
Visible: false;
Label #ClassName0 { Style: (FontSize: 16, RenderBold: true, TextColor: @MmorpgAccent, HorizontalAlignment: Center); Text: "Classe"; Anchor: (Bottom: 4); }
Label #ClassDesc0 { Style: (FontSize: 12, TextColor: @MmorpgMuted, Wrap: true, HorizontalAlignment: Center); Text: "Description"; Anchor: (Bottom: 8); }
TextButton #ClassPick0 { Text: "Choisir"; }
}
Group #ClassCard1 {
LayoutMode: Top;
Padding: (Full: 12);
Background: @MmorpgSlot;
Anchor: (Width: 320);
Visible: false;
Label #ClassName1 { Style: (FontSize: 16, RenderBold: true, TextColor: @MmorpgAccent, HorizontalAlignment: Center); Text: "Classe"; Anchor: (Bottom: 4); }
Label #ClassDesc1 { Style: (FontSize: 12, TextColor: @MmorpgMuted, Wrap: true, HorizontalAlignment: Center); Text: "Description"; Anchor: (Bottom: 8); }
TextButton #ClassPick1 { Text: "Choisir"; }
}
}
Group #ClassRow1 {
LayoutMode: Left;
Group #ClassCard2 {
LayoutMode: Top;
Padding: (Full: 12);
Background: @MmorpgSlot;
Anchor: (Width: 320, Right: 12);
Visible: false;
Label #ClassName2 { Style: (FontSize: 16, RenderBold: true, TextColor: @MmorpgAccent, HorizontalAlignment: Center); Text: "Classe"; Anchor: (Bottom: 4); }
Label #ClassDesc2 { Style: (FontSize: 12, TextColor: @MmorpgMuted, Wrap: true, HorizontalAlignment: Center); Text: "Description"; Anchor: (Bottom: 8); }
TextButton #ClassPick2 { Text: "Choisir"; }
}
Group #ClassCard3 {
LayoutMode: Top;
Padding: (Full: 12);
Background: @MmorpgSlot;
Anchor: (Width: 320);
Visible: false;
Label #ClassName3 { Style: (FontSize: 16, RenderBold: true, TextColor: @MmorpgAccent, HorizontalAlignment: Center); Text: "Classe"; Anchor: (Bottom: 4); }
Label #ClassDesc3 { Style: (FontSize: 12, TextColor: @MmorpgMuted, Wrap: true, HorizontalAlignment: Center); Text: "Description"; Anchor: (Bottom: 8); }
TextButton #ClassPick3 { Text: "Choisir"; }
}
}
}
Label #FooterHint {
Style: (FontSize: 11, TextColor: @MmorpgMuted, HorizontalAlignment: Center);
Text: "Vous devez choisir une classe pour jouer";
Anchor: (Top: 12);
}
}
}
@@ -102,6 +102,23 @@ Group {
} }
} }
Group #TabStatsWrap {
LayoutMode: Top;
Padding: (Horizontal: 12, Vertical: 4);
Anchor: (Right: 6);
Background: PatchStyle(Color: #00000000);
TextButton #TabStats {
Text: "Statistiques";
}
Group #TabStatsIndicator {
Anchor: (Height: 2, Top: 4);
Background: PatchStyle(Color: @MmorpgAccent);
Visible: false;
}
}
Group #TabSkillsWrap { Group #TabSkillsWrap {
LayoutMode: Top; LayoutMode: Top;
Padding: (Horizontal: 12, Vertical: 4); Padding: (Horizontal: 12, Vertical: 4);
@@ -92,6 +92,23 @@ Group {
} }
} }
Group #TabStatsWrap {
LayoutMode: Top;
Padding: (Horizontal: 12, Vertical: 4);
Anchor: (Right: 6);
Background: PatchStyle(Color: #00000000);
TextButton #TabStats {
Text: "Statistiques";
}
Group #TabStatsIndicator {
Anchor: (Height: 2, Top: 4);
Background: PatchStyle(Color: @MmorpgAccent);
Visible: false;
}
}
Group #TabSkillsWrap { Group #TabSkillsWrap {
LayoutMode: Top; LayoutMode: Top;
Padding: (Horizontal: 12, Vertical: 4); Padding: (Horizontal: 12, Vertical: 4);
@@ -0,0 +1,65 @@
@MmorpgAccent = #f5c842;
@MmorpgText = #e8e8ec;
@MmorpgMuted = #878e9c;
Group {
LayoutMode: Top;
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "ATTRIBUTS";
Anchor: (Bottom: 4);
}
Label #StatPointsLabel {
Style: (FontSize: 14, RenderBold: true, TextColor: @MmorpgAccent);
Text: "Points à attribuer : 0";
Anchor: (Bottom: 12);
}
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "RÉPARTITION";
Anchor: (Bottom: 8);
}
Group {
LayoutMode: Top;
Background: #141820;
Padding: (Full: 10);
Anchor: (Bottom: 12);
Group #StatForceRow { LayoutMode: Left; Anchor: (Bottom: 6);
Label #StatForceLabel { Style: (FontSize: 12, TextColor: @MmorpgText); Text: "Force 0"; FlexWeight: 1; }
TextButton #StatForceAdd { Text: "+"; Anchor: (Width: 32); Visible: false; }
}
Group #StatVitalityRow { LayoutMode: Left; Anchor: (Bottom: 6);
Label #StatVitalityLabel { Style: (FontSize: 12, TextColor: @MmorpgText); Text: "Vitalité 0"; FlexWeight: 1; }
TextButton #StatVitalityAdd { Text: "+"; Anchor: (Width: 32); Visible: false; }
}
Group #StatDexterityRow { LayoutMode: Left; Anchor: (Bottom: 6);
Label #StatDexterityLabel { Style: (FontSize: 12, TextColor: @MmorpgText); Text: "Dextérité 0"; FlexWeight: 1; }
TextButton #StatDexterityAdd { Text: "+"; Anchor: (Width: 32); Visible: false; }
}
Group #StatSpeedRow { LayoutMode: Left; Anchor: (Bottom: 2);
Label #StatSpeedLabel { Style: (FontSize: 12, TextColor: @MmorpgText); Text: "Vitesse 0"; FlexWeight: 1; }
TextButton #StatSpeedAdd { Text: "+"; Anchor: (Width: 32); Visible: false; }
}
}
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "EFFETS PAR POINT";
Anchor: (Bottom: 6);
}
Label #StatForceBonus { Style: (FontSize: 11, TextColor: @MmorpgText); Text: "Force : +2 attaque"; Anchor: (Bottom: 3); }
Label #StatVitalityBonus { Style: (FontSize: 11, TextColor: @MmorpgText); Text: "Vitalité : +10 vie"; Anchor: (Bottom: 3); }
Label #StatDexterityBonus { Style: (FontSize: 11, TextColor: @MmorpgText); Text: "Dextérité : +1% vitesse d'attaque"; Anchor: (Bottom: 3); }
Label #StatSpeedBonus { Style: (FontSize: 11, TextColor: @MmorpgText); Text: "Vitesse : +2 déplacement"; Anchor: (Bottom: 8); }
Label #StatsHint {
Style: (FontSize: 10, TextColor: @MmorpgMuted, Wrap: true);
Text: "Gagnez 10 points à chaque niveau. Cliquez + pour attribuer un point. Toutes les statistiques commencent à 0.";
}
}
@@ -0,0 +1,9 @@
{
"Type": "Projectile",
"RunTime": 0.2,
"Effects": {
"ItemAnimationId": "ShootCharged",
"ClearAnimationOnFinish": true
},
"Config": "Projectile_Config_Arrow_Shortbow_Strength_4"
}
@@ -0,0 +1,200 @@
{
"TranslationProperties": {
"Name": "server.items.Weapon_Arrow_Crude.name"
},
"Categories": [
"Items.Weapons"
],
"Quality": "Common",
"ItemLevel": 9,
"PlayerAnimationsId": "Dagger",
"Recipe": {
"TimeSeconds": 0,
"KnowledgeRequired": false,
"Input": [
{
"Quantity": 4,
"ItemId": "Ingredient_Stick"
},
{
"ResourceTypeId": "Rubble",
"Quantity": 1
}
],
"OutputQuantity": 4,
"BenchRequirement": [
{
"Type": "Crafting",
"Categories": [
"Workbench_Survival"
],
"Id": "Workbench"
},
{
"Type": "Crafting",
"Categories": [
"Weapon_Bow"
],
"Id": "Weapon_Bench"
}
]
},
"IconProperties": {
"Scale": 0.5,
"Rotation": [
45.0,
90.0,
0.0
],
"Translation": [
-25.0,
-25.0
]
},
"Model": "Items/Weapons/Arrow/Arrow.blockymodel",
"Texture": "Items/Weapons/Arrow/Crude_Texture.png",
"Icon": "Icons/ItemsGenerated/Weapon_Arrow_Crude.png",
"Interactions": {
"Primary": "Knife_Attack",
"Secondary": "Knife_Block"
},
"InteractionVars": {
"Knife_Block_Damage": "Knife_Block_Damage",
"Knife_Swing_Left_Effect": {
"Interactions": [
{
"Parent": "Knife_Swing_Left_Effect",
"Effects": {
"WorldSoundEventId": "SFX_Axe_Special_Swing"
}
}
]
},
"Knife_Swing_Left_Damage": {
"Interactions": [
{
"Parent": "Knife_Swing_Left_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 1
}
},
"DamageEffects": {
"Knockback": {
"Force": 0,
"VelocityType": "Add"
},
"WorldSoundEventId": "SFX_Axe_Special_Impact"
}
}
]
},
"Knife_Swing_Right_Effect": {
"Interactions": [
{
"Parent": "Knife_Swing_Right_Effect",
"Effects": {
"WorldSoundEventId": "SFX_Axe_Special_Swing"
}
}
]
},
"Knife_Swing_Right_Damage": {
"Interactions": [
{
"Parent": "Knife_Swing_Right_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 1
}
},
"DamageEffects": {
"Knockback": {
"Force": 0,
"VelocityType": "Add"
},
"WorldSoundEventId": "SFX_Axe_Special_Impact"
}
}
]
},
"Knife_Stab_Effect": {
"Interactions": [
{
"Parent": "Knife_Stab_Effect",
"Effects": {
"WorldSoundEventId": "SFX_Axe_Special_Swing"
}
}
]
},
"Knife_Stab_Damage": {
"Interactions": [
{
"Parent": "Knife_Stab_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 1
}
},
"DamageEffects": {
"Knockback": {
"Force": 0,
"VelocityType": "Add"
},
"WorldSoundEventId": "SFX_Axe_Special_Impact"
}
}
]
},
"Knife_Lunge_Effect": {
"Interactions": [
{
"Parent": "Knife_Lunge_Effect",
"Effects": {
"WorldSoundEventId": "SFX_Axe_Special_Swing"
}
}
]
},
"Knife_Lunge_Damage": {
"Interactions": [
{
"Parent": "Knife_Lunge_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 1
}
},
"DamageEffects": {
"Knockback": {
"Force": 0,
"VelocityType": "Add"
},
"WorldSoundEventId": "SFX_Axe_Special_Impact"
}
}
]
},
"Knife_Throw_Charged_Projectile": {
"Interactions": [
{
"Parent": "Knife_Throw_Charged_Projectile",
"ProjectileId": "Arrow_HalfCharge"
}
]
}
},
"MaxStack": 100,
"DroppedItemAnimation": "Items/Animations/Dropped/Dropped_Diagonal_Left.blockyanim",
"Tags": {
"Type": [
"Weapon"
],
"Family": [
"Arrow"
]
},
"Weapon": {},
"ItemSoundSetId": "ISS_Weapons_Arrows"
}
@@ -286,6 +286,14 @@
"MaxDurability": 150, "MaxDurability": 150,
"DurabilityLossOnHit": 0.58, "DurabilityLossOnHit": 0.58,
"Interactions": { "Interactions": {
"Primary": {
"RequireNewClick": true,
"Interactions": [
{
"Type": "mmorpg_archer_shoot"
}
]
},
"Ability1": { "Ability1": {
"Interactions": [ "Interactions": [
{ {
@@ -0,0 +1,10 @@
{
"Interactions": [
"Mmorpg_Archer_Projectile"
],
"Tags": {
"Attack": [
"Ranged"
]
}
}
@@ -0,0 +1,6 @@
-- Per-character death timestamp and play time.
-- death_time: NULL while alive, epoch millis when permanently dead.
-- total_time_play: milliseconds spent on this character only.
ALTER TABLE characters ADD COLUMN death_time INTEGER;
ALTER TABLE characters ADD COLUMN total_time_play INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,7 @@
-- Allocatable character stats (Force, Vitalité, Dextérité, Vitesse).
ALTER TABLE characters ADD COLUMN stat_points INTEGER NOT NULL DEFAULT 0;
ALTER TABLE characters ADD COLUMN stat_force INTEGER NOT NULL DEFAULT 0;
ALTER TABLE characters ADD COLUMN stat_vitality INTEGER NOT NULL DEFAULT 0;
ALTER TABLE characters ADD COLUMN stat_dexterity INTEGER NOT NULL DEFAULT 0;
ALTER TABLE characters ADD COLUMN stat_speed INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1 @@
-- Four-stat model finalized (Force, Vitalité, Dextérité, Vitesse). No legacy columns to remove.
@@ -1,7 +1,7 @@
{ {
"Group": "com.disklexar", "Group": "com.disklexar",
"Name": "MMORPG", "Name": "MMORPG",
"Version": "0.4.7", "Version": "0.4.8",
"Description": "Serveur MMORPG Hytale — progression, persistance et systèmes sociaux", "Description": "Serveur MMORPG Hytale — progression, persistance et systèmes sociaux",
"Authors": [ "Authors": [
{ {
Binary file not shown.
@@ -0,0 +1,198 @@
@HealthBarFill = PatchStyle(Color: #e74c3c);
@HealthBarBg = PatchStyle(Color: #2a1515);
@StaminaBarFill = PatchStyle(Color: #2ecc71);
@StaminaBarBg = PatchStyle(Color: #152a1c);
@XpBarFill = PatchStyle(Color: #4a9eff);
@XpBarBg = PatchStyle(Color: #152030);
@VitalsPanelWidth = 260;
@VitalsBarWidth = 244;
@VitalsBarHeight = 16;
@AbilitySlotSize = 68;
@AbilityPanelPad = 3;
@AbilityPanelWidth = @AbilitySlotSize + (@AbilityPanelPad * 2);
Group {
LayoutMode: Top;
Anchor: (Left: 16, Top: 16, Width: @VitalsPanelWidth);
Label #HeaderLabel {
Style: (FontSize: 14, RenderBold: true, TextColor: #f5c842);
Text: "Nv.1 — Classe";
Anchor: (Left: 0, Bottom: 8);
}
Group #VitalsPanel {
LayoutMode: Top;
Padding: (Full: 8);
Background: #0d0f14(0.92);
Anchor: (Left: 0, Width: @VitalsPanelWidth, Bottom: 8);
Group #HealthRow {
LayoutMode: Top;
Anchor: (Bottom: 6);
Label #HealthTitle {
Style: (FontSize: 10, RenderBold: true, TextColor: #ff7b7b);
Text: "Vie";
Anchor: (Bottom: 2);
}
Group #HealthBarTrack {
LayoutMode: Full;
Anchor: (Width: @VitalsBarWidth, Height: @VitalsBarHeight);
ProgressBar #HealthBar {
Anchor: (Width: @VitalsBarWidth, Height: @VitalsBarHeight);
Bar: @HealthBarFill;
Background: @HealthBarBg;
Value: 1.0;
Direction: End;
}
Group {
LayoutMode: CenterMiddle;
Anchor: (Width: @VitalsBarWidth, Height: @VitalsBarHeight);
Label #HealthValue {
Style: (FontSize: 9, RenderBold: true, TextColor: #ffffff);
Text: "0 / 0";
}
}
}
}
Group #StaminaRow {
LayoutMode: Top;
Anchor: (Bottom: 6);
Label #StaminaTitle {
Style: (FontSize: 10, RenderBold: true, TextColor: #7bdc9a);
Text: "Endurance";
Anchor: (Bottom: 2);
}
Group #StaminaBarTrack {
LayoutMode: Full;
Anchor: (Width: @VitalsBarWidth, Height: @VitalsBarHeight);
ProgressBar #StaminaBar {
Anchor: (Width: @VitalsBarWidth, Height: @VitalsBarHeight);
Bar: @StaminaBarFill;
Background: @StaminaBarBg;
Value: 1.0;
Direction: End;
}
Group {
LayoutMode: CenterMiddle;
Anchor: (Width: @VitalsBarWidth, Height: @VitalsBarHeight);
Label #StaminaValue {
Style: (FontSize: 9, RenderBold: true, TextColor: #ffffff);
Text: "0 / 0";
}
}
}
}
Group #XpRow {
LayoutMode: Top;
Label #XpTitle {
Style: (FontSize: 10, RenderBold: true, TextColor: #8eb6ff);
Text: "Expérience";
Anchor: (Bottom: 2);
}
Group #XpBarTrack {
LayoutMode: Full;
Anchor: (Width: @VitalsBarWidth, Height: @VitalsBarHeight);
ProgressBar #XpBar {
Anchor: (Width: @VitalsBarWidth, Height: @VitalsBarHeight);
Bar: @XpBarFill;
Background: @XpBarBg;
Value: 0.0;
Direction: End;
}
Group {
LayoutMode: CenterMiddle;
Anchor: (Width: @VitalsBarWidth, Height: @VitalsBarHeight);
Label #XpValue {
Style: (FontSize: 9, RenderBold: true, TextColor: #ffffff);
Text: "0 / 0";
}
}
}
}
}
Label #BarTitle {
Style: (FontSize: 12, RenderBold: true, TextColor: #f5c842);
Text: "CAPACITÉS";
Anchor: (Left: 0, Bottom: 4);
}
Group #AbilityBarPanel {
LayoutMode: Top;
Padding: (Full: @AbilityPanelPad);
Background: #0d0f14(0.92);
Anchor: (Left: 0, Width: @AbilityPanelWidth);
Group #AbilitySlot1 {
Anchor: (Width: @AbilitySlotSize, Height: @AbilitySlotSize, Bottom: 5);
Background: #2a3140;
LayoutMode: Top;
Padding: (Full: 4);
Label #Slot1Key {
Style: (FontSize: 12, RenderBold: true, TextColor: #f5c842);
Text: "[Q]";
Anchor: (Bottom: 2);
}
Label #Slot1Name {
Style: (FontSize: 10, TextColor: #ffffff, Wrap: true);
Text: "Capacité 1";
}
}
Group #AbilitySlot2 {
Anchor: (Width: @AbilitySlotSize, Height: @AbilitySlotSize, Bottom: 5);
Background: #2a3140;
LayoutMode: Top;
Padding: (Full: 4);
Label #Slot2Key {
Style: (FontSize: 12, RenderBold: true, TextColor: #f5c842);
Text: "[E]";
Anchor: (Bottom: 2);
}
Label #Slot2Name {
Style: (FontSize: 10, TextColor: #ffffff, Wrap: true);
Text: "Capacité 2";
}
}
Group #AbilitySlot3 {
Anchor: (Width: @AbilitySlotSize, Height: @AbilitySlotSize);
Background: #2a3140;
LayoutMode: Top;
Padding: (Full: 4);
Label #Slot3Key {
Style: (FontSize: 12, RenderBold: true, TextColor: #f5c842);
Text: "[R]";
Anchor: (Bottom: 2);
}
Label #Slot3Name {
Style: (FontSize: 10, TextColor: #ffffff, Wrap: true);
Text: "Capacité 3";
}
}
}
}
@@ -0,0 +1,100 @@
@MmorpgBg = #0d0f14(0.98);
@MmorpgPanel = #1a1c20;
@MmorpgAccent = #f5c842;
@MmorpgText = #e8e8ec;
@MmorpgMuted = #878e9c;
Group {
LayoutMode: Center;
Group #CharacterSelectRoot {
LayoutMode: Top;
Anchor: (Width: 640, Height: 520);
Background: @MmorpgBg;
Padding: (Full: 20);
Label #Title {
Style: (FontSize: 22, RenderBold: true, TextColor: @MmorpgAccent, HorizontalAlignment: Center);
Text: "Selection du personnage";
Anchor: (Bottom: 6);
}
Label #Subtitle {
Style: (FontSize: 14, TextColor: @MmorpgText, HorizontalAlignment: Center);
Text: "Choisissez un personnage pour entrer en jeu";
Anchor: (Bottom: 14);
}
Group #CharacterListHost {
LayoutMode: Top;
FlexWeight: 1;
Background: @MmorpgPanel;
Padding: (Full: 12);
Label #EmptyLabel {
Style: (FontSize: 14, TextColor: @MmorpgMuted, HorizontalAlignment: Center, Wrap: true);
Text: "Aucun personnage — creez-en un pour commencer.";
Visible: false;
}
Group #CharRow0 { LayoutMode: Left; Padding: (Vertical: 6); Anchor: (Bottom: 4); Visible: false;
Label #CharName0 { Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true); Text: "Perso 1"; FlexWeight: 1; }
TextButton #CharDelete0 { Text: "Suppr."; Anchor: (Width: 72, Right: 6); }
TextButton #CharSelect0 { Text: "Jouer"; Anchor: (Width: 88); }
}
Group #CharRow1 { LayoutMode: Left; Padding: (Vertical: 6); Anchor: (Bottom: 4); Visible: false;
Label #CharName1 { Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true); Text: "Perso 2"; FlexWeight: 1; }
TextButton #CharDelete1 { Text: "Suppr."; Anchor: (Width: 72, Right: 6); }
TextButton #CharSelect1 { Text: "Jouer"; Anchor: (Width: 88); }
}
Group #CharRow2 { LayoutMode: Left; Padding: (Vertical: 6); Anchor: (Bottom: 4); Visible: false;
Label #CharName2 { Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true); Text: "Perso 3"; FlexWeight: 1; }
TextButton #CharDelete2 { Text: "Suppr."; Anchor: (Width: 72, Right: 6); }
TextButton #CharSelect2 { Text: "Jouer"; Anchor: (Width: 88); }
}
Group #CharRow3 { LayoutMode: Left; Padding: (Vertical: 6); Anchor: (Bottom: 4); Visible: false;
Label #CharName3 { Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true); Text: "Perso 4"; FlexWeight: 1; }
TextButton #CharDelete3 { Text: "Suppr."; Anchor: (Width: 72, Right: 6); }
TextButton #CharSelect3 { Text: "Jouer"; Anchor: (Width: 88); }
}
Group #CharRow4 { LayoutMode: Left; Padding: (Vertical: 6); Anchor: (Bottom: 4); Visible: false;
Label #CharName4 { Style: (FontSize: 15, TextColor: @MmorpgText, Wrap: true); Text: "Perso 5"; FlexWeight: 1; }
TextButton #CharDelete4 { Text: "Suppr."; Anchor: (Width: 72, Right: 6); }
TextButton #CharSelect4 { Text: "Jouer"; Anchor: (Width: 88); }
}
}
Group #ConfirmPanel {
LayoutMode: Top;
Visible: false;
Background: #2a1515(0.95);
Padding: (Full: 12);
Anchor: (Bottom: 8);
Label #ConfirmMessage {
Style: (FontSize: 14, TextColor: @MmorpgText, Wrap: true, HorizontalAlignment: Center);
Text: "Confirmer la suppression ?";
Anchor: (Bottom: 10);
}
Group #ConfirmActions {
LayoutMode: Left;
TextButton #ConfirmDeleteButton { Text: "Confirmer la suppression"; FlexWeight: 1; Anchor: (Right: 8); }
TextButton #CancelDeleteButton { Text: "Annuler"; FlexWeight: 1; }
}
}
Group #Actions {
LayoutMode: Left;
Anchor: (Top: 12);
TextButton #CreateCharacterButton { Text: "Nouveau personnage"; FlexWeight: 1; Anchor: (Right: 8); }
TextButton #RefreshButton { Text: "Actualiser"; FlexWeight: 1; }
}
Label #FooterHint {
Style: (FontSize: 11, TextColor: @MmorpgMuted, HorizontalAlignment: Center);
Text: "Vous devez selectionner un personnage pour jouer";
Anchor: (Top: 10);
}
}
}
@@ -0,0 +1,171 @@
@MmorpgAccent = #f5c842;
@MmorpgText = #e8e8ec;
@MmorpgMuted = #878e9c;
@MmorpgBorder = #3a3f4a;
@HealthFill = PatchStyle(Color: #e74c3c);
@HealthBg = PatchStyle(Color: #2a1515);
@StaminaFill = PatchStyle(Color: #2ecc71);
@StaminaBg = PatchStyle(Color: #152a1c);
@XpFill = PatchStyle(Color: #4a9eff);
@XpBg = PatchStyle(Color: #152030);
Group {
LayoutMode: Top;
Group {
LayoutMode: Left;
Group #LeftColumn {
LayoutMode: Top;
Anchor: (Width: 400, Right: 16);
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "IDENTITÉ";
Anchor: (Bottom: 6);
}
Label #IdentityName {
Style: (FontSize: 18, RenderBold: true, TextColor: @MmorpgAccent);
Text: "Joueur";
Anchor: (Bottom: 4);
}
Label #RaceLabel {
Style: (FontSize: 13, TextColor: @MmorpgText);
Text: "Race : Humain";
Anchor: (Bottom: 2);
}
Label #ClassLabel {
Style: (FontSize: 13, TextColor: @MmorpgText);
Text: "Classe : —";
Anchor: (Bottom: 10);
}
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "PROGRESSION";
Anchor: (Bottom: 6);
}
Label #XpValue {
Style: (FontSize: 11, TextColor: @MmorpgText);
Text: "0 / 100 XP";
Anchor: (Bottom: 4);
}
ProgressBar #XpBar {
Anchor: (Width: 360, Height: 14, Bottom: 10);
Bar: @XpFill;
Background: @XpBg;
Value: 0.0;
Direction: End;
}
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "VITALITÉ";
Anchor: (Bottom: 6);
}
Label #HealthValue {
Style: (FontSize: 10, TextColor: @MmorpgText);
Text: "Vie : —";
Anchor: (Bottom: 2);
}
ProgressBar #HealthBar {
Anchor: (Width: 360, Height: 12, Bottom: 6);
Bar: @HealthFill;
Background: @HealthBg;
Value: 1.0;
Direction: End;
}
Label #StaminaValue {
Style: (FontSize: 10, TextColor: @MmorpgText);
Text: "Endurance : —";
Anchor: (Bottom: 2);
}
ProgressBar #StaminaBar {
Anchor: (Width: 360, Height: 12, Bottom: 10);
Bar: @StaminaFill;
Background: @StaminaBg;
Value: 1.0;
Direction: End;
}
}
Group #RightColumn {
LayoutMode: Top;
FlexWeight: 1;
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "POUVOIRS PASSIFS";
Anchor: (Bottom: 4);
}
Label #PowersList {
Style: (FontSize: 12, TextColor: @MmorpgText, Wrap: true);
Text: "Aucun";
Anchor: (Bottom: 10);
}
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "MÉTIERS";
Anchor: (Bottom: 4);
}
Label #JobsList {
Style: (FontSize: 12, TextColor: @MmorpgText, Wrap: true);
Text: "Aucun";
Anchor: (Bottom: 10);
}
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "SOCIAL";
Anchor: (Bottom: 4);
}
Label #GroupLabel {
Style: (FontSize: 12, TextColor: @MmorpgText);
Text: "Groupe : Aucun";
Anchor: (Bottom: 2);
}
Label #GuildLabel {
Style: (FontSize: 12, TextColor: @MmorpgText);
Text: "Guilde : Aucune";
Anchor: (Bottom: 10);
}
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "ACTIVITÉ";
Anchor: (Bottom: 4);
}
Label #PlayTimeLabel {
Style: (FontSize: 12, TextColor: @MmorpgText);
Text: "Temps de jeu : 0 min";
Anchor: (Bottom: 2);
}
Label #LastLoginLabel {
Style: (FontSize: 12, TextColor: @MmorpgText);
Text: "Dernière connexion : —";
Anchor: (Bottom: 2);
}
Label #CreatedLabel {
Style: (FontSize: 12, TextColor: @MmorpgText);
Text: "Création : —";
}
}
}
}
@@ -0,0 +1,95 @@
@MmorpgBg = #0d0f14(0.98);
@MmorpgPanel = #1a1c20;
@MmorpgSlot = #2a3140;
@MmorpgAccent = #f5c842;
@MmorpgText = #e8e8ec;
@MmorpgMuted = #878e9c;
Group {
LayoutMode: Center;
Group #ClassSelectRoot {
LayoutMode: Top;
Anchor: (Width: 720, Height: 520);
Background: @MmorpgBg;
Padding: (Full: 20);
Label #Title {
Style: (FontSize: 22, RenderBold: true, TextColor: @MmorpgAccent, HorizontalAlignment: Center);
Text: "Choisissez votre classe";
Anchor: (Bottom: 6);
}
Label #Subtitle {
Style: (FontSize: 14, TextColor: @MmorpgText, HorizontalAlignment: Center);
Text: "Cliquez sur Choisir pour valider";
Anchor: (Bottom: 16);
}
Group #ClassGrid {
LayoutMode: Top;
FlexWeight: 1;
Background: @MmorpgPanel;
Padding: (Full: 14);
Group #ClassRow0 {
LayoutMode: Left;
Anchor: (Bottom: 12);
Group #ClassCard0 {
LayoutMode: Top;
Padding: (Full: 12);
Background: @MmorpgSlot;
Anchor: (Width: 320, Right: 12);
Visible: false;
Label #ClassName0 { Style: (FontSize: 16, RenderBold: true, TextColor: @MmorpgAccent, HorizontalAlignment: Center); Text: "Classe"; Anchor: (Bottom: 4); }
Label #ClassDesc0 { Style: (FontSize: 12, TextColor: @MmorpgMuted, Wrap: true, HorizontalAlignment: Center); Text: "Description"; Anchor: (Bottom: 8); }
TextButton #ClassPick0 { Text: "Choisir"; }
}
Group #ClassCard1 {
LayoutMode: Top;
Padding: (Full: 12);
Background: @MmorpgSlot;
Anchor: (Width: 320);
Visible: false;
Label #ClassName1 { Style: (FontSize: 16, RenderBold: true, TextColor: @MmorpgAccent, HorizontalAlignment: Center); Text: "Classe"; Anchor: (Bottom: 4); }
Label #ClassDesc1 { Style: (FontSize: 12, TextColor: @MmorpgMuted, Wrap: true, HorizontalAlignment: Center); Text: "Description"; Anchor: (Bottom: 8); }
TextButton #ClassPick1 { Text: "Choisir"; }
}
}
Group #ClassRow1 {
LayoutMode: Left;
Group #ClassCard2 {
LayoutMode: Top;
Padding: (Full: 12);
Background: @MmorpgSlot;
Anchor: (Width: 320, Right: 12);
Visible: false;
Label #ClassName2 { Style: (FontSize: 16, RenderBold: true, TextColor: @MmorpgAccent, HorizontalAlignment: Center); Text: "Classe"; Anchor: (Bottom: 4); }
Label #ClassDesc2 { Style: (FontSize: 12, TextColor: @MmorpgMuted, Wrap: true, HorizontalAlignment: Center); Text: "Description"; Anchor: (Bottom: 8); }
TextButton #ClassPick2 { Text: "Choisir"; }
}
Group #ClassCard3 {
LayoutMode: Top;
Padding: (Full: 12);
Background: @MmorpgSlot;
Anchor: (Width: 320);
Visible: false;
Label #ClassName3 { Style: (FontSize: 16, RenderBold: true, TextColor: @MmorpgAccent, HorizontalAlignment: Center); Text: "Classe"; Anchor: (Bottom: 4); }
Label #ClassDesc3 { Style: (FontSize: 12, TextColor: @MmorpgMuted, Wrap: true, HorizontalAlignment: Center); Text: "Description"; Anchor: (Bottom: 8); }
TextButton #ClassPick3 { Text: "Choisir"; }
}
}
}
Label #FooterHint {
Style: (FontSize: 11, TextColor: @MmorpgMuted, HorizontalAlignment: Center);
Text: "Vous devez choisir une classe pour jouer";
Anchor: (Top: 12);
}
}
}
@@ -0,0 +1,277 @@
@MmorpgBg = #0d0f14(0.96);
@MmorpgPanel = #1a1c20;
@MmorpgAccent = #f5c842;
@MmorpgText = #e8e8ec;
@MmorpgMuted = #878e9c;
@MmorpgBorder = #3a3f4a;
@SlotSize = 52;
@SlotGap = 4;
@SlotColWidth = @SlotSize + @SlotGap;
@SlotRowHeight = @SlotSize + @SlotGap;
@StorageGridWidth = (@SlotColWidth * 9) - @SlotGap;
@StorageGridHeight = (@SlotRowHeight * 4) - @SlotGap;
@HotbarWidth = (@SlotColWidth * 9) - @SlotGap;
@ColumnHeight = (@SlotRowHeight * 4) - @SlotGap;
@ShellWidth = 920;
@ShellHeight = 660;
Group {
LayoutMode: Center;
Group #MmorpgShell {
LayoutMode: Top;
Anchor: (Width: @ShellWidth, Height: @ShellHeight);
Background: @MmorpgBg;
Padding: (Full: 16);
Group #Header {
LayoutMode: Left;
Anchor: (Bottom: 12);
Group {
LayoutMode: Top;
FlexWeight: 1;
Label #PlayerName {
Style: (FontSize: 20, RenderBold: true, TextColor: @MmorpgAccent);
Text: "Joueur";
Anchor: (Bottom: 2);
}
Label #ClassLabel {
Style: (FontSize: 13, TextColor: @MmorpgText);
Text: "Classe";
}
}
Group {
LayoutMode: Top;
Anchor: (Right: 24);
Label #LevelLabel {
Style: (FontSize: 14, RenderBold: true, TextColor: @MmorpgText);
Text: "Nv. 1";
Anchor: (Bottom: 2);
}
Label #MoneyLabel {
Style: (FontSize: 13, TextColor: @MmorpgAccent);
Text: "0 pièces";
}
}
}
Group #TabBar {
LayoutMode: Left;
Padding: (Bottom: 10);
Anchor: (Bottom: 8);
Group #TabCharacterWrap {
LayoutMode: Top;
Padding: (Horizontal: 12, Vertical: 4);
Anchor: (Right: 6);
Background: PatchStyle(Color: #00000000);
TextButton #TabCharacter {
Text: "Personnage";
}
Group #TabCharacterIndicator {
Anchor: (Height: 2, Top: 4);
Background: PatchStyle(Color: @MmorpgAccent);
Visible: false;
}
}
Group #TabInventoryWrap {
LayoutMode: Top;
Padding: (Horizontal: 12, Vertical: 4);
Anchor: (Right: 6);
Background: PatchStyle(Color: #00000000);
TextButton #TabInventory {
Text: "Inventaire";
}
Group #TabInventoryIndicator {
Anchor: (Height: 2, Top: 4);
Background: PatchStyle(Color: @MmorpgAccent);
Visible: false;
}
}
Group #TabStatsWrap {
LayoutMode: Top;
Padding: (Horizontal: 12, Vertical: 4);
Anchor: (Right: 6);
Background: PatchStyle(Color: #00000000);
TextButton #TabStats {
Text: "Statistiques";
}
Group #TabStatsIndicator {
Anchor: (Height: 2, Top: 4);
Background: PatchStyle(Color: @MmorpgAccent);
Visible: false;
}
}
Group #TabSkillsWrap {
LayoutMode: Top;
Padding: (Horizontal: 12, Vertical: 4);
Anchor: (Right: 6);
Background: PatchStyle(Color: #00000000);
TextButton #TabSkills {
Text: "Compétences";
}
Group #TabSkillsIndicator {
Anchor: (Height: 2, Top: 4);
Background: PatchStyle(Color: @MmorpgAccent);
Visible: false;
}
}
}
Group #TabContent {
LayoutMode: Left;
FlexWeight: 1;
Group #EquipmentPanel {
LayoutMode: Top;
Anchor: (Width: 88, Right: 12);
Padding: (Full: 8);
Background: @MmorpgPanel;
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "ARMURE";
Anchor: (Bottom: 4);
}
Label #ArmorSummary {
Style: (FontSize: 9, TextColor: @MmorpgMuted);
Text: "Armure : 0 / 4";
Anchor: (Bottom: 6);
}
Group #ArmorSlots {
LayoutMode: Top;
Anchor: (Width: @SlotSize, Height: @ColumnHeight, Bottom: 10);
}
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "UTILITAIRE";
Anchor: (Bottom: 4);
}
Label #UtilitySummary {
Style: (FontSize: 9, TextColor: @MmorpgMuted);
Text: "Utilitaire : 0 / 4";
Anchor: (Bottom: 6);
}
Group #UtilitySlots {
LayoutMode: Top;
Anchor: (Width: @SlotSize, Height: @ColumnHeight);
}
}
Group #InventoryCenter {
LayoutMode: Top;
FlexWeight: 1;
Padding: (Full: 8);
Background: @MmorpgPanel;
Label #StorageSummary {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgAccent);
Text: "Sac : 0 / 36";
Anchor: (Bottom: 6);
}
Group #StorageSlots {
LayoutMode: Top;
Anchor: (Width: @StorageGridWidth, Height: @StorageGridHeight, Bottom: 10);
}
Label #HotbarSummary {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgAccent);
Text: "Barre rapide : 0 / 9";
Anchor: (Bottom: 6);
}
Group #HotbarSlots {
LayoutMode: Top;
Anchor: (Width: @HotbarWidth, Height: @SlotSize);
}
}
Group #ItemDetails {
LayoutMode: Top;
Anchor: (Width: 260, Left: 12);
Padding: (Full: 10);
Background: @MmorpgPanel;
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "DÉTAILS";
Anchor: (Bottom: 8);
}
Label #ItemName {
Style: (FontSize: 15, RenderBold: true, TextColor: @MmorpgAccent);
Text: "Aucun objet";
Anchor: (Bottom: 6);
}
Label #ItemRarity {
Style: (FontSize: 11, TextColor: @MmorpgMuted);
Text: "Sélectionnez un emplacement";
Anchor: (Bottom: 8);
}
Label #ItemDescription {
Style: (FontSize: 12, TextColor: @MmorpgText, Wrap: true);
Text: "Cliquez un objet pour afficher ses informations.";
Anchor: (Bottom: 12);
}
Group #WeightTrack {
LayoutMode: Top;
Anchor: (Bottom: 4);
Label {
Style: (FontSize: 10, TextColor: @MmorpgMuted);
Text: "Remplissage du sac";
Anchor: (Bottom: 4);
}
ProgressBar #WeightBar {
Anchor: (Width: 220, Height: 10);
Bar: PatchStyle(Color: @MmorpgAccent);
Background: PatchStyle(Color: @MmorpgBorder);
Value: 0.0;
Direction: End;
}
Label #WeightValue {
Style: (FontSize: 9, TextColor: @MmorpgMuted);
Text: "0 piles";
Anchor: (Top: 4);
}
}
}
}
Label #FooterHint {
Style: (FontSize: 11, TextColor: @MmorpgMuted);
Text: "Échap pour fermer — clic source puis destination pour déplacer, survol pour les détails";
Anchor: (Top: 10);
}
}
}
@@ -0,0 +1,161 @@
@MmorpgPanel = #1a1c20;
@MmorpgAccent = #f5c842;
@MmorpgText = #e8e8ec;
@MmorpgMuted = #878e9c;
@MmorpgBorder = #3a3f4a;
@GridStyle = ItemGridStyle(
SlotSpacing: 4,
SlotSize: 52,
SlotIconSize: 44
);
Group {
LayoutMode: Left;
Group #EquipmentPanel {
LayoutMode: Top;
Anchor: (Width: 88, Right: 12);
Padding: (Full: 8);
Background: @MmorpgPanel;
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "ARMURE";
Anchor: (Bottom: 4);
}
Label #ArmorSummary {
Style: (FontSize: 9, TextColor: @MmorpgMuted);
Text: "Armure : 0 / 4";
Anchor: (Bottom: 6);
}
ItemGrid #ArmorGrid {
Anchor: (Width: 56, Height: 232, Bottom: 10);
SlotsPerRow: 1;
InventorySectionId: -3;
AreItemsDraggable: true;
DisplayItemQuantity: true;
RenderItemQualityBackground: true;
Style: @GridStyle;
}
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "UTILITAIRE";
Anchor: (Bottom: 4);
}
Label #UtilitySummary {
Style: (FontSize: 9, TextColor: @MmorpgMuted);
Text: "Utilitaire : 0 / 4";
Anchor: (Bottom: 6);
}
ItemGrid #UtilityGrid {
Anchor: (Width: 56, Height: 232);
SlotsPerRow: 1;
InventorySectionId: -5;
AreItemsDraggable: true;
DisplayItemQuantity: true;
RenderItemQualityBackground: true;
Style: @GridStyle;
}
}
Group #InventoryCenter {
LayoutMode: Top;
FlexWeight: 1;
Padding: (Full: 8);
Background: @MmorpgPanel;
Label #StorageSummary {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgAccent);
Text: "Sac : 0 / 36";
Anchor: (Bottom: 6);
}
ItemGrid #StorageGrid {
Anchor: (Width: 500, Height: 232, Bottom: 10);
SlotsPerRow: 9;
InventorySectionId: -2;
AreItemsDraggable: true;
DisplayItemQuantity: true;
RenderItemQualityBackground: true;
Style: @GridStyle;
}
Label #HotbarSummary {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgAccent);
Text: "Barre rapide : 0 / 9";
Anchor: (Bottom: 6);
}
ItemGrid #HotbarGrid {
Anchor: (Width: 500, Height: 60);
SlotsPerRow: 9;
InventorySectionId: -1;
AreItemsDraggable: true;
DisplayItemQuantity: true;
RenderItemQualityBackground: true;
Style: @GridStyle;
}
}
Group #ItemDetails {
LayoutMode: Top;
Anchor: (Width: 260, Left: 12);
Padding: (Full: 10);
Background: @MmorpgPanel;
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "DÉTAILS";
Anchor: (Bottom: 8);
}
Label #ItemName {
Style: (FontSize: 15, RenderBold: true, TextColor: @MmorpgAccent);
Text: "Aucun objet";
Anchor: (Bottom: 6);
}
Label #ItemRarity {
Style: (FontSize: 11, TextColor: @MmorpgMuted);
Text: "Sélectionnez un emplacement";
Anchor: (Bottom: 8);
}
Label #ItemDescription {
Style: (FontSize: 12, TextColor: @MmorpgText, Wrap: true);
Text: "Cliquez un objet pour afficher ses informations.";
Anchor: (Bottom: 12);
}
Group #WeightTrack {
LayoutMode: Top;
Anchor: (Bottom: 4);
Label {
Style: (FontSize: 10, TextColor: @MmorpgMuted);
Text: "Remplissage du sac";
Anchor: (Bottom: 4);
}
ProgressBar #WeightBar {
Anchor: (Width: 220, Height: 10);
Bar: PatchStyle(Color: @MmorpgAccent);
Background: PatchStyle(Color: @MmorpgBorder);
Value: 0.0;
Direction: End;
}
Label #WeightValue {
Style: (FontSize: 9, TextColor: @MmorpgMuted);
Text: "0 piles";
Anchor: (Top: 4);
}
}
}
}
@@ -0,0 +1,29 @@
$C = "../Common.ui";
$C.@PageOverlay {}
$C.@Container {
Anchor: (Width: 600, Height: 700);
#Title {
Group {
$C.@Title {
@Text = %server.customUI.warpListPage.warps;
}
$C.@HeaderSearch {}
}
}
#Content {
LayoutMode: Left;
Group #ProfileList {
FlexWeight: 1;
LayoutMode: TopScrolling;
ScrollbarStyle: $C.@DefaultScrollbarStyle;
}
}
}
$C.@BackButton {}
@@ -0,0 +1,148 @@
@MmorpgBg = #0d0f14(0.96);
@MmorpgPanel = #1a1c20;
@MmorpgAccent = #f5c842;
@MmorpgText = #e8e8ec;
@MmorpgMuted = #878e9c;
@ShellWidth = 920;
@ShellHeight = 660;
Group {
LayoutMode: Center;
Group #MmorpgShell {
LayoutMode: Top;
Anchor: (Width: @ShellWidth, Height: @ShellHeight);
Background: @MmorpgBg;
Padding: (Full: 16);
Group #Header {
LayoutMode: Left;
Anchor: (Bottom: 12);
Group {
LayoutMode: Top;
FlexWeight: 1;
Label #PlayerName {
Style: (FontSize: 20, RenderBold: true, TextColor: @MmorpgAccent);
Text: "Joueur";
Anchor: (Bottom: 2);
}
Label #HeaderSubtitle {
Style: (FontSize: 13, TextColor: @MmorpgText);
Text: "Profil MMORPG";
}
}
Group {
LayoutMode: Top;
Anchor: (Right: 24);
Label #LevelLabel {
Style: (FontSize: 14, RenderBold: true, TextColor: @MmorpgText);
Text: "Nv. 1";
Anchor: (Bottom: 2);
}
Label #MoneyLabel {
Style: (FontSize: 13, TextColor: @MmorpgAccent);
Text: "0 pièces";
}
}
}
Group #TabBar {
LayoutMode: Left;
Padding: (Bottom: 10);
Anchor: (Bottom: 8);
Group #TabCharacterWrap {
LayoutMode: Top;
Padding: (Horizontal: 12, Vertical: 4);
Anchor: (Right: 6);
Background: PatchStyle(Color: #00000000);
TextButton #TabCharacter {
Text: "Personnage";
}
Group #TabCharacterIndicator {
Anchor: (Height: 2, Top: 4);
Background: PatchStyle(Color: @MmorpgAccent);
Visible: false;
}
}
Group #TabInventoryWrap {
LayoutMode: Top;
Padding: (Horizontal: 12, Vertical: 4);
Anchor: (Right: 6);
Background: PatchStyle(Color: #00000000);
TextButton #TabInventory {
Text: "Inventaire";
}
Group #TabInventoryIndicator {
Anchor: (Height: 2, Top: 4);
Background: PatchStyle(Color: @MmorpgAccent);
Visible: false;
}
}
Group #TabStatsWrap {
LayoutMode: Top;
Padding: (Horizontal: 12, Vertical: 4);
Anchor: (Right: 6);
Background: PatchStyle(Color: #00000000);
TextButton #TabStats {
Text: "Statistiques";
}
Group #TabStatsIndicator {
Anchor: (Height: 2, Top: 4);
Background: PatchStyle(Color: @MmorpgAccent);
Visible: false;
}
}
Group #TabSkillsWrap {
LayoutMode: Top;
Padding: (Horizontal: 12, Vertical: 4);
Anchor: (Right: 6);
Background: PatchStyle(Color: #00000000);
TextButton #TabSkills {
Text: "Compétences";
}
Group #TabSkillsIndicator {
Anchor: (Height: 2, Top: 4);
Background: PatchStyle(Color: @MmorpgAccent);
Visible: false;
}
}
}
Group #TabContent {
LayoutMode: Top;
FlexWeight: 1;
Group #DynamicTabHost {
LayoutMode: Top;
FlexWeight: 1;
Background: @MmorpgPanel;
Padding: (Full: 10);
}
}
Label #FooterHint {
Style: (FontSize: 11, TextColor: @MmorpgMuted);
Text: "Échap pour fermer";
Anchor: (Top: 10);
}
}
}
@@ -0,0 +1,198 @@
@MmorpgAccent = #f5c842;
@MmorpgText = #e8e8ec;
@MmorpgMuted = #878e9c;
@MmorpgSlot = #2a3140;
@MmorpgLocked = #1e2228;
Group {
LayoutMode: Top;
Label #ClassTitle {
Style: (FontSize: 16, RenderBold: true, TextColor: @MmorpgAccent);
Text: "Classe";
Anchor: (Bottom: 2);
}
Label #WeaponsLabel {
Style: (FontSize: 11, TextColor: @MmorpgMuted);
Text: "Armes : —";
Anchor: (Bottom: 6);
}
Label #PointsLabel {
Style: (FontSize: 12, TextColor: @MmorpgText);
Text: "Points de compétence : 0";
Anchor: (Bottom: 10);
}
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "CAPACITÉS DE CLASSE";
Anchor: (Bottom: 6);
}
Group {
LayoutMode: Left;
Anchor: (Bottom: 14);
Group #AbilityPanel1 {
LayoutMode: Top;
Padding: (Full: 8);
Background: @MmorpgSlot;
Anchor: (Width: 250, Right: 10);
Label #Ability1Key {
Style: (FontSize: 12, RenderBold: true, TextColor: @MmorpgAccent);
Text: "[Q]";
Anchor: (Bottom: 4);
}
Label #Ability1Name {
Style: (FontSize: 14, RenderBold: true, TextColor: @MmorpgText);
Text: "Capacité 1";
Anchor: (Bottom: 4);
}
Label #Ability1Cooldown {
Style: (FontSize: 10, TextColor: @MmorpgMuted);
Text: "Prêt";
Anchor: (Bottom: 6);
}
Label #Ability1Desc {
Style: (FontSize: 11, TextColor: @MmorpgMuted, Wrap: true);
Text: "—";
}
}
Group #AbilityPanel2 {
LayoutMode: Top;
Padding: (Full: 8);
Background: @MmorpgSlot;
Anchor: (Width: 250, Right: 10);
Label #Ability2Key {
Style: (FontSize: 12, RenderBold: true, TextColor: @MmorpgAccent);
Text: "[E]";
Anchor: (Bottom: 4);
}
Label #Ability2Name {
Style: (FontSize: 14, RenderBold: true, TextColor: @MmorpgText);
Text: "Capacité 2";
Anchor: (Bottom: 4);
}
Label #Ability2Cooldown {
Style: (FontSize: 10, TextColor: @MmorpgMuted);
Text: "Prêt";
Anchor: (Bottom: 6);
}
Label #Ability2Desc {
Style: (FontSize: 11, TextColor: @MmorpgMuted, Wrap: true);
Text: "—";
}
}
Group #AbilityPanel3 {
LayoutMode: Top;
Padding: (Full: 8);
Background: @MmorpgSlot;
Anchor: (Width: 250);
Label #Ability3Key {
Style: (FontSize: 12, RenderBold: true, TextColor: @MmorpgAccent);
Text: "[R]";
Anchor: (Bottom: 4);
}
Label #Ability3Name {
Style: (FontSize: 14, RenderBold: true, TextColor: @MmorpgText);
Text: "Capacité 3";
Anchor: (Bottom: 4);
}
Label #Ability3Cooldown {
Style: (FontSize: 10, TextColor: @MmorpgMuted);
Text: "Prêt";
Anchor: (Bottom: 6);
}
Label #Ability3Desc {
Style: (FontSize: 11, TextColor: @MmorpgMuted, Wrap: true);
Text: "—";
}
}
}
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "BRANCHES DE COMPÉTENCES";
Anchor: (Bottom: 6);
}
Group {
LayoutMode: Left;
Anchor: (Bottom: 8);
Group #Branch1 {
LayoutMode: Top;
Padding: (Full: 8);
Background: @MmorpgLocked;
Anchor: (Width: 180, Right: 10);
Label #Branch1Title {
Style: (FontSize: 12, RenderBold: true, TextColor: @MmorpgMuted);
Text: "Offensive";
Anchor: (Bottom: 4);
}
Label #Branch1Status {
Style: (FontSize: 10, TextColor: @MmorpgMuted);
Text: "Verrouillé — M1";
}
}
Group #Branch2 {
LayoutMode: Top;
Padding: (Full: 8);
Background: @MmorpgLocked;
Anchor: (Width: 180, Right: 10);
Label #Branch2Title {
Style: (FontSize: 12, RenderBold: true, TextColor: @MmorpgMuted);
Text: "Défensive";
Anchor: (Bottom: 4);
}
Label #Branch2Status {
Style: (FontSize: 10, TextColor: @MmorpgMuted);
Text: "Verrouillé — M1";
}
}
Group #Branch3 {
LayoutMode: Top;
Padding: (Full: 8);
Background: @MmorpgLocked;
Anchor: (Width: 180);
Label #Branch3Title {
Style: (FontSize: 12, RenderBold: true, TextColor: @MmorpgMuted);
Text: "Utilitaire";
Anchor: (Bottom: 4);
}
Label #Branch3Status {
Style: (FontSize: 10, TextColor: @MmorpgMuted);
Text: "Verrouillé — M1";
}
}
}
Label #SkillsHint {
Style: (FontSize: 10, TextColor: @MmorpgMuted, Wrap: true);
Text: "Utilisez [Q] [E] [R] (Use Ability 1/2/3) en jeu. L'arbre de talents complet arrive en M1.";
}
}
@@ -0,0 +1,65 @@
@MmorpgAccent = #f5c842;
@MmorpgText = #e8e8ec;
@MmorpgMuted = #878e9c;
Group {
LayoutMode: Top;
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "ATTRIBUTS";
Anchor: (Bottom: 4);
}
Label #StatPointsLabel {
Style: (FontSize: 14, RenderBold: true, TextColor: @MmorpgAccent);
Text: "Points à attribuer : 0";
Anchor: (Bottom: 12);
}
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "RÉPARTITION";
Anchor: (Bottom: 8);
}
Group {
LayoutMode: Top;
Background: #141820;
Padding: (Full: 10);
Anchor: (Bottom: 12);
Group #StatForceRow { LayoutMode: Left; Anchor: (Bottom: 6);
Label #StatForceLabel { Style: (FontSize: 12, TextColor: @MmorpgText); Text: "Force 0"; FlexWeight: 1; }
TextButton #StatForceAdd { Text: "+"; Anchor: (Width: 32); Visible: false; }
}
Group #StatVitalityRow { LayoutMode: Left; Anchor: (Bottom: 6);
Label #StatVitalityLabel { Style: (FontSize: 12, TextColor: @MmorpgText); Text: "Vitalité 0"; FlexWeight: 1; }
TextButton #StatVitalityAdd { Text: "+"; Anchor: (Width: 32); Visible: false; }
}
Group #StatDexterityRow { LayoutMode: Left; Anchor: (Bottom: 6);
Label #StatDexterityLabel { Style: (FontSize: 12, TextColor: @MmorpgText); Text: "Dextérité 0"; FlexWeight: 1; }
TextButton #StatDexterityAdd { Text: "+"; Anchor: (Width: 32); Visible: false; }
}
Group #StatSpeedRow { LayoutMode: Left; Anchor: (Bottom: 2);
Label #StatSpeedLabel { Style: (FontSize: 12, TextColor: @MmorpgText); Text: "Vitesse 0"; FlexWeight: 1; }
TextButton #StatSpeedAdd { Text: "+"; Anchor: (Width: 32); Visible: false; }
}
}
Label {
Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted);
Text: "EFFETS PAR POINT";
Anchor: (Bottom: 6);
}
Label #StatForceBonus { Style: (FontSize: 11, TextColor: @MmorpgText); Text: "Force : +2 attaque"; Anchor: (Bottom: 3); }
Label #StatVitalityBonus { Style: (FontSize: 11, TextColor: @MmorpgText); Text: "Vitalité : +10 vie"; Anchor: (Bottom: 3); }
Label #StatDexterityBonus { Style: (FontSize: 11, TextColor: @MmorpgText); Text: "Dextérité : +1% vitesse d'attaque"; Anchor: (Bottom: 3); }
Label #StatSpeedBonus { Style: (FontSize: 11, TextColor: @MmorpgText); Text: "Vitesse : +2 déplacement"; Anchor: (Bottom: 8); }
Label #StatsHint {
Style: (FontSize: 10, TextColor: @MmorpgMuted, Wrap: true);
Text: "Gagnez 10 points à chaque niveau. Cliquez + pour attribuer un point. Toutes les statistiques commencent à 0.";
}
}
@@ -0,0 +1,9 @@
{
"Type": "Projectile",
"RunTime": 0.2,
"Effects": {
"ItemAnimationId": "ShootCharged",
"ClearAnimationOnFinish": true
},
"Config": "Projectile_Config_Arrow_Shortbow_Strength_4"
}
@@ -0,0 +1,200 @@
{
"TranslationProperties": {
"Name": "server.items.Weapon_Arrow_Crude.name"
},
"Categories": [
"Items.Weapons"
],
"Quality": "Common",
"ItemLevel": 9,
"PlayerAnimationsId": "Dagger",
"Recipe": {
"TimeSeconds": 0,
"KnowledgeRequired": false,
"Input": [
{
"Quantity": 4,
"ItemId": "Ingredient_Stick"
},
{
"ResourceTypeId": "Rubble",
"Quantity": 1
}
],
"OutputQuantity": 4,
"BenchRequirement": [
{
"Type": "Crafting",
"Categories": [
"Workbench_Survival"
],
"Id": "Workbench"
},
{
"Type": "Crafting",
"Categories": [
"Weapon_Bow"
],
"Id": "Weapon_Bench"
}
]
},
"IconProperties": {
"Scale": 0.5,
"Rotation": [
45.0,
90.0,
0.0
],
"Translation": [
-25.0,
-25.0
]
},
"Model": "Items/Weapons/Arrow/Arrow.blockymodel",
"Texture": "Items/Weapons/Arrow/Crude_Texture.png",
"Icon": "Icons/ItemsGenerated/Weapon_Arrow_Crude.png",
"Interactions": {
"Primary": "Knife_Attack",
"Secondary": "Knife_Block"
},
"InteractionVars": {
"Knife_Block_Damage": "Knife_Block_Damage",
"Knife_Swing_Left_Effect": {
"Interactions": [
{
"Parent": "Knife_Swing_Left_Effect",
"Effects": {
"WorldSoundEventId": "SFX_Axe_Special_Swing"
}
}
]
},
"Knife_Swing_Left_Damage": {
"Interactions": [
{
"Parent": "Knife_Swing_Left_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 1
}
},
"DamageEffects": {
"Knockback": {
"Force": 0,
"VelocityType": "Add"
},
"WorldSoundEventId": "SFX_Axe_Special_Impact"
}
}
]
},
"Knife_Swing_Right_Effect": {
"Interactions": [
{
"Parent": "Knife_Swing_Right_Effect",
"Effects": {
"WorldSoundEventId": "SFX_Axe_Special_Swing"
}
}
]
},
"Knife_Swing_Right_Damage": {
"Interactions": [
{
"Parent": "Knife_Swing_Right_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 1
}
},
"DamageEffects": {
"Knockback": {
"Force": 0,
"VelocityType": "Add"
},
"WorldSoundEventId": "SFX_Axe_Special_Impact"
}
}
]
},
"Knife_Stab_Effect": {
"Interactions": [
{
"Parent": "Knife_Stab_Effect",
"Effects": {
"WorldSoundEventId": "SFX_Axe_Special_Swing"
}
}
]
},
"Knife_Stab_Damage": {
"Interactions": [
{
"Parent": "Knife_Stab_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 1
}
},
"DamageEffects": {
"Knockback": {
"Force": 0,
"VelocityType": "Add"
},
"WorldSoundEventId": "SFX_Axe_Special_Impact"
}
}
]
},
"Knife_Lunge_Effect": {
"Interactions": [
{
"Parent": "Knife_Lunge_Effect",
"Effects": {
"WorldSoundEventId": "SFX_Axe_Special_Swing"
}
}
]
},
"Knife_Lunge_Damage": {
"Interactions": [
{
"Parent": "Knife_Lunge_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 1
}
},
"DamageEffects": {
"Knockback": {
"Force": 0,
"VelocityType": "Add"
},
"WorldSoundEventId": "SFX_Axe_Special_Impact"
}
}
]
},
"Knife_Throw_Charged_Projectile": {
"Interactions": [
{
"Parent": "Knife_Throw_Charged_Projectile",
"ProjectileId": "Arrow_HalfCharge"
}
]
}
},
"MaxStack": 100,
"DroppedItemAnimation": "Items/Animations/Dropped/Dropped_Diagonal_Left.blockyanim",
"Tags": {
"Type": [
"Weapon"
],
"Family": [
"Arrow"
]
},
"Weapon": {},
"ItemSoundSetId": "ISS_Weapons_Arrows"
}
@@ -0,0 +1,167 @@
{
"Parent": "Template_Weapon_Battleaxe",
"TranslationProperties": {
"Name": "server.items.Weapon_Battleaxe_Mithril.name"
},
"Model": "Items/Weapons/Battleaxe/Mithril.blockymodel",
"Texture": "Items/Weapons/Battleaxe/Mithril_Texture.png",
"Icon": "Icons/ItemsGenerated/Weapon_Battleaxe_Mithril.png",
"Recipe": {
"TimeSeconds": 5,
"KnowledgeRequired": false,
"Input": [
{
"ItemId": "Ingredient_Bar_Mithril",
"Quantity": 10
},
{
"ItemId": "Ingredient_Leather_Storm",
"Quantity": 4
},
{
"ItemId": "Ingredient_Voidheart",
"Quantity": 1
}
],
"BenchRequirement": [
{
"Type": "Crafting",
"Categories": [
"Weapon_Battleaxe"
],
"Id": "Weapon_Bench",
"RequiredTierLevel": 3
}
]
},
"Quality": "Epic",
"ItemLevel": 50,
"InteractionVars": {
"Swing_Down_Damage": {
"Interactions": [
{
"Parent": "Weapon_Battleaxe_Primary_Swing_Down_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 73
}
},
"DamageEffects": {
"WorldSoundEventId": "SFX_Battleaxe_T2_Impact",
"LocalSoundEventId": "SFX_Battleaxe_T2_Impact"
}
}
]
},
"Swing_Down_Left_Damage": {
"Interactions": [
{
"Parent": "Weapon_Battleaxe_Primary_Swing_Down_Left_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 36
}
},
"DamageEffects": {
"WorldSoundEventId": "SFX_Battleaxe_T2_Impact",
"LocalSoundEventId": "SFX_Battleaxe_T2_Impact"
}
}
]
},
"Swing_Down_Right_Damage": {
"Interactions": [
{
"Parent": "Weapon_Battleaxe_Primary_Swing_Down_Right_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 44
}
},
"DamageEffects": {
"WorldSoundEventId": "SFX_Battleaxe_T2_Impact",
"LocalSoundEventId": "SFX_Battleaxe_T2_Impact"
}
}
]
},
"Downstrike_Damage": {
"Interactions": [
{
"Parent": "Weapon_Battleaxe_Primary_Downstrike_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 57
}
},
"EntityStatsOnHit": [
{
"EntityStatId": "SignatureEnergy",
"Amount": 2
}
],
"DamageEffects": {
"WorldSoundEventId": "SFX_Battleaxe_T2_Impact",
"LocalSoundEventId": "SFX_Battleaxe_T2_Impact"
}
}
]
},
"Signature_Whirlwind_Damage": {
"Interactions": [
{
"Parent": "Weapon_Battleaxe_Signature_Whirlwind_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 29
}
},
"EntityStatsOnHit": [],
"DamageEffects": {
"WorldSoundEventId": "SFX_Battleaxe_T2_Impact",
"LocalSoundEventId": "SFX_Battleaxe_T2_Impact"
}
}
]
},
"Guard_Wield": {
"Interactions": [
{
"Parent": "Weapon_Battleaxe_Secondary_Guard_Wield",
"StaminaCost": {
"Value": 20,
"CostType": "Damage"
}
}
]
}
},
"MaxDurability": 220,
"DurabilityLossOnHit": 0.45,
"Interactions": {
"Ability1": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 1
}
]
},
"Ability2": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 2
}
]
},
"Ability3": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 3
}
]
}
}
}
@@ -0,0 +1,88 @@
{
"Parent": "Template_Weapon_Crossbow",
"TranslationProperties": {
"Name": "server.items.Weapon_Crossbow_Ancient_Steel.name"
},
"Model": "Items/Weapons/Crossbow/Iron.blockymodel",
"Texture": "Items/Weapons/Crossbow/Iron_Rusty_Texture.png",
"Icon": "Icons/ItemsGenerated/Weapon_Crossbow_Iron_Rusty.png",
"Quality": "Rare",
"ItemLevel": 30,
"InteractionVars": {
"Standard_Projectile_Damage": {
"Interactions": [
{
"Parent": "Weapon_Crossbow_Damage_Standard_Projectile",
"DamageCalculator": {
"BaseDamage": {
"Projectile": 16
}
}
}
]
},
"Combo_Projectile_Damage": {
"Interactions": [
{
"Parent": "Weapon_Crossbow_Damage_Combo_Projectile",
"DamageCalculator": {
"BaseDamage": {
"Projectile": 42
}
}
}
]
},
"Signature_BigArrow_Damage": {
"Interactions": [
{
"Parent": "Weapon_Crossbow_Damage_Signature_Projectile",
"DamageCalculator": {
"BaseDamage": {
"Projectile": 123
}
}
}
]
},
"Guard_Wield": {
"Interactions": [
{
"Parent": "Weapon_Crossbow_Secondary_Guard_Wield",
"StaminaCost": {
"Value": 15.56,
"CostType": "Damage"
}
}
]
}
},
"MaxDurability": 180,
"DurabilityLossOnHit": 0.28,
"Interactions": {
"Ability1": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 1
}
]
},
"Ability2": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 2
}
]
},
"Ability3": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 3
}
]
}
}
}
@@ -0,0 +1,463 @@
{
"Parent": "Template_Weapon_Daggers",
"TranslationProperties": {
"Name": "server.items.Weapon_Daggers_Mithril.name"
},
"Model": "Items/Weapons/Dagger/Mithril.blockymodel",
"Texture": "Items/Weapons/Dagger/Mithril_Texture.png",
"Icon": "Icons/ItemsGenerated/Weapon_Daggers_Mithril.png",
"Recipe": {
"TimeSeconds": 5,
"KnowledgeRequired": false,
"Input": [
{
"ItemId": "Ingredient_Bar_Mithril",
"Quantity": 10
},
{
"ItemId": "Ingredient_Leather_Storm",
"Quantity": 3
},
{
"ItemId": "Ingredient_Voidheart",
"Quantity": 1
}
],
"BenchRequirement": [
{
"Type": "Crafting",
"Categories": [
"Weapon_Daggers"
],
"Id": "Weapon_Bench",
"RequiredTierLevel": 3
}
]
},
"Quality": "Epic",
"ItemLevel": 50,
"InteractionVars": {
"Swing_Left_Damage": {
"Interactions": [
{
"Parent": "Weapon_Daggers_Primary_Swing_Left_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 8
}
},
"DamageEffects": {
"WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact",
"LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact"
},
"AngledDamage": []
}
]
},
"Swing_Right_Damage": {
"Interactions": [
{
"Parent": "Weapon_Daggers_Primary_Swing_Right_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 8
}
},
"DamageEffects": {
"WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact",
"LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact"
},
"AngledDamage": []
}
]
},
"Stab_Left_Damage": {
"Interactions": [
{
"Parent": "Weapon_Daggers_Primary_Stab_Left_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 15
}
},
"DamageEffects": {
"WorldSoundEventId": "SFX_Daggers_T2_Stab_Impact",
"LocalSoundEventId": "SFX_Daggers_T2_Stab_Impact"
},
"AngledDamage": []
}
]
},
"Stab_Right_Damage": {
"Interactions": [
{
"Parent": "Weapon_Daggers_Primary_Stab_Right_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 25
}
},
"DamageEffects": {
"WorldSoundEventId": "SFX_Daggers_T2_Stab_Impact",
"LocalSoundEventId": "SFX_Daggers_T2_Stab_Impact"
},
"AngledDamage": []
}
]
},
"Pounce_Sweep_Damage": {
"Interactions": [
{
"Parent": "Weapon_Daggers_Primary_Pounce_Sweep_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 61
}
},
"EntityStatsOnHit": [
{
"EntityStatId": "SignatureEnergy",
"Amount": 1
}
],
"DamageEffects": {
"WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact",
"LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact"
},
"AngledDamage": [
{
"Angle": 180,
"AngleDistance": 80,
"DamageEffects": {
"Knockback": {
"Direction": {
"X": 0,
"Y": 2,
"Z": -3
},
"Type": "Force",
"Force": 6,
"VelocityType": "Set",
"VelocityConfig": {
"AirResistance": 0.99,
"AirResistanceMax": 0.98,
"GroundResistance": 0.94,
"GroundResistanceMax": 0.3,
"Threshold": 3.0,
"Style": "Linear"
}
},
"WorldParticles": [
{
"SystemId": "Impact_Critical",
"Scale": 1,
"RotationOffset": {
"Yaw": 180,
"Pitch": 0,
"Roll": 0
}
}
],
"LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact",
"WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact"
},
"DamageCalculator": {
"BaseDamage": {
"Physical": 92
}
}
}
]
}
]
},
"Pounce_Stab_Damage": {
"Interactions": [
{
"Parent": "Weapon_Daggers_Primary_Pounce_Stab_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 76
}
},
"EntityStatsOnHit": [
{
"EntityStatId": "SignatureEnergy",
"Amount": 6
}
],
"DamageEffects": {
"WorldSoundEventId": "SFX_Daggers_T2_Stab_Impact",
"LocalSoundEventId": "SFX_Daggers_T2_Stab_Impact"
},
"AngledDamage": [
{
"Angle": 180,
"AngleDistance": 80,
"DamageEffects": {
"Knockback": {
"Direction": {
"X": 0,
"Y": 1,
"Z": -2
},
"Type": "Force",
"Force": 5,
"VelocityType": "Set",
"VelocityConfig": {
"AirResistance": 0.99,
"AirResistanceMax": 0.98,
"GroundResistance": 0.94,
"GroundResistanceMax": 0.3,
"Threshold": 3.0,
"Style": "Linear"
}
},
"WorldParticles": [
{
"SystemId": "Impact_Critical",
"Scale": 1,
"RotationOffset": {
"Yaw": 180,
"Pitch": 0,
"Roll": 0
}
}
],
"LocalSoundEventId": "SFX_Daggers_T2_Stab_Impact",
"WorldSoundEventId": "SFX_Daggers_T2_Stab_Impact"
},
"DamageCalculator": {
"BaseDamage": {
"Physical": 114
}
}
}
]
}
]
},
"Razorstrike_Slash_Damage": {
"Interactions": [
{
"Parent": "Weapon_Daggers_Signature_Razorstrike_Slash_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 49
}
},
"EntityStatsOnHit": [],
"DamageEffects": {
"WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact",
"LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact"
},
"AngledDamage": [
{
"Angle": 180,
"AngleDistance": 80,
"DamageEffects": {
"Knockback": {
"Direction": {
"X": 0,
"Y": 1,
"Z": -2
},
"Type": "Force",
"Force": 7,
"VelocityType": "Add",
"VelocityConfig": {
"AirResistance": 0.99,
"AirResistanceMax": 0.98,
"GroundResistance": 0.94,
"GroundResistanceMax": 0.3,
"Threshold": 3.0,
"Style": "Linear"
}
},
"WorldParticles": [
{
"SystemId": "Impact_Critical",
"Scale": 1,
"RotationOffset": {
"Yaw": 180,
"Pitch": 0,
"Roll": 0
}
}
],
"LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact",
"WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact"
},
"DamageCalculator": {
"BaseDamage": {
"Physical": 74
}
}
}
]
}
]
},
"Razorstrike_Sweep_Damage": {
"Interactions": [
{
"Parent": "Weapon_Daggers_Signature_Razorstrike_Sweep_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 61
}
},
"EntityStatsOnHit": [],
"DamageEffects": {
"WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact",
"LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact"
},
"AngledDamage": [
{
"Angle": 180,
"AngleDistance": 80,
"DamageEffects": {
"Knockback": {
"Direction": {
"X": 0,
"Y": 1,
"Z": -1
},
"Type": "Force",
"Force": 7,
"VelocityType": "Add",
"VelocityConfig": {
"AirResistance": 0.99,
"AirResistanceMax": 0.98,
"GroundResistance": 0.94,
"GroundResistanceMax": 0.3,
"Threshold": 3.0,
"Style": "Linear"
}
},
"WorldParticles": [
{
"SystemId": "Impact_Critical",
"Scale": 1,
"RotationOffset": {
"Yaw": 180,
"Pitch": 0,
"Roll": 0
}
}
],
"LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact",
"WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact"
},
"DamageCalculator": {
"BaseDamage": {
"Physical": 92
}
}
}
]
}
]
},
"Razorstrike_Lunge_Damage": {
"Interactions": [
{
"Parent": "Weapon_Daggers_Signature_Razorstrike_Lunge_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 73
}
},
"EntityStatsOnHit": [],
"DamageEffects": {
"WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact",
"LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact"
},
"AngledDamage": [
{
"Angle": 180,
"AngleDistance": 80,
"DamageEffects": {
"Knockback": {
"Direction": {
"X": 0,
"Y": 2,
"Z": -3
},
"Type": "Force",
"Force": 12,
"VelocityType": "Set",
"VelocityConfig": {
"AirResistance": 0.99,
"AirResistanceMax": 0.98,
"GroundResistance": 0.94,
"GroundResistanceMax": 0.3,
"Threshold": 3.0,
"Style": "Linear"
}
},
"WorldParticles": [
{
"SystemId": "Impact_Critical",
"Scale": 1,
"RotationOffset": {
"Yaw": 180,
"Pitch": 0,
"Roll": 0
}
}
],
"LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact",
"WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact"
},
"DamageCalculator": {
"BaseDamage": {
"Physical": 110
}
}
}
]
}
]
},
"Guard_Wield": {
"Interactions": [
{
"Parent": "Weapon_Daggers_Secondary_Guard_Wield",
"StaminaCost": {
"Value": 20,
"CostType": "Damage"
}
}
]
}
},
"MaxDurability": 220,
"DurabilityLossOnHit": 0.1,
"Interactions": {
"Ability1": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 1
}
]
},
"Ability2": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 2
}
]
},
"Ability3": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 3
}
]
}
}
}
@@ -0,0 +1,167 @@
{
"TranslationProperties": {
"Name": "server.items.Weapon_Longsword_Mithril.name"
},
"Categories": [
"Items.Weapons"
],
"Quality": "Epic",
"ItemLevel": 50,
"Model": "Items/Weapons/Longsword/Mithril.blockymodel",
"Texture": "Items/Weapons/Longsword/Mithril_Texture.png",
"PlayerAnimationsId": "Longsword",
"Interactions": {
"Primary": "Longsword_Attack",
"Ability1": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 1
}
]
},
"Ability2": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 2
}
]
},
"Ability3": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 3
}
]
}
},
"InteractionVars": {
"Longsword_Swing_Left_Effect": {
"Interactions": [
{
"Parent": "Longsword_Swing_Left_Effect",
"Effects": {
"WorldSoundEventId": "SFX_Longsword_Special_Swing"
}
}
]
},
"Longsword_Swing_Left_Damage": {
"Interactions": [
{
"Parent": "Longsword_Swing_Left_Damage",
"DamageCalculator": {
"Type": "Absolute",
"BaseDamage": {
"Physical": 70
},
"RandomPercentageModifier": 0.15
},
"DamageEffects": {
"WorldSoundEventId": "SFX_Longsword_Special_Impact"
}
}
]
},
"Longsword_Swing_Right_Effect": {
"Interactions": [
{
"Parent": "Longsword_Swing_Left_Effect",
"Effects": {
"WorldSoundEventId": "SFX_Longsword_Special_Swing"
}
}
]
},
"Longsword_Swing_Right_Damage": {
"Interactions": [
{
"Parent": "Longsword_Swing_Right_Damage",
"DamageCalculator": {
"Type": "Absolute",
"BaseDamage": {
"Physical": 70
},
"RandomPercentageModifier": 0.15
},
"DamageEffects": {
"WorldSoundEventId": "SFX_Longsword_Special_Impact"
}
}
]
},
"Longsword_Swing_Up_Left_Effect": {
"Interactions": [
{
"Parent": "Longsword_Swing_Left_Effect",
"Effects": {
"WorldSoundEventId": "SFX_Longsword_Special_Swing"
}
}
]
},
"Longsword_Swing_Up_Left_Damage": {
"Interactions": [
{
"Parent": "Longsword_Swing_Up_Left_Damage",
"DamageCalculator": {
"Type": "Absolute",
"BaseDamage": {
"Physical": 70
},
"RandomPercentageModifier": 0.15
},
"DamageEffects": {
"WorldSoundEventId": "SFX_Longsword_Special_Impact"
}
}
]
},
"Longsword_Stab_Charged_Effect": "Longsword_Stab_Charged_Effect",
"Longsword_Stab_Charged_Damage": {
"Interactions": [
{
"Parent": "Longsword_Stab_Charged_Damage",
"DamageCalculator": {
"BaseDamage": {
"Physical": 210
},
"Type": "Absolute",
"RandomPercentageModifier": 0.15
},
"DamageEffects": {
"WorldSoundEventId": "SFX_Longsword_Special_Impact"
}
}
]
}
},
"DroppedItemAnimation": "Items/Animations/Dropped/Dropped_Diagonal_Left.blockyanim",
"IconProperties": {
"Scale": 0.31,
"Translation": [
-47,
-47
],
"Rotation": [
45,
90,
0
]
},
"Icon": "Icons/ItemsGenerated/Weapon_Longsword_Mithril.png",
"Tags": {
"Type": [
"Weapon"
],
"Family": [
"Longsword"
]
},
"MaxDurability": 160,
"DurabilityLossOnHit": 0.52,
"Weapon": {},
"ItemSoundSetId": "ISS_Weapons_Blade_Large"
}
@@ -0,0 +1,322 @@
{
"TranslationProperties": {
"Name": "server.items.Weapon_Shortbow_Cobalt.name"
},
"Parent": "Template_Weapon_Shortbow",
"Quality": "Rare",
"Model": "Items/Weapons/Bow/Cobalt.blockymodel",
"Texture": "Items/Weapons/Bow/Cobalt_Texture.png",
"Icon": "Icons/ItemsGenerated/Weapon_Bow_Cobalt.png",
"ItemLevel": 35,
"Recipe": {
"TimeSeconds": 4,
"KnowledgeRequired": false,
"Input": [
{
"ItemId": "Ingredient_Bar_Cobalt",
"Quantity": 10
},
{
"ItemId": "Ingredient_Leather_Heavy",
"Quantity": 2
},
{
"ItemId": "Ingredient_Fabric_Scrap_Shadoweave",
"Quantity": 3
}
],
"BenchRequirement": [
{
"Type": "Crafting",
"Categories": [
"Weapon_Bow"
],
"Id": "Weapon_Bench",
"RequiredTierLevel": 2
},
{
"Type": "DiagramCrafting",
"Categories": [
"Weapons.Bow"
],
"Id": "Armory"
}
]
},
"InteractionVars": {
"Primary_Shoot_Damage_Strength_0": {
"Interactions": [
{
"Parent": "Weapon_Shortbow_Primary_Shoot_Damage_Strength_0",
"DamageCalculator": {
"BaseDamage": {
"Projectile": 7
}
}
}
]
},
"Primary_Shoot_Damage_Strength_1": {
"Interactions": [
{
"Parent": "Weapon_Shortbow_Primary_Shoot_Damage_Strength_1",
"DamageCalculator": {
"BaseDamage": {
"Projectile": 11
}
}
}
]
},
"Primary_Shoot_Damage_Strength_2": {
"Interactions": [
{
"Parent": "Weapon_Shortbow_Primary_Shoot_Damage_Strength_2",
"DamageCalculator": {
"BaseDamage": {
"Projectile": 15
}
}
}
]
},
"Primary_Shoot_Damage_Strength_3": {
"Interactions": [
{
"Parent": "Weapon_Shortbow_Primary_Shoot_Damage_Strength_3",
"DamageCalculator": {
"BaseDamage": {
"Projectile": 20
}
}
}
]
},
"Primary_Shoot_Damage_Strength_4": {
"Interactions": [
{
"Parent": "Weapon_Shortbow_Primary_Shoot_Damage_Strength_4",
"DamageCalculator": {
"BaseDamage": {
"Projectile": 23
}
},
"TargetedDamage": {
"Head": {
"DamageCalculator": {
"BaseDamage": {
"Projectile": 30
}
},
"DamageEffects": {
"Knockback": {
"Type": "Force",
"VelocityConfig": {
"AirResistance": 0.99,
"AirResistanceMax": 0.98,
"GroundResistance": 0.94,
"GroundResistanceMax": 0.3,
"Threshold": 3.0,
"Style": "Linear"
},
"Direction": {
"X": 0.0,
"Y": 0.5,
"Z": -1
},
"Force": 8,
"VelocityType": "Set"
},
"WorldParticles": [
{
"SystemId": "Impact_Critical",
"Scale": 1,
"RotationOffset": {
"Yaw": 180,
"Pitch": 0,
"Roll": 0
}
}
]
}
}
}
}
]
},
"Signature_Volley_Damage": {
"Interactions": [
{
"Parent": "Weapon_Shortbow_Signature_Volley_Damage",
"DamageCalculator": {
"BaseDamage": {
"Projectile": 17
}
}
}
]
},
"Guard_Wield": {
"Interactions": [
{
"Parent": "Weapon_Shortbow_Secondary_Guard_Wield",
"StaminaCost": {
"Value": 12.22,
"CostType": "Damage"
}
}
]
}
},
"ItemAppearanceConditions": {
"SignatureCharges": [
{
"Condition": [
1,
100
],
"ConditionValueType": "Percent",
"Model": "Items/Weapons/Bow/Cobalt_Triple.blockymodel",
"Texture": "Items/Weapons/Bow/Cobalt_Texture.png",
"LocalSoundEventId": "SFX_Bow_T2_Signature_Loop_Local",
"WorldSoundEventId": "SFX_Bow_T2_Signature_Loop",
"Particles": [
{
"SystemId": "Bow_Signature_Charge",
"TargetNodeName": "ARROW-PLACEHOLDER",
"PositionOffset": {
"X": 0.5,
"Y": 0.25,
"Z": 0
},
"Scale": 0.5,
"TargetEntityPart": "PrimaryItem"
},
{
"SystemId": "Bow_Signature_Charge",
"TargetNodeName": "ARROW-PLACEHOLDER",
"PositionOffset": {
"X": 0.5,
"Y": 0,
"Z": 0
},
"Scale": 0.5,
"TargetEntityPart": "PrimaryItem"
},
{
"SystemId": "Bow_Signature_Charge",
"TargetNodeName": "ARROW-PLACEHOLDER",
"PositionOffset": {
"X": 0.5,
"Y": -0.25,
"Z": 0
},
"Scale": 0.5,
"TargetEntityPart": "PrimaryItem"
}
],
"ModelVFXId": "Bow_Signature_Status"
}
],
"SignatureEnergy": [
{
"Condition": [
100,
100
],
"ConditionValueType": "Percent",
"Particles": [
{
"SystemId": "Sword_Signature_Ready",
"TargetNodeName": "Handle",
"PositionOffset": {
"X": 0.8
},
"TargetEntityPart": "PrimaryItem"
},
{
"SystemId": "Sword_Signature_Status_Spawn",
"TargetEntityPart": "PrimaryItem"
},
{
"SystemId": "Bow_Signature_Status",
"TargetNodeName": "Bow-Top",
"PositionOffset": {
"X": 0.55
},
"TargetEntityPart": "PrimaryItem",
"Scale": 1.3
},
{
"SystemId": "Bow_Signature_Status",
"TargetNodeName": "Bow-Bot",
"TargetEntityPart": "PrimaryItem",
"Scale": 1.3
}
],
"FirstPersonParticles": [
{
"SystemId": "Sword_Signature_Ready",
"TargetNodeName": "Handle",
"PositionOffset": {
"X": 0.8
},
"TargetEntityPart": "PrimaryItem"
},
{
"SystemId": "Sword_Signature_Status_Spawn",
"TargetNodeName": "Handle",
"TargetEntityPart": "PrimaryItem",
"PositionOffset": {
"Y": 0
}
},
{
"SystemId": "Bow_Signature_Status_FP",
"TargetNodeName": "Bow-Top",
"PositionOffset": {
"X": 0.5
},
"TargetEntityPart": "PrimaryItem"
}
]
}
]
},
"MaxDurability": 150,
"DurabilityLossOnHit": 0.58,
"Interactions": {
"Primary": {
"RequireNewClick": true,
"Interactions": [
{
"Type": "mmorpg_archer_shoot"
}
]
},
"Ability1": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 1
}
]
},
"Ability2": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 2
}
]
},
"Ability3": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 3
}
]
}
}
}
@@ -0,0 +1,94 @@
{
"TranslationProperties": {
"Name": "server.items.Weapon_Staff_Mithril.name"
},
"Categories": [
"Items.Weapons"
],
"Quality": "Epic",
"ItemLevel": 50,
"Model": "Items/Weapons/Staff/Mithril.blockymodel",
"Texture": "Items/Weapons/Staff/Mithril_Texture.png",
"PlayerAnimationsId": "Staff",
"Interactions": {
"Primary": "Staff_Primary",
"Secondary": "Staff_Primary",
"Ability1": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 1
}
]
},
"Ability2": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 2
}
]
},
"Ability3": {
"Interactions": [
{
"Type": "mmorpg_cast_ability",
"Slot": 3
}
]
}
},
"InteractionVars": {
"Staff_Cast_Summon_Charged": {
"Interactions": [
{
"Parent": "Staff_Cast_Summon_Charged",
"Costs": {
"Mana": 50
}
}
]
},
"Staff_Cast_Summon_Cost": {
"Interactions": [
{
"Parent": "Staff_Cast_Cost",
"StatModifiers": {
"Mana": -50
}
}
]
},
"Staff_Cast_Summon_Launch": "Staff_Cast_Launch",
"Staff_Cast_Summon_Effect": "Staff_Cast_Effect",
"Staff_Cast_Summon_Fail": "Staff_Cast_Fail",
"Spear_Swing_Left_Damage": "Spear_Swing_Left_Damage",
"Spear_Swing_Right_Damage": "Spear_Swing_Right_Damage",
"Spear_Swing_Left_Effect": "Spear_Swing_Left_Effect",
"Spear_Swing_Right_Effect": "Spear_Swing_Right_Effect"
},
"IconProperties": {
"Scale": 0.28,
"Translation": [
-52,
-52
],
"Rotation": [
22.5,
45,
315
]
},
"DroppedItemAnimation": "Items/Animations/Dropped/Dropped_Diagonal_Left.blockyanim",
"Icon": "Icons/ItemsGenerated/Weapon_Staff_Mithril.png",
"Tags": {
"Type": [
"Weapon"
],
"Family": [
"Staff"
]
},
"Weapon": {},
"ItemSoundSetId": "ISS_Weapons_Wood"
}
@@ -0,0 +1,10 @@
{
"Interactions": [
"Mmorpg_Archer_Projectile"
],
"Tags": {
"Attack": [
"Ranged"
]
}
}
+15 -6
View File
@@ -1,9 +1,18 @@
{ {
"Group": "ScaffoldIt", "Group": "com.disklexar",
"Name": "Hytalemmorpg", "Name": "MMORPG",
"Version": "0.0.0", "Version": "0.4.8",
"Authors": [], "Description": "Serveur MMORPG Hytale — progression, persistance et systèmes sociaux",
"Authors": [
{
"Name": "Disklexar"
}
],
"Website": "https://github.com/disklexar/hytale-mmorpg",
"DisabledByDefault": false, "DisabledByDefault": false,
"IncludesAssetPack": false, "IncludesAssetPack": true,
"Main": "scaffoldit.hytalemmorpg.Hytalemmorpg" "Dependencies": {},
"OptionalDependencies": {},
"ServerVersion": "*",
"Main": "com.disklexar.mmorpg.MmorpgPlugin"
} }
+80
View File
@@ -100,6 +100,83 @@ reset_devserver_if_needed() {
fi fi
} }
sync_third_party_plugins() {
local src="$ROOT/plugins"
local dest="$ROOT/devserver/mods"
local jar
[[ -d "$src" ]] || return 0
mkdir -p "$dest"
shopt -s nullglob
for jar in "$src"/*.jar; do
cp -u "$jar" "$dest/"
echo "Plugin tiers: $(basename "$jar") -> devserver/mods/"
done
shopt -u nullglob
}
apply_world_spawn() {
local world_config="$ROOT/devserver/universe/worlds/default/config.json"
local spawn_template="$ROOT/config/universe/worlds/default/spawn-provider.json"
local players_dir="$ROOT/devserver/universe/players"
[[ -f "$spawn_template" ]] || return 0
[[ -f "$world_config" ]] || return 0
python3 - "$spawn_template" "$world_config" "$players_dir" <<'PY'
import json
import sys
from pathlib import Path
spawn_template = Path(sys.argv[1])
world_config = Path(sys.argv[2])
players_dir = Path(sys.argv[3])
patch = json.loads(spawn_template.read_text(encoding="utf-8"))
config = json.loads(world_config.read_text(encoding="utf-8"))
config.update(patch)
world_config.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
spawn = patch["SpawnProvider"]["SpawnPoint"]
last_position = {
"X": spawn["X"],
"Y": spawn["Y"],
"Z": spawn["Z"],
"Pitch": spawn.get("Pitch", 0.0),
"Yaw": spawn.get("Yaw", 0.0),
"Roll": spawn.get("Roll", 0.0),
}
position = {"X": spawn["X"], "Y": spawn["Y"], "Z": spawn["Z"]}
rotation = {
"Pitch": spawn.get("Pitch", 0.0),
"Yaw": spawn.get("Yaw", 0.0),
"Roll": spawn.get("Roll", 0.0),
}
if players_dir.is_dir():
for player_file in players_dir.glob("*.json"):
data = json.loads(player_file.read_text(encoding="utf-8"))
components = data.get("Components", {})
transform = components.get("Transform")
if isinstance(transform, dict):
transform["Position"] = dict(position)
transform["Rotation"] = dict(rotation)
player_data = components.get("Player", {}).get("PlayerData", {})
per_world = player_data.get("PerWorldData", {}).get("default")
if isinstance(per_world, dict):
per_world["LastPosition"] = dict(last_position)
player_file.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
print(
f"Spawn default: X={spawn['X']}, Y={spawn['Y']}, Z={spawn['Z']} "
f"(world config + joueurs existants)"
)
PY
}
run_gradle() { run_gradle() {
"$ROOT/gradlew" --stop 2>/dev/null || true "$ROOT/gradlew" --stop 2>/dev/null || true
"$ROOT/gradlew" "${GRADLE_ARGS[@]}" "$@" "$ROOT/gradlew" "${GRADLE_ARGS[@]}" "$@"
@@ -116,9 +193,12 @@ reset_devserver_if_needed
echo "Configuration du serveur de dev..." echo "Configuration du serveur de dev..."
run_gradle setupServer run_gradle setupServer
sync_third_party_plugins
echo "Compilation..." echo "Compilation..."
run_gradle build run_gradle build
apply_world_spawn
echo "Démarrage du serveur (logs: devserver/logs/)..." echo "Démarrage du serveur (logs: devserver/logs/)..."
exec "$ROOT/gradlew" "${GRADLE_ARGS[@]}" runServer exec "$ROOT/gradlew" "${GRADLE_ARGS[@]}" runServer
+15
View File
@@ -281,6 +281,21 @@ code {
color: #94a3b8; color: #94a3b8;
} }
.tag-alive {
background: rgba(74, 222, 128, 0.15);
color: var(--success);
}
.tag-dead {
background: rgba(248, 113, 113, 0.15);
color: var(--danger);
}
.death-date {
display: block;
margin-top: 0.2rem;
}
.link { .link {
color: var(--accent); color: var(--accent);
text-decoration: none; text-decoration: none;
+6
View File
@@ -140,6 +140,8 @@ function enrichPlayer(player) {
); );
const jobs = labels.parseJsonArray(player.jobs).map(labels.labelJob); const jobs = labels.parseJsonArray(player.jobs).map(labels.labelJob);
const powers = labels.parseJsonArray(player.powers).map(labels.labelPower); const powers = labels.parseJsonArray(player.powers).map(labels.labelPower);
const deathTime = player.death_time ?? null;
const isDead = deathTime != null && Number(deathTime) > 0;
return { return {
...player, ...player,
@@ -148,6 +150,10 @@ function enrichPlayer(player) {
jobs, jobs,
powers, powers,
isOnline: player.is_connected === 1, isOnline: player.is_connected === 1,
isDead,
lifeStatus: isDead ? "Mort" : "Vivant",
deathTimeFormatted: isDead ? fmt.formatTimestamp(deathTime) : null,
total_time_play: player.total_time_play ?? 0,
}; };
} }
+9 -3
View File
@@ -108,7 +108,9 @@ function getCharactersByAccountUuid(accountUuid) {
money, money,
race_id, race_id,
date_creation, date_creation,
updated_at updated_at,
death_time,
total_time_play
FROM characters FROM characters
WHERE account_uuid = ? WHERE account_uuid = ?
ORDER BY name COLLATE NOCASE ASC ORDER BY name COLLATE NOCASE ASC
@@ -225,7 +227,9 @@ function getAllPlayers() {
c.race_id, c.race_id,
c.money, c.money,
p.is_connected, p.is_connected,
p.total_time_play, c.total_time_play,
p.total_time_play AS account_total_time_play,
c.death_time,
p.last_date_connected, p.last_date_connected,
c.date_creation c.date_creation
FROM characters c FROM characters c
@@ -266,7 +270,9 @@ function getPlayerByUuid(uuid) {
c.guild_id, c.guild_id,
c.group_id, c.group_id,
p.is_connected, p.is_connected,
p.total_time_play, COALESCE(c.total_time_play, 0) AS total_time_play,
p.total_time_play AS account_total_time_play,
c.death_time,
p.last_date_connected, p.last_date_connected,
COALESCE(c.date_creation, p.date_creation) AS date_creation, COALESCE(c.date_creation, p.date_creation) AS date_creation,
COALESCE(c.money, 0) AS money, COALESCE(c.money, 0) AS money,
+15
View File
@@ -20,6 +20,20 @@ function formatDuration(seconds) {
return `${hours} h ${minutes} min`; return `${hours} h ${minutes} min`;
} }
/** Play time stored in milliseconds (characters.total_time_play). */
function formatPlayTime(millis) {
if (!millis || millis <= 0) {
return "0 min";
}
const totalSeconds = Math.floor(millis / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
if (hours === 0) {
return `${minutes} min`;
}
return `${hours} h ${minutes} min`;
}
function formatMoney(value) { function formatMoney(value) {
return new Intl.NumberFormat("fr-FR").format(value ?? 0); return new Intl.NumberFormat("fr-FR").format(value ?? 0);
} }
@@ -31,6 +45,7 @@ function formatNumber(value) {
module.exports = { module.exports = {
formatTimestamp, formatTimestamp,
formatDuration, formatDuration,
formatPlayTime,
formatMoney, formatMoney,
formatNumber, formatNumber,
}; };
+5 -4
View File
@@ -32,13 +32,14 @@
<td><%= p.raceLabel %></td> <td><%= p.raceLabel %></td>
<td><%= fmt.formatMoney(p.money) %></td> <td><%= fmt.formatMoney(p.money) %></td>
<td> <td>
<% if (p.isOnline) { %> <% if (p.isDead) { %>
<span class="tag tag-online">En ligne</span> <span class="tag tag-dead">Mort</span>
<small class="muted death-date"><%= p.deathTimeFormatted %></small>
<% } else { %> <% } else { %>
<span class="tag tag-offline">Hors ligne</span> <span class="tag tag-alive">Vivant</span>
<% } %> <% } %>
</td> </td>
<td><%= fmt.formatDuration(p.total_time_play) %></td> <td><%= fmt.formatPlayTime(p.total_time_play) %></td>
</tr> </tr>
<% }); %> <% }); %>
</tbody> </tbody>