@@ -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);
|
||||||
|
|
||||||
|
|||||||
+80
-25
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+160
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-2
@@ -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>
|
||||||
|
|||||||
+49
-1
@@ -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());
|
||||||
|
|||||||
+82
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+99
-13
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
-21
@@ -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)));
|
||||||
|
|||||||
+2
-2
@@ -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]
|
||||||
|
|||||||
+9
-36
@@ -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,
|
||||||
|
|||||||
+98
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
-5
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -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() {
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-13
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+314
-11
@@ -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"));
|
||||||
|
|||||||
+38
-6
@@ -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) {
|
||||||
|
|||||||
+61
-1
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+73
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+110
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+161
@@ -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) {
|
||||||
|
|||||||
+24
-132
@@ -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 {
|
||||||
|
|||||||
+5
-40
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -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.";
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
@@ -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": [
|
||||||
{
|
{
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"Interactions": [
|
||||||
|
"Mmorpg_Archer_Projectile"
|
||||||
|
],
|
||||||
|
"Tags": {
|
||||||
|
"Attack": [
|
||||||
|
"Ranged"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -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.";
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
@@ -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"
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"Interactions": [
|
||||||
|
"Mmorpg_Archer_Projectile"
|
||||||
|
],
|
||||||
|
"Tags": {
|
||||||
|
"Attack": [
|
||||||
|
"Ranged"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user