From 10ff1244668ef9b4cb78ecf235ccfb952de03736 Mon Sep 17 00:00:00 2001 From: Warren H Date: Fri, 17 Jul 2026 12:47:42 -0400 Subject: [PATCH] new rpg mode --- README.md | 26 + .../iwanttoheal/ControllerBridgeActivity.java | 26 +- db/schema.sql | 74 + server/game-api.mjs | 438 +++- server/game-api.test.mjs | 159 +- src/App.tsx | 124 +- .../claudecraft/weapons/adv_axe_1handed.glb | Bin 0 -> 28332 bytes .../claudecraft/weapons/adv_axe_2handed.glb | Bin 0 -> 37764 bytes .../models/claudecraft/weapons/adv_staff.glb | Bin 0 -> 34680 bytes .../claudecraft/weapons/adv_sword_2handed.glb | Bin 0 -> 34208 bytes .../weapons/adv_sword_2handed_color.glb | Bin 0 -> 34188 bytes .../claudecraft/weapons/axe_1handed.glb | Bin 0 -> 10336 bytes .../claudecraft/weapons/axe_2handed.glb | Bin 0 -> 13196 bytes .../game/models/claudecraft/weapons/axe_a.glb | Bin 0 -> 65876 bytes .../game/models/claudecraft/weapons/axe_b.glb | Bin 0 -> 68320 bytes .../game/models/claudecraft/weapons/axe_c.glb | Bin 0 -> 55336 bytes .../game/models/claudecraft/weapons/axe_d.glb | Bin 0 -> 59072 bytes .../claudecraft/weapons/crossbow_1handed.glb | Bin 0 -> 14828 bytes .../models/claudecraft/weapons/dagger.glb | Bin 0 -> 9580 bytes .../models/claudecraft/weapons/dagger_a.glb | Bin 0 -> 36344 bytes .../models/claudecraft/weapons/dagger_b.glb | Bin 0 -> 38868 bytes .../models/claudecraft/weapons/dagger_c.glb | Bin 0 -> 52512 bytes .../models/claudecraft/weapons/halberd.glb | Bin 0 -> 82980 bytes .../models/claudecraft/weapons/hammer_a.glb | Bin 0 -> 42108 bytes .../models/claudecraft/weapons/hammer_b.glb | Bin 0 -> 47848 bytes .../models/claudecraft/weapons/hammer_c.glb | Bin 0 -> 51268 bytes .../models/claudecraft/weapons/hammer_d.glb | Bin 0 -> 92400 bytes .../models/claudecraft/weapons/quiver.glb | Bin 0 -> 11684 bytes .../models/claudecraft/weapons/scythe.glb | Bin 0 -> 67708 bytes .../claudecraft/weapons/shield_round.glb | Bin 0 -> 10096 bytes .../claudecraft/weapons/shield_square.glb | Bin 0 -> 9660 bytes .../claudecraft/weapons/skeleton_axe.glb | Bin 0 -> 14420 bytes .../claudecraft/weapons/skeleton_blade.glb | Bin 0 -> 12988 bytes .../claudecraft/weapons/skeleton_crossbow.glb | Bin 0 -> 15812 bytes .../weapons/skeleton_shield_large_a.glb | Bin 0 -> 17372 bytes .../claudecraft/weapons/skeleton_staff.glb | Bin 0 -> 27580 bytes .../models/claudecraft/weapons/spear_a.glb | Bin 0 -> 71604 bytes .../game/models/claudecraft/weapons/staff.glb | Bin 0 -> 12336 bytes .../models/claudecraft/weapons/staff_a.glb | Bin 0 -> 39884 bytes .../models/claudecraft/weapons/staff_b.glb | Bin 0 -> 51428 bytes .../models/claudecraft/weapons/staff_c.glb | Bin 0 -> 82344 bytes .../models/claudecraft/weapons/staff_d.glb | Bin 0 -> 94300 bytes .../claudecraft/weapons/sword_1handed.glb | Bin 0 -> 10276 bytes .../claudecraft/weapons/sword_2handed.glb | Bin 0 -> 12196 bytes .../models/claudecraft/weapons/sword_a.glb | Bin 0 -> 32360 bytes .../models/claudecraft/weapons/sword_b.glb | Bin 0 -> 39100 bytes .../models/claudecraft/weapons/sword_c.glb | Bin 0 -> 35844 bytes .../models/claudecraft/weapons/sword_d.glb | Bin 0 -> 44844 bytes .../models/claudecraft/weapons/sword_e.glb | Bin 0 -> 48736 bytes .../models/claudecraft/weapons/sword_f.glb | Bin 0 -> 54696 bytes .../models/claudecraft/weapons/sword_g.glb | Bin 0 -> 70608 bytes .../game/models/claudecraft/weapons/wand.glb | Bin 0 -> 8692 bytes .../models/claudecraft/weapons/wand_a.glb | Bin 0 -> 38504 bytes .../models/claudecraft/weapons/wand_b.glb | Bin 0 -> 40396 bytes src/components/BossRoom.tsx | 452 ++++- src/components/BottomScreen.tsx | 416 +++- src/components/BuffDraftPanel.tsx | 9 +- .../CharacterEquipmentAssets.test.ts | 13 + src/components/CharacterEquipmentAssets.ts | 35 + src/components/FrontEnd.tsx | 998 ++++++++-- src/components/GameScene.tsx | 1768 ++++++++++++++-- src/components/HealerClassAccessory.tsx | 115 ++ src/components/ModularCharacterBody.tsx | 114 ++ src/components/TopScreen.tsx | 232 ++- src/components/aetherAssaultVisuals.test.ts | 26 + src/components/aetherAssaultVisuals.ts | 32 + src/components/boss/bossDeathVisuals.test.ts | 9 + src/components/boss/bossDeathVisuals.ts | 2 + .../rpgRoguelike/RpgRoomPortals.tsx | 171 ++ src/components/rpgRoguelike/RpgRunOverlay.tsx | 431 ++++ .../rpgRoguelike/RpgRunTacticalPanel.tsx | 410 ++++ .../rpgRoguelike/RpgRunUiShared.tsx | 325 +++ src/components/rpgRoguelike/index.ts | 4 + src/components/rpgRoguelike/rpgRoguelike.css | 1771 +++++++++++++++++ src/frontend/appearanceStore.test.ts | 90 + src/frontend/data.test.ts | 20 +- src/frontend/data.ts | 65 +- src/frontend/leaderboardCache.test.ts | 73 + src/frontend/leaderboardCache.ts | 145 ++ src/frontend/offlineLeaderboardFlow.test.ts | 42 + src/frontend/onlineRepository.ts | 72 +- src/frontend/profileSections.test.ts | 28 + src/frontend/profileSections.ts | 45 + src/frontend/saveRepository.test.ts | 160 +- src/frontend/saveRepository.ts | 23 +- src/frontend/saveSync.test.ts | 60 + src/frontend/saveSync.ts | 153 ++ src/frontend/store.ts | 350 +++- src/frontend/types.ts | 21 +- src/game/abilityLoadout.test.ts | 46 + src/game/aetherAssault.test.ts | 224 +++ src/game/aetherAssault.ts | 432 ++++ src/game/aetherAssaultStore.test.ts | 124 ++ src/game/appearanceLab.test.ts | 65 + src/game/appearanceLab.ts | 203 ++ src/game/arena.ts | 26 + src/game/blockbreaker.test.ts | 141 ++ src/game/blockbreaker.ts | 394 ++++ src/game/blockbreakerBiomes.test.ts | 31 + src/game/blockbreakerBiomes.ts | 253 +++ src/game/blockbreakerStore.test.ts | 142 ++ src/game/bossAnimation.test.ts | 71 + src/game/bossAnimation.ts | 174 ++ src/game/bossCatalog.test.ts | 19 +- src/game/bossDeath.ts | 22 +- src/game/bossHome.test.ts | 1 + src/game/bossVisuals.ts | 71 +- src/game/bosses/mechanicPool.test.ts | 95 +- src/game/bosses/mechanicPool.ts | 94 +- src/game/bosses/shared.ts | 20 +- src/game/bosses/types.ts | 1 + src/game/bottomTabs.test.ts | 21 + src/game/bottomTabs.ts | 15 + src/game/characterAppearance.test.ts | 116 ++ src/game/characterAppearance.ts | 210 ++ src/game/characterEquipment.test.ts | 37 + src/game/characterEquipment.ts | 24 + src/game/controllerBindings.test.ts | 12 +- src/game/controllerBindings.ts | 18 +- src/game/data.ts | 14 +- src/game/healerClasses.test.ts | 291 +++ src/game/healerEffects.test.ts | 26 + src/game/healerEffects.ts | 195 ++ src/game/healerMechanics.test.ts | 60 + src/game/healerMechanics.ts | 163 ++ src/game/healerRigAssets.test.ts | 93 + src/game/healerVisuals.test.ts | 36 + src/game/healerVisuals.ts | 148 ++ src/game/healers.ts | 172 +- src/game/hockeyHealing.test.ts | 96 + src/game/hockeyHealing.ts | 252 +++ src/game/hockeyHealingPvp.test.ts | 79 + src/game/hockeyHealingPvp.ts | 281 +++ src/game/hockeyHealingPvpStore.test.ts | 197 ++ src/game/hockeyHealingStore.test.ts | 146 ++ src/game/partyBehaviors.test.ts | 12 + src/game/partyBehaviors.ts | 61 +- src/game/partyCombat.test.ts | 39 + src/game/partyCombat.ts | 127 +- src/game/partyState.ts | 10 + src/game/performance.test.ts | 77 +- src/game/progression/gear.ts | 9 +- src/game/progression/gearEffects.ts | 10 + src/game/progression/hunterStats.test.ts | 52 +- src/game/progression/hunterStats.ts | 91 + src/game/progression/infusions.ts | 15 +- src/game/roguelike.ts | 103 +- src/game/rpgRoguelike/challenges.ts | 100 + src/game/rpgRoguelike/combatAdapter.test.ts | 168 ++ src/game/rpgRoguelike/combatAdapter.ts | 264 +++ src/game/rpgRoguelike/index.ts | 9 + src/game/rpgRoguelike/party.ts | 224 +++ src/game/rpgRoguelike/playSpace.test.ts | 71 + src/game/rpgRoguelike/playSpace.ts | 143 ++ src/game/rpgRoguelike/random.ts | 73 + src/game/rpgRoguelike/rewards.ts | 255 +++ src/game/rpgRoguelike/rpgRoguelike.test.ts | 340 ++++ src/game/rpgRoguelike/run.ts | 352 ++++ src/game/rpgRoguelike/spells.test.ts | 45 + src/game/rpgRoguelike/spells.ts | 107 + src/game/rpgRoguelike/types.ts | 278 +++ src/game/rpgRoguelike/uiModel.test.ts | 130 ++ src/game/rpgRoguelike/uiModel.ts | 247 +++ src/game/rpgRoguelikeStore.test.ts | 300 +++ src/game/runModes.ts | 13 + src/game/staffCastGlow.test.ts | 45 + src/game/staffCastGlow.ts | 45 + src/game/store.test.ts | 95 +- src/game/store.ts | 1649 +++++++++++++-- src/game/types.ts | 112 +- src/game/useGameLoop.ts | 47 +- src/game/weaponCatalog.test.ts | 198 ++ src/game/weaponCatalog.ts | 230 +++ src/input/controller.test.ts | 16 + src/input/nativeControllerFocus.test.ts | 22 + src/input/useMenuController.ts | 30 +- src/platform/BottomDisplayApp.tsx | 29 +- src/platform/dualScreenSync.test.ts | 157 +- src/platform/dualScreenSync.ts | 78 +- src/styles.css | 521 ++++- 180 files changed, 20863 insertions(+), 1014 deletions(-) create mode 100644 src/assets/game/models/claudecraft/weapons/adv_axe_1handed.glb create mode 100644 src/assets/game/models/claudecraft/weapons/adv_axe_2handed.glb create mode 100644 src/assets/game/models/claudecraft/weapons/adv_staff.glb create mode 100644 src/assets/game/models/claudecraft/weapons/adv_sword_2handed.glb create mode 100644 src/assets/game/models/claudecraft/weapons/adv_sword_2handed_color.glb create mode 100644 src/assets/game/models/claudecraft/weapons/axe_1handed.glb create mode 100644 src/assets/game/models/claudecraft/weapons/axe_2handed.glb create mode 100644 src/assets/game/models/claudecraft/weapons/axe_a.glb create mode 100644 src/assets/game/models/claudecraft/weapons/axe_b.glb create mode 100644 src/assets/game/models/claudecraft/weapons/axe_c.glb create mode 100644 src/assets/game/models/claudecraft/weapons/axe_d.glb create mode 100644 src/assets/game/models/claudecraft/weapons/crossbow_1handed.glb create mode 100644 src/assets/game/models/claudecraft/weapons/dagger.glb create mode 100644 src/assets/game/models/claudecraft/weapons/dagger_a.glb create mode 100644 src/assets/game/models/claudecraft/weapons/dagger_b.glb create mode 100644 src/assets/game/models/claudecraft/weapons/dagger_c.glb create mode 100644 src/assets/game/models/claudecraft/weapons/halberd.glb create mode 100644 src/assets/game/models/claudecraft/weapons/hammer_a.glb create mode 100644 src/assets/game/models/claudecraft/weapons/hammer_b.glb create mode 100644 src/assets/game/models/claudecraft/weapons/hammer_c.glb create mode 100644 src/assets/game/models/claudecraft/weapons/hammer_d.glb create mode 100644 src/assets/game/models/claudecraft/weapons/quiver.glb create mode 100644 src/assets/game/models/claudecraft/weapons/scythe.glb create mode 100644 src/assets/game/models/claudecraft/weapons/shield_round.glb create mode 100644 src/assets/game/models/claudecraft/weapons/shield_square.glb create mode 100644 src/assets/game/models/claudecraft/weapons/skeleton_axe.glb create mode 100644 src/assets/game/models/claudecraft/weapons/skeleton_blade.glb create mode 100644 src/assets/game/models/claudecraft/weapons/skeleton_crossbow.glb create mode 100644 src/assets/game/models/claudecraft/weapons/skeleton_shield_large_a.glb create mode 100644 src/assets/game/models/claudecraft/weapons/skeleton_staff.glb create mode 100644 src/assets/game/models/claudecraft/weapons/spear_a.glb create mode 100644 src/assets/game/models/claudecraft/weapons/staff.glb create mode 100644 src/assets/game/models/claudecraft/weapons/staff_a.glb create mode 100644 src/assets/game/models/claudecraft/weapons/staff_b.glb create mode 100644 src/assets/game/models/claudecraft/weapons/staff_c.glb create mode 100644 src/assets/game/models/claudecraft/weapons/staff_d.glb create mode 100644 src/assets/game/models/claudecraft/weapons/sword_1handed.glb create mode 100644 src/assets/game/models/claudecraft/weapons/sword_2handed.glb create mode 100644 src/assets/game/models/claudecraft/weapons/sword_a.glb create mode 100644 src/assets/game/models/claudecraft/weapons/sword_b.glb create mode 100644 src/assets/game/models/claudecraft/weapons/sword_c.glb create mode 100644 src/assets/game/models/claudecraft/weapons/sword_d.glb create mode 100644 src/assets/game/models/claudecraft/weapons/sword_e.glb create mode 100644 src/assets/game/models/claudecraft/weapons/sword_f.glb create mode 100644 src/assets/game/models/claudecraft/weapons/sword_g.glb create mode 100644 src/assets/game/models/claudecraft/weapons/wand.glb create mode 100644 src/assets/game/models/claudecraft/weapons/wand_a.glb create mode 100644 src/assets/game/models/claudecraft/weapons/wand_b.glb create mode 100644 src/components/CharacterEquipmentAssets.test.ts create mode 100644 src/components/CharacterEquipmentAssets.ts create mode 100644 src/components/HealerClassAccessory.tsx create mode 100644 src/components/ModularCharacterBody.tsx create mode 100644 src/components/aetherAssaultVisuals.test.ts create mode 100644 src/components/aetherAssaultVisuals.ts create mode 100644 src/components/rpgRoguelike/RpgRoomPortals.tsx create mode 100644 src/components/rpgRoguelike/RpgRunOverlay.tsx create mode 100644 src/components/rpgRoguelike/RpgRunTacticalPanel.tsx create mode 100644 src/components/rpgRoguelike/RpgRunUiShared.tsx create mode 100644 src/components/rpgRoguelike/index.ts create mode 100644 src/components/rpgRoguelike/rpgRoguelike.css create mode 100644 src/frontend/appearanceStore.test.ts create mode 100644 src/frontend/leaderboardCache.test.ts create mode 100644 src/frontend/leaderboardCache.ts create mode 100644 src/frontend/offlineLeaderboardFlow.test.ts create mode 100644 src/frontend/profileSections.test.ts create mode 100644 src/frontend/profileSections.ts create mode 100644 src/frontend/saveSync.test.ts create mode 100644 src/frontend/saveSync.ts create mode 100644 src/game/abilityLoadout.test.ts create mode 100644 src/game/aetherAssault.test.ts create mode 100644 src/game/aetherAssault.ts create mode 100644 src/game/aetherAssaultStore.test.ts create mode 100644 src/game/appearanceLab.test.ts create mode 100644 src/game/appearanceLab.ts create mode 100644 src/game/blockbreaker.test.ts create mode 100644 src/game/blockbreaker.ts create mode 100644 src/game/blockbreakerBiomes.test.ts create mode 100644 src/game/blockbreakerBiomes.ts create mode 100644 src/game/blockbreakerStore.test.ts create mode 100644 src/game/bossAnimation.test.ts create mode 100644 src/game/bossAnimation.ts create mode 100644 src/game/bottomTabs.test.ts create mode 100644 src/game/bottomTabs.ts create mode 100644 src/game/characterAppearance.test.ts create mode 100644 src/game/characterAppearance.ts create mode 100644 src/game/characterEquipment.test.ts create mode 100644 src/game/characterEquipment.ts create mode 100644 src/game/healerClasses.test.ts create mode 100644 src/game/healerEffects.test.ts create mode 100644 src/game/healerEffects.ts create mode 100644 src/game/healerMechanics.test.ts create mode 100644 src/game/healerMechanics.ts create mode 100644 src/game/healerRigAssets.test.ts create mode 100644 src/game/healerVisuals.test.ts create mode 100644 src/game/healerVisuals.ts create mode 100644 src/game/hockeyHealing.test.ts create mode 100644 src/game/hockeyHealing.ts create mode 100644 src/game/hockeyHealingPvp.test.ts create mode 100644 src/game/hockeyHealingPvp.ts create mode 100644 src/game/hockeyHealingPvpStore.test.ts create mode 100644 src/game/hockeyHealingStore.test.ts create mode 100644 src/game/rpgRoguelike/challenges.ts create mode 100644 src/game/rpgRoguelike/combatAdapter.test.ts create mode 100644 src/game/rpgRoguelike/combatAdapter.ts create mode 100644 src/game/rpgRoguelike/index.ts create mode 100644 src/game/rpgRoguelike/party.ts create mode 100644 src/game/rpgRoguelike/playSpace.test.ts create mode 100644 src/game/rpgRoguelike/playSpace.ts create mode 100644 src/game/rpgRoguelike/random.ts create mode 100644 src/game/rpgRoguelike/rewards.ts create mode 100644 src/game/rpgRoguelike/rpgRoguelike.test.ts create mode 100644 src/game/rpgRoguelike/run.ts create mode 100644 src/game/rpgRoguelike/spells.test.ts create mode 100644 src/game/rpgRoguelike/spells.ts create mode 100644 src/game/rpgRoguelike/types.ts create mode 100644 src/game/rpgRoguelike/uiModel.test.ts create mode 100644 src/game/rpgRoguelike/uiModel.ts create mode 100644 src/game/rpgRoguelikeStore.test.ts create mode 100644 src/game/runModes.ts create mode 100644 src/game/staffCastGlow.test.ts create mode 100644 src/game/staffCastGlow.ts create mode 100644 src/game/weaponCatalog.test.ts create mode 100644 src/game/weaponCatalog.ts create mode 100644 src/input/nativeControllerFocus.test.ts diff --git a/README.md b/README.md index 891feee..7750491 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,32 @@ outside the repository. Touch controls on lower display support party targeting, ability casting, map, and inventory. +## Character model rollout and rollback + +Healer characters use the modular `Rig_Medium` renderer by default. Version 1 composes +head, upper body, lower body, headwear, back item, main hand, and offhand slots while +continuing to use Aelia's shared animation set. + +Load a hunter, then open **Appearance Lab** from the main menu. The upper display shows +the real in-game healer renderer with idle, walk, and cast previews. The lower display +selects healer class and cycles every available part. **Save look** persists the current +class, **Reset** restores its authored default, and **Cancel** discards drafts. **Compare +legacy** shows the previous whole-character model without deleting the modular selection. + +Some source assets currently fuse related pieces, so version 1 exposes honest combined +slots such as face + hair, shirt + arms, and pants + shoes. These can split into finer +customization slots when compatible rigged assets are added. + +The previous whole-GLB renderer remains intact during rollout. Use either rollback: + +```text +?characterModels=legacy +VITE_CHARACTER_MODEL_MODE=legacy +``` + +The query changes one browser/app launch. The environment variable produces a legacy +build. Remove the switch to return to modular rendering. + ## Current game scope - Five-member AI party with Disc Priest healer diff --git a/android/app/src/main/java/com/phenomrom/iwanttoheal/ControllerBridgeActivity.java b/android/app/src/main/java/com/phenomrom/iwanttoheal/ControllerBridgeActivity.java index 267c8b0..0d07f2f 100644 --- a/android/app/src/main/java/com/phenomrom/iwanttoheal/ControllerBridgeActivity.java +++ b/android/app/src/main/java/com/phenomrom/iwanttoheal/ControllerBridgeActivity.java @@ -20,7 +20,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -/** Routes every Thor controller event into one JavaScript input service. */ +/** Routes every Thor controller event into one JavaScript input service without WebView focus. */ public abstract class ControllerBridgeActivity extends BridgeActivity { private static final float AXIS_DEAD_ZONE = 0.45f; private static final long REPEAT_THROTTLE_MS = 55L; @@ -43,9 +43,6 @@ public abstract class ControllerBridgeActivity extends BridgeActivity { getWindow().setAttributes(attributes); if (bridge != null && bridge.getWebView() != null) { bridge.getWebView().setOverScrollMode(View.OVER_SCROLL_NEVER); - bridge.getWebView().setFocusable(true); - bridge.getWebView().setFocusableInTouchMode(true); - bridge.getWebView().requestFocus(); } enterImmersiveMode(); } @@ -60,22 +57,6 @@ public abstract class ControllerBridgeActivity extends BridgeActivity { public void onResume() { super.onResume(); enterImmersiveMode(); - if (bridge != null && bridge.getWebView() != null) bridge.getWebView().requestFocus(); - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - if (hasFocus) enterImmersiveMode(); - else clearHeldControllerState(); - } - - @Override - public boolean dispatchTouchEvent(MotionEvent event) { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN && bridge != null) { - bridge.getWebView().requestFocus(); - } - return super.dispatchTouchEvent(event); } @Override @@ -165,10 +146,7 @@ public abstract class ControllerBridgeActivity extends BridgeActivity { String script = "window.dispatchEvent(new CustomEvent('iwt-native-controller'," + "{detail:{token:'" + token + "',repeat:" + repeat + "}}));"; - bridge.getWebView().post(() -> { - bridge.getWebView().requestFocus(); - bridge.getWebView().evaluateJavascript(script, null); - }); + bridge.getWebView().post(() -> bridge.getWebView().evaluateJavascript(script, null)); } private void dispatchNativeControllerMotion(float moveX, float moveY, float lookX, float lookY) { diff --git a/db/schema.sql b/db/schema.sql index a391353..f2f135b 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -66,3 +66,77 @@ CREATE TABLE IF NOT EXISTS rogue_trials_endless_records ( CREATE INDEX IF NOT EXISTS rogue_trials_endless_rank_idx ON rogue_trials_endless_records (highest_boss_kills DESC, updated_at ASC); + +CREATE TABLE IF NOT EXISTS hockey_healing_records ( + account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + slot_id INTEGER NOT NULL CHECK (slot_id BETWEEN 1 AND 3), + highest_returns INTEGER NOT NULL DEFAULT 0 CHECK (highest_returns >= 0), + duration_seconds REAL NOT NULL DEFAULT 0 CHECK (duration_seconds >= 0), + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (account_id, slot_id), + FOREIGN KEY (account_id, slot_id) REFERENCES hunter_saves(account_id, slot_id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS hockey_healing_rank_idx + ON hockey_healing_records (highest_returns DESC, duration_seconds DESC, updated_at ASC); + +CREATE TABLE IF NOT EXISTS hockey_pvp_records ( + account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + slot_id INTEGER NOT NULL CHECK (slot_id BETWEEN 1 AND 3), + wins INTEGER NOT NULL DEFAULT 0 CHECK (wins >= 0), + losses INTEGER NOT NULL DEFAULT 0 CHECK (losses >= 0), + boss_kills INTEGER NOT NULL DEFAULT 0 CHECK (boss_kills >= 0), + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (account_id, slot_id), + FOREIGN KEY (account_id, slot_id) REFERENCES hunter_saves(account_id, slot_id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS hockey_pvp_wins_rank_idx + ON hockey_pvp_records (wins DESC, losses ASC, updated_at ASC); + +CREATE INDEX IF NOT EXISTS hockey_pvp_boss_kills_rank_idx + ON hockey_pvp_records (boss_kills DESC, wins DESC, updated_at ASC); + +CREATE TABLE IF NOT EXISTS blockbreaker_records ( + account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + slot_id INTEGER NOT NULL CHECK (slot_id BETWEEN 1 AND 3), + highest_bricks INTEGER NOT NULL DEFAULT 0 CHECK (highest_bricks >= 0), + bricks_achieved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + longest_seconds REAL NOT NULL DEFAULT 0 CHECK (longest_seconds >= 0), + time_achieved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + highest_score INTEGER NOT NULL DEFAULT 0 CHECK (highest_score >= 0), + score_achieved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (account_id, slot_id), + FOREIGN KEY (account_id, slot_id) REFERENCES hunter_saves(account_id, slot_id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS blockbreaker_bricks_rank_idx + ON blockbreaker_records (highest_bricks DESC, bricks_achieved_at ASC, account_id ASC, slot_id ASC); + +CREATE INDEX IF NOT EXISTS blockbreaker_time_rank_idx + ON blockbreaker_records (longest_seconds DESC, time_achieved_at ASC, account_id ASC, slot_id ASC); + +CREATE INDEX IF NOT EXISTS blockbreaker_score_rank_idx + ON blockbreaker_records (highest_score DESC, score_achieved_at ASC, account_id ASC, slot_id ASC); + +CREATE TABLE IF NOT EXISTS aether_assault_records ( + account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + slot_id INTEGER NOT NULL CHECK (slot_id BETWEEN 1 AND 3), + highest_score INTEGER NOT NULL DEFAULT 0 CHECK (highest_score >= 0), + wave_at_best INTEGER NOT NULL DEFAULT 0 CHECK (wave_at_best >= 0), + duration_at_best REAL NOT NULL DEFAULT 0 CHECK (duration_at_best >= 0), + score_achieved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (account_id, slot_id), + FOREIGN KEY (account_id, slot_id) REFERENCES hunter_saves(account_id, slot_id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS aether_assault_rank_idx + ON aether_assault_records ( + highest_score DESC, + wave_at_best DESC, + score_achieved_at ASC, + account_id ASC, + slot_id ASC + ); diff --git a/server/game-api.mjs b/server/game-api.mjs index 273ed34..542f22d 100644 --- a/server/game-api.mjs +++ b/server/game-api.mjs @@ -180,14 +180,15 @@ function validateSlotId(value) { } function validateSave(value, slotId) { - if (!value || typeof value !== "object" || Number(value.schemaVersion) !== 5) { + const schemaVersion = Number(value?.schemaVersion); + if (!value || typeof value !== "object" || schemaVersion !== 5 && schemaVersion !== 6) { throw apiError("Save snapshot is invalid."); } if (Number(value.slotId) !== slotId) throw apiError("Save slot does not match request."); if (typeof value.hunterName !== "string" || !value.hunterName.trim()) { throw apiError("Save snapshot has no hunter name."); } - return value; + return { ...value, schemaVersion: 6 }; } function normalizeNonNegativeInteger(value) { @@ -195,6 +196,60 @@ function normalizeNonNegativeInteger(value) { return Number.isFinite(number) ? Math.max(0, number) : 0; } +function normalizeNonNegativeNumber(value) { + const number = Number(value); + return Number.isFinite(number) ? Math.max(0, number) : 0; +} + +function mergeLeaderboardHighWater(database, accountId, slotId, save) { + const blockbreaker = database.prepare(` + SELECT highest_bricks AS highestBricks, longest_seconds AS longestSeconds, highest_score AS highestScore + FROM blockbreaker_records WHERE account_id = ? AND slot_id = ? + `).get(accountId, slotId); + const aether = database.prepare(` + SELECT highest_score AS highestScore, wave_at_best AS waveAtBest, duration_at_best AS durationAtBest + FROM aether_assault_records WHERE account_id = ? AND slot_id = ? + `).get(accountId, slotId); + const stats = save.stats && typeof save.stats === "object" ? save.stats : {}; + const candidateAether = { + score: normalizeNonNegativeInteger(stats.highestAetherAssaultScore), + wave: normalizeNonNegativeInteger(stats.highestAetherAssaultWaveAtBest), + duration: normalizeNonNegativeNumber(stats.longestAetherAssaultSecondsAtBest), + }; + const storedAether = { + score: normalizeNonNegativeInteger(aether?.highestScore), + wave: normalizeNonNegativeInteger(aether?.waveAtBest), + duration: normalizeNonNegativeNumber(aether?.durationAtBest), + }; + const aetherRecord = candidateAether.score > storedAether.score + || candidateAether.score === storedAether.score && candidateAether.wave > storedAether.wave + || candidateAether.score === storedAether.score && candidateAether.wave === storedAether.wave && candidateAether.duration > storedAether.duration + ? candidateAether + : storedAether; + return { + ...save, + schemaVersion: 6, + stats: { + ...stats, + highestBlockbreakerBricks: Math.max( + normalizeNonNegativeInteger(stats.highestBlockbreakerBricks), + normalizeNonNegativeInteger(blockbreaker?.highestBricks), + ), + longestBlockbreakerSeconds: Math.max( + normalizeNonNegativeNumber(stats.longestBlockbreakerSeconds), + normalizeNonNegativeNumber(blockbreaker?.longestSeconds), + ), + highestBlockbreakerScore: Math.max( + normalizeNonNegativeInteger(stats.highestBlockbreakerScore), + normalizeNonNegativeInteger(blockbreaker?.highestScore), + ), + highestAetherAssaultScore: aetherRecord.score, + highestAetherAssaultWaveAtBest: aetherRecord.wave, + longestAetherAssaultSecondsAtBest: aetherRecord.duration, + }, + }; +} + function syncLeaderboardStats(database, accountId, slotId, save) { database.prepare("DELETE FROM boss_kill_records WHERE account_id = ? AND slot_id = ?").run(accountId, slotId); const insertBoss = database.prepare(` @@ -225,14 +280,95 @@ function syncLeaderboardStats(database, accountId, slotId, save) { highest_boss_kills = excluded.highest_boss_kills, updated_at = CURRENT_TIMESTAMP `).run(accountId, slotId, highestEndlessKills); + const highestHockeyReturns = normalizeNonNegativeInteger(save.stats?.highestHockeyHealingReturns); + const hockeyDurationSeconds = normalizeNonNegativeNumber(save.stats?.longestHockeyHealingSecondsAtBest); + database.prepare(` + INSERT INTO hockey_healing_records (account_id, slot_id, highest_returns, duration_seconds, updated_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(account_id, slot_id) DO UPDATE SET + highest_returns = excluded.highest_returns, + duration_seconds = excluded.duration_seconds, + updated_at = CURRENT_TIMESTAMP + `).run(accountId, slotId, highestHockeyReturns, hockeyDurationSeconds); + + const hockeyPvpWins = normalizeNonNegativeInteger(save.stats?.hockeyHealingPvpWins); + const hockeyPvpLosses = normalizeNonNegativeInteger(save.stats?.hockeyHealingPvpLosses); + const hockeyPvpBossKills = normalizeNonNegativeInteger(save.stats?.hockeyHealingPvpBossKills); + database.prepare(` + INSERT INTO hockey_pvp_records (account_id, slot_id, wins, losses, boss_kills, updated_at) + VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(account_id, slot_id) DO UPDATE SET + wins = excluded.wins, + losses = excluded.losses, + boss_kills = excluded.boss_kills, + updated_at = CURRENT_TIMESTAMP + `).run(accountId, slotId, hockeyPvpWins, hockeyPvpLosses, hockeyPvpBossKills); + + const highestBlockbreakerBricks = normalizeNonNegativeInteger(save.stats?.highestBlockbreakerBricks); + const longestBlockbreakerSeconds = normalizeNonNegativeNumber(save.stats?.longestBlockbreakerSeconds); + const highestBlockbreakerScore = normalizeNonNegativeInteger(save.stats?.highestBlockbreakerScore); + database.prepare(` + INSERT INTO blockbreaker_records ( + account_id, slot_id, + highest_bricks, bricks_achieved_at, + longest_seconds, time_achieved_at, + highest_score, score_achieved_at, + updated_at + ) VALUES (?, ?, ?, CURRENT_TIMESTAMP, ?, CURRENT_TIMESTAMP, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ON CONFLICT(account_id, slot_id) DO UPDATE SET + bricks_achieved_at = CASE + WHEN excluded.highest_bricks > blockbreaker_records.highest_bricks THEN CURRENT_TIMESTAMP + ELSE blockbreaker_records.bricks_achieved_at + END, + highest_bricks = MAX(blockbreaker_records.highest_bricks, excluded.highest_bricks), + time_achieved_at = CASE + WHEN excluded.longest_seconds > blockbreaker_records.longest_seconds THEN CURRENT_TIMESTAMP + ELSE blockbreaker_records.time_achieved_at + END, + longest_seconds = MAX(blockbreaker_records.longest_seconds, excluded.longest_seconds), + score_achieved_at = CASE + WHEN excluded.highest_score > blockbreaker_records.highest_score THEN CURRENT_TIMESTAMP + ELSE blockbreaker_records.score_achieved_at + END, + highest_score = MAX(blockbreaker_records.highest_score, excluded.highest_score), + updated_at = CURRENT_TIMESTAMP + `).run(accountId, slotId, highestBlockbreakerBricks, longestBlockbreakerSeconds, highestBlockbreakerScore); + + const highestAetherScore = normalizeNonNegativeInteger(save.stats?.highestAetherAssaultScore); + const aetherWaveAtBest = normalizeNonNegativeInteger(save.stats?.highestAetherAssaultWaveAtBest); + const aetherDurationAtBest = normalizeNonNegativeNumber(save.stats?.longestAetherAssaultSecondsAtBest); + database.prepare(` + INSERT INTO aether_assault_records ( + account_id, slot_id, highest_score, wave_at_best, duration_at_best, score_achieved_at, updated_at + ) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ON CONFLICT(account_id, slot_id) DO UPDATE SET + highest_score = excluded.highest_score, + wave_at_best = excluded.wave_at_best, + duration_at_best = excluded.duration_at_best, + score_achieved_at = CASE + WHEN excluded.highest_score > aether_assault_records.highest_score + OR excluded.highest_score = aether_assault_records.highest_score + AND excluded.wave_at_best > aether_assault_records.wave_at_best + THEN CURRENT_TIMESTAMP + ELSE aether_assault_records.score_achieved_at + END, + updated_at = CURRENT_TIMESTAMP + WHERE excluded.highest_score > aether_assault_records.highest_score + OR excluded.highest_score = aether_assault_records.highest_score + AND excluded.wave_at_best > aether_assault_records.wave_at_best + OR excluded.highest_score = aether_assault_records.highest_score + AND excluded.wave_at_best = aether_assault_records.wave_at_best + AND excluded.duration_at_best > aether_assault_records.duration_at_best + `).run(accountId, slotId, highestAetherScore, aetherWaveAtBest, aetherDurationAtBest); } function writeSave(database, accountId, slotId, rawSave) { - const save = validateSave(rawSave, slotId); - const serialized = JSON.stringify(save); - if (Buffer.byteLength(serialized) > MAX_JSON_BYTES) throw apiError("Save snapshot is too large.", 413); + const validatedSave = validateSave(rawSave, slotId); database.exec("BEGIN IMMEDIATE"); try { + const save = mergeLeaderboardHighWater(database, accountId, slotId, validatedSave); + const serialized = JSON.stringify(save); + if (Buffer.byteLength(serialized) > MAX_JSON_BYTES) throw apiError("Save snapshot is too large.", 413); database.prepare(` INSERT INTO hunter_saves (account_id, slot_id, hunter_name, save_json, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) @@ -243,11 +379,11 @@ function writeSave(database, accountId, slotId, rawSave) { `).run(accountId, slotId, save.hunterName.trim().slice(0, 20), serialized); syncLeaderboardStats(database, accountId, slotId, save); database.exec("COMMIT"); + return save; } catch (error) { database.exec("ROLLBACK"); throw error; } - return save; } function readSave(database, accountId, slotId) { @@ -255,7 +391,12 @@ function readSave(database, accountId, slotId) { SELECT save_json AS saveJson FROM hunter_saves WHERE account_id = ? AND slot_id = ? `).get(accountId, slotId); if (!row) return null; - try { return JSON.parse(row.saveJson); } catch { return null; } + try { + const save = JSON.parse(row.saveJson); + return mergeLeaderboardHighWater(database, accountId, slotId, save); + } catch { + return null; + } } function listSaves(database, accountId) { @@ -263,18 +404,22 @@ function listSaves(database, accountId) { SELECT slot_id AS slotId, save_json AS saveJson, updated_at AS updatedAt FROM hunter_saves WHERE account_id = ? ORDER BY slot_id `).all(accountId).flatMap((row) => { - try { return [{ slotId: row.slotId, save: JSON.parse(row.saveJson), updatedAt: row.updatedAt }]; } + try { + const save = mergeLeaderboardHighWater(database, accountId, row.slotId, JSON.parse(row.saveJson)); + return [{ slotId: row.slotId, save, updatedAt: row.updatedAt }]; + } catch { return []; } }); } -function leaderboardEntry(row, valueKey) { +function leaderboardEntry(row, valueKey, secondaryValueKey) { return { rank: row.rank, username: row.username, hunterName: row.hunterName, slotId: row.slotId, value: row[valueKey], + ...(secondaryValueKey ? { secondaryValue: row[secondaryValueKey] } : {}), }; } @@ -358,11 +503,224 @@ function rogueTrialsEndlessLeaderboard(database, accountId, slotId) { }; } +function hockeyHealingLeaderboard(database, accountId, slotId) { + const rows = database.prepare(` + WITH ranked AS ( + SELECT + RANK() OVER (ORDER BY records.highest_returns DESC, records.duration_seconds DESC) AS rank, + records.account_id AS accountId, + records.slot_id AS slotId, + records.highest_returns AS highestReturns, + records.duration_seconds AS durationSeconds, + accounts.username, + saves.hunter_name AS hunterName, + records.updated_at AS updatedAt + FROM hockey_healing_records records + JOIN accounts ON accounts.id = records.account_id + JOIN hunter_saves saves ON saves.account_id = records.account_id AND saves.slot_id = records.slot_id + WHERE records.highest_returns > 0 OR records.duration_seconds > 0 + ) + SELECT * FROM ranked + ORDER BY highestReturns DESC, durationSeconds DESC, updatedAt ASC, accountId ASC, slotId ASC + `).all(); + const current = rows.find((row) => row.accountId === accountId && row.slotId === slotId) ?? null; + return { + kind: "hockey-healing", + top: rows.slice(0, 5).map((row) => leaderboardEntry(row, "highestReturns", "durationSeconds")), + current: current ? leaderboardEntry(current, "highestReturns", "durationSeconds") : null, + }; +} + +function hockeyPvpLeaderboard(database, accountId, slotId, kind) { + const winsBoard = kind === "hockey-pvp-wins"; + const order = winsBoard + ? "records.wins DESC, records.losses ASC" + : "records.boss_kills DESC, records.wins DESC"; + const rows = database.prepare(` + WITH ranked AS ( + SELECT + RANK() OVER (ORDER BY ${order}) AS rank, + records.account_id AS accountId, + records.slot_id AS slotId, + records.wins, + records.losses, + records.boss_kills AS bossKills, + accounts.username, + saves.hunter_name AS hunterName, + records.updated_at AS updatedAt + FROM hockey_pvp_records records + JOIN accounts ON accounts.id = records.account_id + JOIN hunter_saves saves ON saves.account_id = records.account_id AND saves.slot_id = records.slot_id + WHERE ${winsBoard ? "records.wins > 0 OR records.losses > 0" : "records.boss_kills > 0"} + ) + SELECT * FROM ranked + ORDER BY ${winsBoard ? "wins DESC, losses ASC" : "bossKills DESC, wins DESC"}, updatedAt ASC, accountId ASC, slotId ASC + `).all(); + const current = rows.find((row) => row.accountId === accountId && row.slotId === slotId) ?? null; + const valueKey = winsBoard ? "wins" : "bossKills"; + const secondaryKey = winsBoard ? "losses" : undefined; + return { + kind, + top: rows.slice(0, 5).map((row) => leaderboardEntry(row, valueKey, secondaryKey)), + current: current ? leaderboardEntry(current, valueKey, secondaryKey) : null, + }; +} + +function blockbreakerLeaderboard(database, accountId, slotId, kind) { + const boards = { + "blockbreaker-bricks": { column: "highest_bricks", valueKey: "highestBricks", achieved: "bricks_achieved_at" }, + "blockbreaker-time": { column: "longest_seconds", valueKey: "longestSeconds", achieved: "time_achieved_at" }, + "blockbreaker-score": { column: "highest_score", valueKey: "highestScore", achieved: "score_achieved_at" }, + }; + const board = boards[kind]; + if (!board) throw apiError("Invalid Blockbreaker leaderboard."); + const rows = database.prepare(` + WITH ranked AS ( + SELECT + RANK() OVER (ORDER BY records.${board.column} DESC) AS rank, + records.account_id AS accountId, + records.slot_id AS slotId, + records.${board.column} AS ${board.valueKey}, + records.${board.achieved} AS achievedAt, + accounts.username, + saves.hunter_name AS hunterName + FROM blockbreaker_records records + JOIN accounts ON accounts.id = records.account_id + JOIN hunter_saves saves ON saves.account_id = records.account_id AND saves.slot_id = records.slot_id + WHERE records.${board.column} > 0 + ) + SELECT * FROM ranked + ORDER BY ${board.valueKey} DESC, achievedAt ASC, accountId ASC, slotId ASC + `).all(); + const current = rows.find((row) => row.accountId === accountId && row.slotId === slotId) ?? null; + return { + kind, + top: rows.slice(0, 5).map((row) => leaderboardEntry(row, board.valueKey)), + current: current ? leaderboardEntry(current, board.valueKey) : null, + }; +} + +function aetherAssaultLeaderboard(database, accountId, slotId) { + const rows = database.prepare(` + WITH ranked AS ( + SELECT + RANK() OVER ( + ORDER BY records.highest_score DESC, records.wave_at_best DESC, + records.score_achieved_at ASC, records.account_id ASC, records.slot_id ASC + ) AS rank, + records.account_id AS accountId, + records.slot_id AS slotId, + records.highest_score AS highestScore, + records.wave_at_best AS waveAtBest, + records.score_achieved_at AS achievedAt, + accounts.username, + saves.hunter_name AS hunterName + FROM aether_assault_records records + JOIN accounts ON accounts.id = records.account_id + JOIN hunter_saves saves ON saves.account_id = records.account_id AND saves.slot_id = records.slot_id + WHERE records.highest_score > 0 + ) + SELECT * FROM ranked + ORDER BY highestScore DESC, waveAtBest DESC, achievedAt ASC, accountId ASC, slotId ASC + `).all(); + const current = rows.find((row) => row.accountId === accountId && row.slotId === slotId) ?? null; + return { + kind: "aether-assault", + top: rows.slice(0, 5).map((row) => leaderboardEntry(row, "highestScore", "waveAtBest")), + current: current ? leaderboardEntry(current, "highestScore", "waveAtBest") : null, + }; +} + export function createGameApiHandler(options = {}) { const dataDirectory = resolve(options.dataDirectory ?? process.env.DATA_DIR ?? "data"); mkdirSync(dataDirectory, { recursive: true }); const database = new DatabaseSync(resolve(dataDirectory, "game.db")); database.exec(readFileSync(new URL("../db/schema.sql", import.meta.url), "utf8")); + const hockeyPvpTickets = new Map(); + const hockeyPvpMatches = new Map(); + + function queueResult(ticket) { + const match = ticket.matchId ? hockeyPvpMatches.get(ticket.matchId) : null; + if (!match) return { ticketId: ticket.id, status: "waiting" }; + const opponentSide = ticket.side === "host" ? "guest" : "host"; + const opponent = match.players[opponentSide]; + return { + ticketId: ticket.id, + status: "matched", + match: { + id: match.id, + seed: match.seed, + opponentName: opponent.hunterName, + role: ticket.side, + }, + }; + } + + function joinHockeyPvpQueue(session, payload) { + const slotId = validateSlotId(payload?.slotId); + const hunterName = String(payload?.hunterName ?? "").trim().slice(0, 20); + if (!hunterName) throw apiError("Hunter name is required."); + const existing = [...hockeyPvpTickets.values()].find((ticket) => + ticket.accountId === session.accountId && !ticket.matchId && !ticket.cancelled && !ticket.completed); + if (existing) return queueResult(existing); + + const now = Date.now(); + for (const ticket of hockeyPvpTickets.values()) { + if (!ticket.matchId && now - ticket.createdAt > 30_000) { + ticket.cancelled = true; + ticket.completed = true; + } + } + const opponent = [...hockeyPvpTickets.values()].find((ticket) => + !ticket.matchId && !ticket.cancelled && !ticket.completed && ticket.accountId !== session.accountId); + const ticket = { + id: randomBytes(18).toString("base64url"), + accountId: session.accountId, + username: session.username, + slotId, + hunterName, + createdAt: now, + matchId: null, + side: null, + cancelled: false, + completed: false, + }; + hockeyPvpTickets.set(ticket.id, ticket); + if (!opponent) return queueResult(ticket); + + const matchId = randomBytes(18).toString("base64url"); + const match = { + id: matchId, + seed: randomBytes(4).readUInt32BE(0) || 1, + createdAt: now, + players: { host: opponent, guest: ticket }, + snapshots: { host: null, guest: null }, + }; + opponent.matchId = matchId; + opponent.side = "host"; + ticket.matchId = matchId; + ticket.side = "guest"; + hockeyPvpMatches.set(matchId, match); + return queueResult(ticket); + } + + function requireHockeyPvpTicket(session, ticketId) { + const ticket = hockeyPvpTickets.get(ticketId); + if (!ticket || ticket.accountId !== session.accountId || ticket.cancelled) throw apiError("Queue ticket not found.", 404); + return ticket; + } + + function requireHockeyPvpMatch(session, matchId) { + const match = hockeyPvpMatches.get(matchId); + if (!match) throw apiError("PVP match not found.", 404); + const side = match.players.host.accountId === session.accountId + ? "host" + : match.players.guest.accountId === session.accountId + ? "guest" + : null; + if (!side) throw apiError("PVP match access denied.", 403); + return { match, side }; + } async function handle(request, response, next) { if (!request.url?.startsWith("/api/")) return next(); @@ -400,6 +758,31 @@ export function createGameApiHandler(options = {}) { } const session = requireSession(database, request); + if (path === "/api/hockey-pvp/queue" && request.method === "POST") { + return sendJson(response, 200, joinHockeyPvpQueue(session, await readJson(request))); + } + const queueMatch = path.match(/^\/api\/hockey-pvp\/queue\/([A-Za-z0-9_-]+)$/); + if (queueMatch && request.method === "GET") { + return sendJson(response, 200, queueResult(requireHockeyPvpTicket(session, queueMatch[1]))); + } + if (queueMatch && request.method === "DELETE") { + const ticket = requireHockeyPvpTicket(session, queueMatch[1]); + if (ticket.matchId) throw apiError("Matched queue cannot be cancelled.", 409); + ticket.cancelled = true; + ticket.completed = true; + return sendJson(response, 200, { ok: true }); + } + const pvpStateMatch = path.match(/^\/api\/hockey-pvp\/matches\/([A-Za-z0-9_-]+)\/state$/); + if (pvpStateMatch && request.method === "PUT") { + const { match, side } = requireHockeyPvpMatch(session, pvpStateMatch[1]); + const payload = await readJson(request); + if (!payload?.snapshot || typeof payload.snapshot !== "object") throw apiError("PVP snapshot is invalid."); + match.snapshots[side] = payload.snapshot; + return sendJson(response, 200, { + opponentSnapshot: match.snapshots[side === "host" ? "guest" : "host"], + hostSnapshot: match.snapshots.host, + }); + } if (path === "/api/saves" && request.method === "GET") { return sendJson(response, 200, { slots: listSaves(database, session.accountId) }); } @@ -425,6 +808,34 @@ export function createGameApiHandler(options = {}) { const slotId = validateSlotId(url.searchParams.get("slot")); return sendJson(response, 200, rogueTrialsEndlessLeaderboard(database, session.accountId, slotId)); } + if (path === "/api/leaderboards/hockey-healing" && request.method === "GET") { + const slotId = validateSlotId(url.searchParams.get("slot")); + return sendJson(response, 200, hockeyHealingLeaderboard(database, session.accountId, slotId)); + } + if (path === "/api/leaderboards/hockey-pvp-wins" && request.method === "GET") { + const slotId = validateSlotId(url.searchParams.get("slot")); + return sendJson(response, 200, hockeyPvpLeaderboard(database, session.accountId, slotId, "hockey-pvp-wins")); + } + if (path === "/api/leaderboards/hockey-pvp-boss-kills" && request.method === "GET") { + const slotId = validateSlotId(url.searchParams.get("slot")); + return sendJson(response, 200, hockeyPvpLeaderboard(database, session.accountId, slotId, "hockey-pvp-boss-kills")); + } + if (path === "/api/leaderboards/blockbreaker-bricks" && request.method === "GET") { + const slotId = validateSlotId(url.searchParams.get("slot")); + return sendJson(response, 200, blockbreakerLeaderboard(database, session.accountId, slotId, "blockbreaker-bricks")); + } + if (path === "/api/leaderboards/blockbreaker-time" && request.method === "GET") { + const slotId = validateSlotId(url.searchParams.get("slot")); + return sendJson(response, 200, blockbreakerLeaderboard(database, session.accountId, slotId, "blockbreaker-time")); + } + if (path === "/api/leaderboards/blockbreaker-score" && request.method === "GET") { + const slotId = validateSlotId(url.searchParams.get("slot")); + return sendJson(response, 200, blockbreakerLeaderboard(database, session.accountId, slotId, "blockbreaker-score")); + } + if (path === "/api/leaderboards/aether-assault" && request.method === "GET") { + const slotId = validateSlotId(url.searchParams.get("slot")); + return sendJson(response, 200, aetherAssaultLeaderboard(database, session.accountId, slotId)); + } return sendJson(response, 404, { error: "API route not found." }); } catch (error) { const status = Number(error?.status) || 500; @@ -434,5 +845,12 @@ export function createGameApiHandler(options = {}) { } } - return { handle, close: () => database.close() }; + return { + handle, + close: () => { + hockeyPvpTickets.clear(); + hockeyPvpMatches.clear(); + database.close(); + }, + }; } diff --git a/server/game-api.test.mjs b/server/game-api.test.mjs index e4015bb..afefb26 100644 --- a/server/game-api.test.mjs +++ b/server/game-api.test.mjs @@ -34,12 +34,12 @@ async function json(path, init = {}) { return { response, body }; } -function save(slotId, hunterName, bulldromeKills, highestRoguelikeRound, highestRogueTrialsEndlessKills) { +function save(slotId, hunterName, bulldromeKills, highestRoguelikeRound, highestRogueTrialsEndlessKills, highestHockeyHealingReturns, longestHockeyHealingSecondsAtBest, hockeyHealingPvpWins = 0, hockeyHealingPvpLosses = 0, hockeyHealingPvpBossKills = 0, highestBlockbreakerBricks = 0, longestBlockbreakerSeconds = 0, highestBlockbreakerScore = 0, highestAetherAssaultScore = 0, highestAetherAssaultWaveAtBest = 0, longestAetherAssaultSecondsAtBest = 0) { return { - schemaVersion: 5, + schemaVersion: 6, slotId, hunterName, - stats: { bossKills: { bulldrome: bulldromeKills }, highestRoguelikeRound, highestRogueTrialsEndlessKills }, + stats: { bossKills: { bulldrome: bulldromeKills }, highestRoguelikeRound, highestRogueTrialsEndlessKills, highestHockeyHealingReturns, longestHockeyHealingSecondsAtBest, hockeyHealingPvpWins, hockeyHealingPvpLosses, hockeyHealingPvpBossKills, highestBlockbreakerBricks, longestBlockbreakerSeconds, highestBlockbreakerScore, highestAetherAssaultScore, highestAetherAssaultWaveAtBest, longestAetherAssaultSecondsAtBest }, }; } @@ -63,13 +63,24 @@ test("accounts, server saves, and top-five plus current rankings work end to end const kills = 60 - index * 10; const highestRound = 30 - index * 4; const highestEndlessKills = 24 - index * 3; + const highestHockeyReturns = 30 - index * 4; + const hockeyDuration = 180 - index * 10; + const pvpWins = 30 - index * 4; + const pvpLosses = index + 1; + const pvpBossKills = 120 - index * 12; + const blockbreakerBricks = index < 2 ? 600 : 700 - index * 100; + const blockbreakerSeconds = 360 - index * 30; + const blockbreakerScore = 20_000 - index * 2_000; + const aetherScore = index < 2 ? 50_000 : 54_000 - index * 5_000; + const aetherWave = index < 2 ? 12 : 10 - index; + const aetherDuration = 300 - index * 20; const upload = await json("/api/saves/1", { method: "PUT", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, - body: JSON.stringify({ save: save(1, `Hero ${index}`, kills, highestRound, highestEndlessKills) }), + body: JSON.stringify({ save: save(1, `Hero ${index}`, kills, highestRound, highestEndlessKills, highestHockeyReturns, hockeyDuration, pvpWins, pvpLosses, pvpBossKills, blockbreakerBricks, blockbreakerSeconds, blockbreakerScore, aetherScore, aetherWave, aetherDuration) }), }); assert.equal(upload.response.status, 200); - players.push({ token, kills, highestRound, highestEndlessKills }); + players.push({ token, kills, highestRound, highestEndlessKills, highestHockeyReturns, hockeyDuration, pvpWins, pvpLosses, pvpBossKills, blockbreakerBricks, blockbreakerSeconds, blockbreakerScore, aetherScore, aetherWave, aetherDuration }); } const current = players[5]; @@ -97,12 +108,150 @@ test("accounts, server saves, and top-five plus current rankings work end to end assert.equal(endlessBoard.body.current.rank, 6); assert.equal(endlessBoard.body.current.value, current.highestEndlessKills); + const hockeyBoard = await json("/api/leaderboards/hockey-healing?slot=1", { + headers: { Authorization: `Bearer ${current.token}` }, + }); + assert.equal(hockeyBoard.body.kind, "hockey-healing"); + assert.equal(hockeyBoard.body.top.length, 5); + assert.equal(hockeyBoard.body.top[0].value, 30); + assert.equal(hockeyBoard.body.current.rank, 6); + assert.equal(hockeyBoard.body.current.value, current.highestHockeyReturns); + assert.equal(hockeyBoard.body.current.secondaryValue, current.hockeyDuration); + + const pvpWinsBoard = await json("/api/leaderboards/hockey-pvp-wins?slot=1", { + headers: { Authorization: `Bearer ${current.token}` }, + }); + assert.equal(pvpWinsBoard.body.kind, "hockey-pvp-wins"); + assert.equal(pvpWinsBoard.body.top[0].value, 30); + assert.equal(pvpWinsBoard.body.current.value, current.pvpWins); + assert.equal(pvpWinsBoard.body.current.secondaryValue, current.pvpLosses); + + const pvpKillsBoard = await json("/api/leaderboards/hockey-pvp-boss-kills?slot=1", { + headers: { Authorization: `Bearer ${current.token}` }, + }); + assert.equal(pvpKillsBoard.body.kind, "hockey-pvp-boss-kills"); + assert.equal(pvpKillsBoard.body.top[0].value, 120); + assert.equal(pvpKillsBoard.body.current.value, current.pvpBossKills); + + const blockbreakerBricksBoard = await json("/api/leaderboards/blockbreaker-bricks?slot=1", { + headers: { Authorization: `Bearer ${current.token}` }, + }); + assert.equal(blockbreakerBricksBoard.body.kind, "blockbreaker-bricks"); + assert.equal(blockbreakerBricksBoard.body.top.length, 5); + assert.equal(blockbreakerBricksBoard.body.top[0].value, 600); + assert.equal(blockbreakerBricksBoard.body.top[0].rank, 1); + assert.equal(blockbreakerBricksBoard.body.top[1].rank, 1); + assert.equal(blockbreakerBricksBoard.body.current.rank, 6); + assert.equal(blockbreakerBricksBoard.body.current.value, current.blockbreakerBricks); + + const blockbreakerTimeBoard = await json("/api/leaderboards/blockbreaker-time?slot=1", { + headers: { Authorization: `Bearer ${current.token}` }, + }); + assert.equal(blockbreakerTimeBoard.body.kind, "blockbreaker-time"); + assert.equal(blockbreakerTimeBoard.body.top[0].value, 360); + assert.equal(blockbreakerTimeBoard.body.current.value, current.blockbreakerSeconds); + + const blockbreakerScoreBoard = await json("/api/leaderboards/blockbreaker-score?slot=1", { + headers: { Authorization: `Bearer ${current.token}` }, + }); + assert.equal(blockbreakerScoreBoard.body.kind, "blockbreaker-score"); + assert.equal(blockbreakerScoreBoard.body.top[0].value, 20_000); + assert.equal(blockbreakerScoreBoard.body.current.value, current.blockbreakerScore); + + const aetherBoard = await json("/api/leaderboards/aether-assault?slot=1", { + headers: { Authorization: `Bearer ${current.token}` }, + }); + assert.equal(aetherBoard.body.kind, "aether-assault"); + assert.equal(aetherBoard.body.top.length, 5); + assert.equal(aetherBoard.body.top[0].value, 50_000); + assert.equal(aetherBoard.body.top[0].secondaryValue, 12); + assert.equal(aetherBoard.body.top[0].username, "hunter_0"); + assert.equal(aetherBoard.body.top[0].rank, 1); + assert.equal(aetherBoard.body.top[1].value, 50_000); + assert.equal(aetherBoard.body.top[1].secondaryValue, 12); + assert.equal(aetherBoard.body.top[1].username, "hunter_1"); + assert.equal(aetherBoard.body.top[1].rank, 2); + assert.equal(aetherBoard.body.current.rank, 6); + assert.equal(aetherBoard.body.current.value, current.aetherScore); + assert.equal(aetherBoard.body.current.secondaryValue, current.aetherWave); + + const legacySnapshot = save(1, "Hero 5", current.kills, current.highestRound, current.highestEndlessKills, current.highestHockeyReturns, current.hockeyDuration, current.pvpWins, current.pvpLosses, current.pvpBossKills); + delete legacySnapshot.stats.highestBlockbreakerBricks; + delete legacySnapshot.stats.longestBlockbreakerSeconds; + delete legacySnapshot.stats.highestBlockbreakerScore; + delete legacySnapshot.stats.highestAetherAssaultScore; + delete legacySnapshot.stats.highestAetherAssaultWaveAtBest; + delete legacySnapshot.stats.longestAetherAssaultSecondsAtBest; + legacySnapshot.schemaVersion = 5; + const legacyUpload = await json("/api/saves/1", { + method: "PUT", + headers: { Authorization: `Bearer ${current.token}`, "Content-Type": "application/json" }, + body: JSON.stringify({ save: legacySnapshot }), + }); + assert.equal(legacyUpload.body.save.stats.highestBlockbreakerBricks, current.blockbreakerBricks); + assert.equal(legacyUpload.body.save.stats.longestBlockbreakerSeconds, current.blockbreakerSeconds); + assert.equal(legacyUpload.body.save.stats.highestBlockbreakerScore, current.blockbreakerScore); + assert.equal(legacyUpload.body.save.schemaVersion, 6); + assert.equal(legacyUpload.body.save.stats.highestAetherAssaultScore, current.aetherScore); + assert.equal(legacyUpload.body.save.stats.highestAetherAssaultWaveAtBest, current.aetherWave); + assert.equal(legacyUpload.body.save.stats.longestAetherAssaultSecondsAtBest, current.aetherDuration); + const download = await json("/api/saves/1", { headers: { Authorization: `Bearer ${current.token}` }, }); assert.equal(download.body.save.hunterName, "Hero 5"); }); +test("Healing Hockey PVP queue pairs players and relays match snapshots", async () => { + const registerPlayer = async (username) => { + const registration = await json("/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password: `long-password-${username}` }), + }); + return registration.body.token; + }; + const alphaToken = await registerPlayer("pvp_alpha"); + const betaToken = await registerPlayer("pvp_beta"); + const alphaQueue = await json("/api/hockey-pvp/queue", { + method: "POST", + headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" }, + body: JSON.stringify({ slotId: 1, hunterName: "Alpha" }), + }); + assert.equal(alphaQueue.body.status, "waiting"); + + const betaQueue = await json("/api/hockey-pvp/queue", { + method: "POST", + headers: { Authorization: `Bearer ${betaToken}`, "Content-Type": "application/json" }, + body: JSON.stringify({ slotId: 1, hunterName: "Beta" }), + }); + assert.equal(betaQueue.body.status, "matched"); + assert.equal(betaQueue.body.match.role, "guest"); + assert.equal(betaQueue.body.match.opponentName, "Alpha"); + + const alphaMatched = await json(`/api/hockey-pvp/queue/${alphaQueue.body.ticketId}`, { + headers: { Authorization: `Bearer ${alphaToken}` }, + }); + assert.equal(alphaMatched.body.status, "matched"); + assert.equal(alphaMatched.body.match.role, "host"); + assert.equal(alphaMatched.body.match.id, betaQueue.body.match.id); + assert.equal(alphaMatched.body.match.seed, betaQueue.body.match.seed); + + const hostSnapshot = { sequence: 1, party: [], puck: { goalSequence: 0 } }; + await json(`/api/hockey-pvp/matches/${alphaMatched.body.match.id}/state`, { + method: "PUT", + headers: { Authorization: `Bearer ${alphaToken}`, "Content-Type": "application/json" }, + body: JSON.stringify({ snapshot: hostSnapshot }), + }); + const guestExchange = await json(`/api/hockey-pvp/matches/${alphaMatched.body.match.id}/state`, { + method: "PUT", + headers: { Authorization: `Bearer ${betaToken}`, "Content-Type": "application/json" }, + body: JSON.stringify({ snapshot: { sequence: 1, party: [] } }), + }); + assert.deepEqual(guestExchange.body.opponentSnapshot, hostSnapshot); + assert.deepEqual(guestExchange.body.hostSnapshot, hostSnapshot); +}); + test("invalid credentials cannot access server saves", async () => { const login = await json("/api/auth/login", { method: "POST", diff --git a/src/App.tsx b/src/App.tsx index 9d9c5f7..95b204a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,15 +3,19 @@ import packageJson from "../package.json"; import { DualDisplayFrame } from "./components/DualDisplayFrame"; import { FrontEnd } from "./components/FrontEnd"; import { useActiveHunter, useFrontendStore } from "./frontend/store"; -import { useGameStore } from "./game/store"; +import { getHockeyPvpNetworkSnapshot, useGameStore } from "./game/store"; import type { BossId } from "./game/types"; import type { DifficultySlug } from "./game/progression/loot"; import { useActionBindings } from "./game/useGameLoop"; import { useAuthoritativeDualScreenSync, useForcedThorDisplays } from "./platform/useThorDualScreen"; import { DUAL_SCREEN_EXIT_EVENT, DUAL_SCREEN_LAUNCH_EVENT } from "./platform/dualScreenSync"; +import { startSaveSyncCoordinator } from "./frontend/saveSync"; +import type { HockeyPvpMatchConfig } from "./game/hockeyHealingPvp"; +import { onlineRepository } from "./frontend/onlineRepository"; const TopScreen = lazy(() => import("./components/TopScreen").then((module) => ({ default: module.TopScreen }))); const BottomScreen = lazy(() => import("./components/BottomScreen").then((module) => ({ default: module.BottomScreen }))); +const HealerModelGallery = lazy(() => import("./components/GameScene").then((module) => ({ default: module.HealerModelGallery }))); function GameLoadingScreen() { return ( @@ -22,10 +26,11 @@ function GameLoadingScreen() { ); } -export default function App() { +function MainApp() { useForcedThorDisplays(); useAuthoritativeDualScreenSync(); const screen = useFrontendStore((state) => state.screen); + const accountId = useFrontendStore((state) => state.accountId); const hunter = useActiveHunter(); const settings = useFrontendStore((state) => state.settings); const navigate = useFrontendStore((state) => state.navigate); @@ -34,36 +39,65 @@ export default function App() { const recordBossVictory = useFrontendStore((state) => state.recordBossVictory); const recordRoguelikeDefeat = useFrontendStore((state) => state.recordRoguelikeDefeat); const recordRogueTrialsEndlessDefeat = useFrontendStore((state) => state.recordRogueTrialsEndlessDefeat); + const recordHockeyHealingDefeat = useFrontendStore((state) => state.recordHockeyHealingDefeat); + const recordHockeyPvpResult = useFrontendStore((state) => state.recordHockeyPvpResult); + const recordHockeyPvpBossKill = useFrontendStore((state) => state.recordHockeyPvpBossKill); + const recordBlockbreakerDefeat = useFrontendStore((state) => state.recordBlockbreakerDefeat); + const recordAetherAssaultDefeat = useFrontendStore((state) => state.recordAetherAssaultDefeat); const clearRecentRewards = useFrontendStore((state) => state.clearRecentRewards); const rewardedBossInstances = useRef(new Set()); const screenRef = useRef(screen); screenRef.current = screen; const leaveGame = useCallback(() => { const { accountId, activeSlotId, uploadSlot } = useFrontendStore.getState(); - updateActiveHealerInventory(useGameStore.getState().inventory); + const game = useGameStore.getState(); + // RPG Roguelike equipment belongs only to its current run. Never leak it + // into the hunter's permanent inventory when leaving the expedition. + if (game.runMode !== "rpg-roguelike") updateActiveHealerInventory(game.inventory); touchActiveSave(); navigate("home"); if (accountId && activeSlotId) void uploadSlot(activeSlotId); }, [navigate, touchActiveSave, updateActiveHealerInventory]); - const launchGame = useCallback((bossIds: readonly BossId[], requestedDifficultySlug?: DifficultySlug) => { + const launchGame = useCallback((bossIds: readonly BossId[], requestedDifficultySlug?: DifficultySlug, hockeyPvpMatch?: HockeyPvpMatchConfig) => { if (!hunter) return; const progress = hunter.healers[hunter.activeClassId]; const selectedMode = useFrontendStore.getState().selectedMode; - const runMode = selectedMode === "roguelike-pve" ? "roguelike" : selectedMode === "rogue-trials" ? "rogue-trials" : "encounter"; + const runMode = selectedMode === "roguelike-pve" + ? "rpg-roguelike" + : selectedMode === "rogue-trials" + ? "rogue-trials" + : selectedMode === "hockey-healing" + ? "hockey-healing" + : selectedMode === "hockey-healing-pvp" + ? "hockey-healing-pvp" + : selectedMode === "blockbreaker" + ? "blockbreaker" + : selectedMode === "aether-assault" + ? "aether-assault" + : "encounter"; const launchDifficulty = runMode !== "encounter" ? "initiate" : requestedDifficultySlug ?? useFrontendStore.getState().selectedDifficultySlug; rewardedBossInstances.current.clear(); clearRecentRewards(); - useGameStore.getState().configureHealer(hunter.activeClassId, hunter.hunterName, progress.inventory, bossIds, runMode, hunter.gearProgress, launchDifficulty); + useGameStore.getState().configureHealer( + hunter.activeClassId, + hunter.hunterName, + progress.inventory, + bossIds, + runMode, + hunter.gearProgress, + launchDifficulty, + hockeyPvpMatch, + ); touchActiveSave(); navigate("game"); }, [clearRecentRewards, hunter, navigate, touchActiveSave]); useEffect(() => { const onDualScreenLaunch = (event: Event) => { - const detail = (event as CustomEvent<{ bossIds: readonly BossId[]; difficultySlug?: DifficultySlug } | readonly BossId[]>).detail; - if ("bossIds" in detail) launchGame(detail.bossIds, detail.difficultySlug); + const detail = (event as CustomEvent<{ bossIds: readonly BossId[]; difficultySlug?: DifficultySlug; hockeyPvpMatch?: HockeyPvpMatchConfig } | readonly BossId[]>).detail; + if ("bossIds" in detail) launchGame(detail.bossIds, detail.difficultySlug, detail.hockeyPvpMatch); else launchGame(detail); }; window.addEventListener(DUAL_SCREEN_LAUNCH_EVENT, onDualScreenLaunch); @@ -75,6 +109,44 @@ export default function App() { return () => window.removeEventListener(DUAL_SCREEN_EXIT_EVENT, leaveGame); }, [leaveGame]); + useEffect(() => { + if (!accountId) return; + return startSaveSyncCoordinator((slotId) => useFrontendStore.getState().uploadSlot(slotId)); + }, [accountId]); + + useEffect(() => { + if (screen !== "game") return; + let stopped = false; + let exchangeActive = false; + const exchange = async () => { + if (stopped || exchangeActive) return; + const state = useGameStore.getState(); + if (state.runMode !== "hockey-healing-pvp" || !state.hockeyPvp.matchId || state.hockeyPvp.role === "cpu") return; + const snapshot = getHockeyPvpNetworkSnapshot(); + if (!snapshot) return; + exchangeActive = true; + try { + const result = await onlineRepository.exchangeHockeyPvpState(state.hockeyPvp.matchId, snapshot); + if (!stopped && result.opponentSnapshot) { + useGameStore.getState().applyHockeyPvpRemoteSnapshot( + result.opponentSnapshot, + result.hostSnapshot?.puck, + ); + } + } catch { + // Last authoritative snapshot remains playable through short network gaps. + } finally { + exchangeActive = false; + } + }; + void exchange(); + const timer = window.setInterval(() => { void exchange(); }, 120); + return () => { + stopped = true; + window.clearInterval(timer); + }; + }, [screen]); + useActionBindings(screen === "game", leaveGame); useEffect(() => { @@ -97,9 +169,31 @@ export default function App() { if (state.runMode === "roguelike" && state.phase === "defeat" && previousState.phase !== "defeat") { recordRoguelikeDefeat(state.round); } - if (state.endlessMode && state.phase === "defeat" && previousState.phase !== "defeat") { + if (state.runMode === "rpg-roguelike" + && (state.phase === "defeat" || state.phase === "victory") + && state.phase !== previousState.phase) { + recordRoguelikeDefeat(Math.max(1, state.rpgRun?.bossesDefeated ?? 0)); + } + if (state.runMode === "rogue-trials" && state.endlessMode && state.phase === "defeat" && previousState.phase !== "defeat") { recordRogueTrialsEndlessDefeat(state.endlessBossKills); } + if (state.runMode === "hockey-healing" && state.phase === "defeat" && previousState.phase !== "defeat") { + recordHockeyHealingDefeat(state.hockey.returns, state.time); + } + if (state.runMode === "hockey-healing-pvp" + && (state.phase === "victory" || state.phase === "defeat") + && state.phase !== previousState.phase) { + recordHockeyPvpResult(state.phase === "victory"); + } + if (state.runMode === "blockbreaker" && state.phase === "defeat" && previousState.phase !== "defeat") { + recordBlockbreakerDefeat(state.blockbreaker.bricksBroken, state.time, state.blockbreaker.score); + } + if (state.runMode === "aether-assault" && state.phase === "defeat" && previousState.phase !== "defeat") { + recordAetherAssaultDefeat(state.aetherAssault.score, state.aetherAssault.wave, state.time); + } + // RPG rewards are generated inside the run reducer. Permanent boss loot + // here would duplicate its chest and break run-only progression. + if (state.runMode === "rpg-roguelike") return; const bossCount = 1 + state.additionalBosses.length; if (state.boss.hp <= 0 && previousState.boss.hp > 0) { const primaryInstanceId = state.bossInstanceId; @@ -108,6 +202,7 @@ export default function App() { const defeatedBefore = (state.round - 1) * bossCount; const rewardDifficulty = state.runMode !== "encounter" && defeatedBefore >= 5 ? "veteran" : state.difficultySlug; recordBossVictory(state.boss.id, rewardDifficulty); + if (state.runMode === "hockey-healing-pvp") recordHockeyPvpBossKill(); } } for (let index = 0; index < state.additionalBosses.length; index += 1) { @@ -121,7 +216,7 @@ export default function App() { recordBossVictory(entry.boss.id, rewardDifficulty); } }); - }, [clearRecentRewards, recordBossVictory, recordRoguelikeDefeat, recordRogueTrialsEndlessDefeat]); + }, [clearRecentRewards, recordAetherAssaultDefeat, recordBlockbreakerDefeat, recordBossVictory, recordHockeyHealingDefeat, recordHockeyPvpBossKill, recordHockeyPvpResult, recordRoguelikeDefeat, recordRogueTrialsEndlessDefeat]); return (
@@ -130,8 +225,15 @@ export default function App() {

Offline-first healer roguelike v{packageJson.version}

{screen === "game" - ? }>} bottom={} /> + ? }>} bottom={} /> : }
); } + +export default function App() { + if (import.meta.env.DEV && new URLSearchParams(window.location.search).get("preview") === "healer-models") { + return ; + } + return ; +} diff --git a/src/assets/game/models/claudecraft/weapons/adv_axe_1handed.glb b/src/assets/game/models/claudecraft/weapons/adv_axe_1handed.glb new file mode 100644 index 0000000000000000000000000000000000000000..2da3b354e067170f4094ab62d6c5c37b4c2dea92 GIT binary patch literal 28332 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/adv_axe_2handed.glb b/src/assets/game/models/claudecraft/weapons/adv_axe_2handed.glb new file mode 100644 index 0000000000000000000000000000000000000000..eb3da1eb43e5643791f6ad02f807231bda4c0155 GIT binary patch literal 37764 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 IK%W`=e^Ql_u>b%7 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/adv_sword_2handed_color.glb b/src/assets/game/models/claudecraft/weapons/adv_sword_2handed_color.glb new file mode 100644 index 0000000000000000000000000000000000000000..5050cec16354b174971b3bc20b7580fa13344aab GIT binary patch literal 34188 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/axe_a.glb b/src/assets/game/models/claudecraft/weapons/axe_a.glb new file mode 100644 index 0000000000000000000000000000000000000000..43e713b01c5ee6384b75f4b2d1524d75f0e3f595 GIT binary patch literal 65876 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 b6usa-e%hpms#Rtns>3s#+kW%(fcO3nU%&V0 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/axe_b.glb b/src/assets/game/models/claudecraft/weapons/axe_b.glb new file mode 100644 index 0000000000000000000000000000000000000000..528de9b9dd22510983208a4272456f568b0e6d8e GIT binary patch literal 68320 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/axe_c.glb b/src/assets/game/models/claudecraft/weapons/axe_c.glb new file mode 100644 index 0000000000000000000000000000000000000000..60cb68323c65b30a98fbd68ce67dd91fb2ad5fca GIT binary patch literal 55336 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/crossbow_1handed.glb b/src/assets/game/models/claudecraft/weapons/crossbow_1handed.glb new file mode 100644 index 0000000000000000000000000000000000000000..a463d5e400276aa5ca2bd8964c10205895c1f243 GIT binary patch literal 14828 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/dagger.glb b/src/assets/game/models/claudecraft/weapons/dagger.glb new file mode 100644 index 0000000000000000000000000000000000000000..4b0ea4bf0f3b23a3d3c0dede8707fb0966d41416 GIT binary patch literal 9580 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/dagger_a.glb b/src/assets/game/models/claudecraft/weapons/dagger_a.glb new file mode 100644 index 0000000000000000000000000000000000000000..fa27daba36dbe4781891769addc71bd31969801d GIT binary patch literal 36344 zcmb4r3tY|j|Nr~bMY&A5U(<0JnN%vdbv~bChS1G*?lFpxP%%Rt<2!6F4U>w=T*_t` z$?bGbGfPsmiYRl7+&7A;oX+__U!QY6+eydv`+q$??{&`W^Lk%iuh;u^Ki^sN#tgAw zj8$%6%+s2&;iCimmvx#R5fL2OssFN0vx4UbFPa`1zNl0GPP6c8&oPUp&yNTRUlb-= z?CIj^;?~Kz)8gPo5uxGp@rFA-uW;@(ea4L7h=}k-5uN%^TGlCY>4IRu#tk0iftP25 zhb;)7A3Q&DjQDl$UhZD>Wz>8u=h+8;hfV(!Xl^dcUY)yKJ) zi<^g=r-!F|Z@0c4y}aB!o!wmA`t){l_v(wU`nY-Y?bXX`3T6n!UMBT4HwAs!yHDTV zeZ4$9`t|GW*4wLBZ)bdIZkkV`LPCNUjSCH4(y4zh=T4t2jSLP52@&byw!+!;T<-su zbN$P7z1(Zh_kW_(zs%=uy1&tb2Ko*hWp;-ie^b6!FL!qw(d5MRvDS$}*ni*#&Kf>< z>hD(j-Clh}MiRPj@cdbkvq3+wsV^P!9Vo^b{(il;yN7WDqazoE&V>BX=g8@cW|3HW zc=q!WN%oG}di{eg&FJp#=~;UMj}MxQ$44FPdQ~+)+y)l{b z)xW>Qj;2Qh4+@_bzG#er0E8Sme`YZ5vD=Cj;tql%X4gLR%;~dc1uqI3Nb4^O4GWD7 zU2J?|dL%ghNmL|#T-GTtVDzvt!vg$8LXH_ceo#QbsE>le`!Fv@SBZ>Rncg-&7N zBqU(5bXXAqy9k9Ug2b$tLi?DHw`x(rWE)}FqmiN!GobZqy*HY^!Cs{a+{Ed`_XtNO zlclU>?89OHtVQN;-mku4>W2el@_Ntwg}c{Wg|e*ueD%Y}{Rh@xB-DlX5uS98wN9lDA1SWy@v);| zc|7IJj>$e|;lY(LLgoLKs$d3`b@4HOwueyPwzDSV_wUq?R!m5z`A=Z}N9jYw`MW-i z(QHe+;7$1E;LX=}(#S{e^q!qISW_3?NASw_(8OZ?!kgczC+_)8Mf217jwptU^N(qE zSj{{adRMGlps5S*BRuXHr``?vwO#qo{2O3PW{kA z2oLuc%)w3Z+ZypVet%sh;eP}C6TlA(_t%(%o8n9RkLvBFs`;1mnZmtszO6W4^Chkt zbNHt7Up;@sTRIvP^-yHnKw8lc` zBk2CeQKQlm6x09do%N!gKIIr1Lj{A<*G3UH=SQPKk?_^&Tk5z zXFUI%`Tl?3|F3aBO!Y%Ts;D0X>HeF-&FP1K;C+NYjr!r|-@oHWTjhDX?UARiy5B)Q zwQGQo(y6O3`dK(%;x$J-uymnN7ydyl_zy9^c`uCIm|ovTCZE_kK=4j-5}bd?;(L#u z%+J)$6zamAYr)F~rh1pH3`uuCRbO6w-e35@vyad=_=$X=O_jQ*YKmYEzTvQj59e<3 za|%Zaht_6#4_xjkzmeQZ?lV3>&_uNtdPmIRoBi;K`tA>l%)n)};6Jat;5}{K_uh@4 zwUM{>4G>oQCE#R`96^{Sr2m#6N@ z-|zm1T77AnU=H5=t6%slbFT9?T}BBFR%=uvuf)n_)0)bs7Yq|xC$hv4W`sLu0ESI@pbMlc80ZfvQZcJLJ+A2vlOiMXKZyX%Omb5$Gp zWSil_=H;D*8=hCyKRoeLyH(FH1MgM~9+GxZl@r!4ea*3U@?*`03(@tu3O#IBtH=Ht z!@v1szEBtbK`r>v(}z_DlkCzz@Tn)iI&--2^B?6yUkA2;j~^}9KEXIl%+vDV-6`)>G| z!SC=N_#FgO{O%Tm(z9I-$)hhU_MTQWL>Sz(wa{z#EKSItJ=Le4o#*Sq+t-2*|Mi3P zHJvxg<6DgP{`rTYf|I7ZFgkjYrt8LZ^_%)9cysXX4TovEEbk!NA9L`-&KYX&aZGrz zb*ON;X<+)qvzFUrW!RMawNFR9lXVsed zGrYgB^%0t5Qw`RNAv(bhkqHU~E+Kc@I+AwT^fKhL%f)kyNO`@kxdBtPkp zpC8_mA5-{N$WNj{e$3!I3IC4oB$(n$@=;N>QYFbxIpl|j{Iu^dO>GWtPJT@Br$K&xhWzY*JV+zy=a1(!PnY!fCg|^2gZ?%HmkFkD zvLAu4A1>3Hrc3stDeOl}*pI7QW@zfd9RyQ2>GyHa|1-1nDoOv#p#LvH|A*cmqcI16 zZ&OQk5cL1{uqhfzKW{(QE?v^!-J!o{8}zps_y>^Rf8dh+@Th8&F4>Peupb8v_Cr$_ z-c2xtOZvUPPrY55~884FAF2 z;6Ipw%LG&SS@;j*;XibE)+Sx@A9lijxB&k_dvK#mxjF$ez&{@V)p zZ?g(VYMvF1l>dIH!h3qgYE_2sD8W=8K zgU5!bCa)MNG%j=zCiXwV=cjO*H=hjGn1jok4;0j`oiwz6bMR%_KtVa}F~8hDRo!9q zB>8;j$n=exFI7j&Mhd$&brb%)U&E^_x2g55M{3N$2M<^zwA#NDEf-2<&YFmA?NZIbALIlGOKv{nQ@oD<&7anq zFT5rG9R&Y!z`ui0IlRQbf8bpNQ@A8Q@{HBqlKjnw{MCc}4L&nUFb603v3pDY{=x4k z{5!ru{v>>f{~gALcuVp#1M*V<`KkV7xL^)$PJT@BmqUJ*Lw=U{r}7ei_Kf<@SoaCn+>6EuejGi^ZoT1 z{4Yxsf8*!fT%j_>k9!N>6#f_R4*~z@B?^tSzpmNM(xv^~!Tye8e<7cSYIZF7*_-x9 z@P+>i5ck*O!7g6tvWi!@&C%@M=OZV05!V0nhfyMY?b&w1nfDg*YO4>;!tb!pgvsM3 zi|3Q@cjEjK{%RwBhSi6HIXK}@G2%=6Bl=4FYiiuzhi-F(51%dccG~5jj?FmfZOEVY z5w;Y^@J$OEYpmmk*Mhr$I9eE&TfpBx{}umY>QjEC{rJD(X&EO~5`H4^n;7sl()IuLW;@XM~8~;3tLVajW`5zh>vuK?fF!__RMm zezK2XQ52)j&Tp)Vi65>pg^!E!7x71Bex+vEe=Xhy;qQA7h4c5PCSYI%OOY4s}uAdM3OY~WW^$qzctPlFF#`+8Mj`OB) zGxXyvUEk}|sNQPzYnL9!el7_VzP(k3x#oo^>EvL+6g~y;58i^GjDO|5r`rtm3pVei9RXvHt9s4IiTNV&~FFm z*L%+}&D(IIuPHpucz$idfOP5n@i>1j&Oe|gOk)a{&i^jBbpC&bm-43YZ6gEamv?@u zZj`l3mDTrSA@BB0epq2|;kLblX71r5>bK$fZ^760pD)k(ZiM=5>NZvQn{mQlCBO5D zH^vJ$h`h7e~0IeLgOAxZRI~$V+wCA z!b|vf!KWq=ehGiA^9rHsp4MXgVv28om)3&&3=B-q#rd1#{D-55i{~F*&|4GJ#6i>R zrz7eYAIwj`i1Yo5^VzC~i|6yaF<#@1^L;TWS6!`+PCtwNTe$C5wQ(3Ou79%g3eD#I zhV|>hOZj)fXT%!t?f-Dpti<{E2MpGj;v3+lyeZsbS(b|KFUjA&Y(G){erdBoy%O@b z2lDqeoaE0G9vFRJMfaEFzvn|gQU0&`>{CZZB=Cb%f;Faa7c+2W-3rf!oWYl6i)nm8&2{O3iKeYgVEc^!xb>XG_ zyWpn!>HW9#6U}d`pCtGpw=cc7hcx29PmY&cI|m3X{1$)a_hueQZHV~|bMXAB$-I?p zs7K773kASc68>iefg*mv)MT}VZ0NBS?|>t3La7sJehEKa!pHn-ipR!e0yg6;2|)U=IET@TK!(eYtUcd1h-ras3yEA0CzFHwVZ5c;o(Pei0w* zr=K_C-vPcfzd1Pe#~b&j`TT+_|INowD)orDOT_b;gYQ*m z%F`Y`RwW*e*4#LmF0SuCw|=^H)Ch6^9a}t4@8lgV;+umniSEzeyOE=E*t6_!{J^>O z<^P6n3MYIo;CI-wOpx|>?a=}6f7`z){4n;v75h&(94%mfBLA^}`Db5_5%-Vz)yDmq zh2wm3>3mpUZKR)f$Q;D8pdaXyZln+GuMQma2sRK8~m-Y|( zq?_*FNFSoFcs^6O>HgPj4^+o*Es<9Z-s%0z@d-lvGxzzf3s&$=>VGQo&lEmt&_p4> zaREP}Q+Lh2)4%e%3tfb|@KXNZ0q1nDU(~8s$6|!N;nUQ2mKVs|KR+`dW9&r1#r6TO z4`0EzFwSoZ-`{PL@OhKk^Vfx!@{0cBHNQ2iFVOvy;2oZyIrDGx6I_~qEcU+#`?t>S zqA|rcz-!NMaz2$rpKhQ})4KFgodf+kfPR;7{$)7-U&mrJrucvTGEta%?Kl3d^?9NX z_BY^K(MLGHxV~U=zQ5ra2L^+FMT9RTVgHudU5LJK!A1J2CHi80UgEE6*bwaRN-g}i z;lzKOPi=C(xA4XD$&KeD{!4I`i)yeWAA(Un==^WNMfs2$<)apy_GgqowIqMi`5-@M zu8Z^$<>T-9>%z^`kMIrjt4$xWFU7x(;4k<^%gO#+>i#Q#uxz9H!B=j=6}ujyeX9+> zLU}b3JyHG$f8I-8w4a2ZAV1F!cra2utHwjP6zC%2o5FYe2RuPu#J8G|tafWN^w_fJ z2K!nYzYg46f|LED^<5u~16*T`uz>3`Tq^y0DmXGXx|C{yMNXv|3RAH2*1KB%&(s= zt^Y23Y5lkHjr14iH>LmOKo@cS+HjG7ypg}P;55G}{olqn!0X^k@+bO7#`V=k{?q)m z;Kupi2`Bl3e5j4`VFtf0+{j;XeIx(WM*fTVwc)kz5BXmN-@NOoH~CM5-|)e4b%#$s z5Q?976Y9dR@J&Z;PXA(jj3_?@kN$%v|4sD2HuLBC#aA-b6_cHWGX8xLzc##xPmaE= zzI~^Ss$G+iTJY0YKlWen4e;9dG!D zIL;gCPyX*)@GE@RQJdu^^p_jyn@;$2{_rcA{7d8c3BNX6JU?%wzc{}HC;ye`|1Nw3 zyf!}hw?_KQ32vmnw7%#c*M=MLjr14cM*4Y6=jV*)2mQ^!uki2V{3i4l=>z_G)A~fe zci|i0wei1I{vn_CgMx3JyxH4+_;6wIUzz-lxWoL(-#Q7uzjhPq!msj8hwS1<`bLQI zZwhbnANU4%ZG78^Qu%@cPyRv>lV92}T)27%y$|)1`KtBr3x6uzgu3u+{O*>$_*V~$ zRoRV}*MeXE4}1f>Hh$Wn0etk`#{A)x-+TL74i%o;T;{WWY9yRmUCob~*i(2LUj85O z=Qg+aku86dcP#%ywRS_a8T{JttWO5=PtUaFmzG(1|9X0eu*fr)FZ;Zn&_b{f&Tn-Q zOyQrT4-v+-|1bFO{Qt_j3)78ub4|>87a*H_&g} zBa!|ZiT`brH>)K1*#-GIWsn~;@Y?)0#h2vgUHInYM>^k%GAortp9i2{OVID!Ru@fO zc!VUDv<==&Gj((DS99j9jYJ^dKvLQbz$WP$Jo|?MwTl^5n#|VRb)Pj@zk$k-i z-vF{}x=NzgoJ#;BUI|{-qyTI99rUYT`fcAMyV! zxX6FC@&3{N)yDgy^3;wO_kaGUMw-(&|0u)xYr&28Bi(pE%;1an!z^6Be1cG(pUa!h z5B{l*_ea(K!wKU3xc_-Qjq&~v|KEa({MVS=4-pRe@s{wxe@!cc{LuY-3tk&vIzRaD zP3I^5aRKK)gYyR%^oMwU2~Pa|H#pG;@{w+o58>_k%in@a{K5T|OZ>Z3KUq!s^NB%! ziv0TrZs4Es{*rzdx8SwqM;Z66^WgidgjoGr*YCdb7dqVE z#@Fmxss4Jgyb2w#PuKd_=#68`5zzgV|)3smy7Gbo#vh;xY`un=LUYu zQdK76tG&+f-rIlT=T&S^uRlIq#5aX&(Gvx@+7#aVv6av{utLQDt5+KT`^wGyz-7x- z>jp)M_@;2rh35&bmf#D%{XyQ@`T<|n^&8jb!J~zogo}K)sR#Hbk)Nl}slQM#2k#EJ zy#cO~_sW&O|EGh{Y3w}L1*69Zv!j0I1DgJi|4Z3be)P@acfuIG|h~;y}+&gn96Ju^cV3>;Y9zN;Ex0c{n91+6}<3~ z=%<$GV-D`u^nYrJzTi)~#6P#&!$kgpKWd3T=HTG3TH-J0pDxkg{hW_Pf3-y4cflq8 z5dHg)iseUKKPS>}$EkDtw<9by-1zn0T~19B&ucB6{> z&eHq`T-bcAt$ed8f&cY~eAmFNK*8d4AwRJ-C*8|fqB8|f#}rw-gmKZ(Ay|8!~p70Z34{cBA2|8H;s^huZKXHzsnq@U`@ z6}1Wd>cBx?2`=nmLvG3W}Yz4*@ zEQNWo{>%xZ6LVw9Y&qUa#nZAT}D<4VW!PTQ-@E!Z&^x{g{GHVm7Q1#zt%y zcJ0DqF~+h3*pU;9#~9Bx;$H&$0^=80BaU@t))=j^#z4%_A7g)Yn z$~)mnVci(V4q`mWPP6)~Axp)W%8ueMOLhq3A$;PPJv)iuzG#0U!S|jN|`NZ z#g*a|zhkv4>^jEltem}I#n{~oJWsLf3RaD=n(3L2RpFF6JT;);ef9|BBlevA$sS|x zf8u!tsui&kj3w+YyTfjP0(bD-W|u&}dZ1%Hu7DLX8_tp|#HlXh#AWOu#)s@Rt7H!_ zYbBmn*l7(YnTtowrgQtaPTX`n&ADK1BG(vWW3DOJl54=(W3=a5b9UT&TnmgXxMtie z?i(%{V=@=S4drG4Ybc&ZToYhT!Z?W=i}yNn-7$8@`&~ItE}k3B`Ed~#Be=O-E6$Ny zfpG=55bHQ`aTw#U>PT)1Hwfb(E`Zy_eZ?)oxP)7c_1kkDFm}Lhf&iU@aS9j8eawY( zZ7{aMs&?3C1B?x@(-GVd>}Ld?hFD>kI5xz39k~IVlH0<`xpv(DI4@3tQNeBCqPV5p zI*jYEH=Y~F^~Kni>(70__2k?zx^W)d9?qHT!pX3Q_88l9Z83)fw+7=H?rUx(w+v@m ziKipC3zYc=<2T$+yv1Xlws>T~?gUDA!Sfy04V3AOu`~BRmw>Z7VRQne!Z4cyMhBef zBW^h7kI|p|kQ>8I;3i|7%ms3vaD(y9CwQiEqqsTTe2nwCaoi%#m-E5s!!6)u}Wq`or-4~7tQtK`e5wCea`j5 z%!wEix!$0PJLihg6*PJu)b+sV0b2aPskmRbYg`U@5aU7aD0iIup3CIUb1B?eE|p8? z4seCsZ`^5&r@1t)h`Y`m!+4DQiOb_Ib3bGJnLEQ3b6H#l#tiNfcZEB_UBGyOD*=8g zcM{`CPK|#jF@qYJ`*AsYuJ9>yMbvp3*$FlFn&*}J6qc58t49mCm^7`w;Q4>G2 zKDBaH>{@8rPTT6QPES_t z(sa`vSrYxSw7Rp;o1b6rDsmiHEa=MD;N!92=EfhN%*M5)$vI;Up3<0dgp<*tBNX`1)|uU#z+u|P6?*q21Xd^=*ustU$C^hmiPrc-*`Nmt zZOO`C;_tlax}XqF=3ixr|GTX5Eaez&H1@VDMrJ?FMscHh`E`2 zW&?Znay*;YEGAJ~a_*K_TScPwNvhJ*L&22Ihq7`N+qzUgx0$f@5m$U~SBS-S5gAK# zIcX{LuMWG!2E|p|C_4~&J~^mQz2JCyvc>jf>%H-q{@ix`H`h7)2Q`QNAOpR+DntKZ zO3zoj_Ui*8Y?xxpO-*iNX6vC3Zze<@(0f%4inCVs4N@{$R^_d(`o2|h8|ty?T`_ad z{V}04baqp+9Zx@HmdXx8*;SznjZ>S_6?3r|3O!h!*-9T zvh1>fOY+k$Y^zA9ECC>I@0;*;Rp&!l=wiZMGk^}bf1RT&n+29xDt2lw-r(%JtzZv- zvhM4ziw(A6tNU3tn)29DHvILjvZfjRiMU-(TDe_nbHpo%mEN;s12aYLeDtVh_lTMk zI6jsM-OmDpZ31^xmfYLbRp+&Yor`B_>l~RZQ|pQoGiCd57FHgvyZZCU2FQj$By3MVw1VOOS#!wgvjnO$}U5h%=Z;mV4p8%J`we&?I=H7 zV8N0yAKFcUXsw4JD0g3UU}5=dTwdMs%GCFNFUe2G{Hpbyt}OO`V`bk*I5;D=@_R1 z%dJ@CEN+TY_KPrc0=C_ZWi7H%?5ONG;`J@Bt}IN=iY^F#db{b5H91;4$Vy6qK6#yE z(lEVU@MlW*hAa@GheIpAf}pvwur&5zTxX^Iek~Z~bxE0dK?6Yrg_K>unA}G8Y|eUJ z)I`PgwHiNNn3s}yKGJasPa(sjGdz1JNSfX5gTraRS>5$zCFktygW2pYF$t9gLo8?G zMrl~;FvqizEc7lomEGYL^Z{emF5JNamcN6obXh|1qzky!Y04rnc2Og}fel-U?lr>d zM=V=g&mKo!cg(uIY}lW6Q#L}D^ye=)LOSxgu6Q^~ALhd4VDhI}m=`ap-Pk{{in3{i z-fKWcKIA!BTk6R$9ri`?5p_2p!itfYGWlY5FJ6@YVXy7-?tzytPSAaA=%(26hX8g} zv{D+Ri1jRsFjXb7L^uM}?|C~c-)6SE4k!2?vj(e~h4JPGChH2$;fjDW z=q44={b;ix367b2qq@^&faUs^4#|; zqsm>k@S>1yB_RV3+&e>q=mdXVVDD<^4ok(RJC4~eS@1@t*xoZ^A(XGrLMz$jhD`Cb zwZe9^4qG_J?31Ax^boG+c2oA)tldH)-F1a+0jJpBIBDKE+-{#PUA($J)=d2n^xe{Iw$In<@KZ)z}xBd7etIM_B)rpxpnPz9^Iny}QPFfRZJh z2em;rmzDFj@XRWn?L7sZ2~F73i(Lt2!6ZJDlpO3u6H$i^Ye_ORCEH+lWS1Hp@zdGw z*OlC}8#_a1gX_XR%h8v5Dm`%{Nxd@n_T1WtdZ~WZ;6#D`1j!O~ykbKOM%p4NrktXN9bY_K1Ip(vd{oToG2 z#3WX7Y)%d zxH>p#%4oevK2mK48+W@HOmul=H$9s~snpt@CH?x;j&4I#IdlvdKfS-QFX=F_^jvpH zvl4u(BxP5=p0UA@^0oOj$s1a-DO1^9XyRD+vY4=P*rH!z5=Ue_p$)UG6F7_6`T7&< z9FMl=vY>EZ?!MSWrtJEAOd@XGV~u}9C$L6waXXiJLC9@Dau1u7)W0h3I@EYQrDrq6 z!%_toSexs^>iEuSdWG>_Oi9U^j;82Zf9s=x)PS28$M=E zqxHf0;!UY6I0w6y_8Yqki2*}4y#R3_OKYJkdL}JYGs}-2>50;fmEI08S={DGJ@W(c zn#CkIu)OfIq8%eg~a1XkF(hJ-DST_t;|927Tz z74LOS<`h>4k-za=e;Fja8y;b`*RR@szom}{G=L@t>`PO^*vYaPn~*K!hO>^jwz0T1 zpb``{#F|E1zZp^niHvXEdCa*zK3`_rX^Abc70WJB_ zr4O_P$vas;W;?ou3yMUs4TK?Hz#{1j&NH=hJGo+K zz1R>~5jYzVYWDpQ1CNa8Y-T?THhn6bTe8b8aGJATG_ailI}0IDGh8tCxi2d|w<`p@ zZo?&ITP)aN5O-J$aE`nqOV6r?iW>jz?H%M z#T}}EYV0?J?kfx%SpsdTGvgE+e}kISI-cH23})Gd!WjAwxt6Khb^H2Rz#7^!t2UrA z$p)Mbw`NMO7v%KLZW|7NLRauokJ~XYzS?z5G$|D*p}SB*6`HIcFD_U^2RudjeI_O)XZ;4`~Gk;d$785@EF#NLJb zXbWSRSXqLb{IaAymr`H}F(Gf(T6yxg=;QNj-#>9h$}wbeS?n}UcIge=J;f@lFaqxz z;syMXboNb50{o#krG+7~(BMlk*jRd%k##aeGo9(%_gd2U!E)wHGKt9V8fSlE8{RU6 zM(`=|wU068Y5LsDBu4u7eZ2qBP49Y)!7soaYYK5(s>l0=7%A{h!)e$?|03GP+Nzj% zZ0SCNqr7Qk>SSkaGB6wd*J2#Y2j-7bK=xEu(r#D2#aaHZUpGBQY*!i-ovyRZ*Hvj- zy&4+(SANi~!@}pOx6)rdpz!dKYs}aftI#0*;i+WAbwCWWifDT^*B`2XUf#$)-bCS69}v0^|iTo4frok zJiGa7Xx@HHrON>E_|JkZ#_xY)1HEbK+^(Sl`WFr-f}C*;mCjdg!3OQ@z_0pm?&&%DJQUVf9$nsG6|yHSTgnJgMq821h>@ zAro=TMs#d(zA0WT6L+m89Aq$OuinP#C}YzRd%$bd6dvS+6gpg`yx@kP;#y3$xMqk^q1Ja_eB&6I8}<9$HDIK& zYq&C$A_UDmoZ<&dAKcd}_&3;LY#G#LV`I3KCV$nyclY+QoZ3zA{^-O6gcz3RLRsLx>XLi6LTZvlx9Q^-!#@KkfSD zOUe_73%WS6-E;lmDei)Qv-*_vX}{{ceQyvvNqu8P4zP!OT@22zAP=q2Qerle5%B^;L1*e8V zE(dO}hGPq+6yYH$9#{JQ=wFMj~tn`H5e93*k9>IodyC4M1dbMLrtXguKc8Y#O z;7ph~YbQ)ax_iAo3`PU7bY;hjjhT-Jmc75LtVZ)JOUMmk#U#mk&t@53FASm$rg+`8 zVY7Sb!z-*?pN0eLCOU|S5filq5dFy(;41_=r;<)gMk%&;5q|44@BS^Q?+I}2F{^(K zcdnmBKCut+N7hRsh3y8v)Q~z7yv&fqQtI9P!B(g<^=aw~ZdoFLUzH4cb;7 zUg22j<)|dm8kH5?KQ_HTOEKK$QixwP!b^&NKjoAVLd9>rxOvbq*ei^-4fzTY^~WSu zhezPrY2j{r#i7E$_I^rlku1U#$HQ5yhIYsVEs|+32;OuQj*~6D&cP1Qej)6*v}wy8 zZ%Xp3N}d3}-d#5c@fg&t3pYQazHId=s|HEK^+9FUNBqTL0&dRc7hsZ@KZ zJ|W0rAGLuuZa0O3bJ4La512FwJ`O=&Si*|qvO$2Br7GRKDT!c9xz%T8g1G7gxAVUWCTPXeA{cTZ3agpgaK1z8Jx#6i_hZ{s;~cqQNB04if_(*XJmF z9809BWRUTadvwDcX0iNnHf%y^ABBC07S<(cL{0KK_^5$cNsRfDhQAsV=WrVb1|?xh zibYlHV5vxO%|HU6q7`UlDSC)Uaa|DIiDo8jRsa;vk2s!797~Jm_FuQS)^&w4rMKSI zLqC^z63LLtn+@yx7k1@W0~Rp*eQ(wr>gIqOh!l%8JA1VoE_w1g3J4>t6+5beaMhw9 zBFHf0YBjlS7CdYlr&PpIkW6~srZu^lLt(~0@zs}Xv`&JB#;n0Zl%9{EHFINb@6NFe zEOVuh2lfSVRoWhIZx!3>RfK@d^A$u0qOeZX_G=FP#H^5+T!ZaH$$@1q(7Dhn6JxmH z6xM6}s!sgl&#|@T*6g&l-l5#av4~SQ^i(2_HFzd5I}qN3734`%NRmnxcj2)Z|4UYOVEkA++tNITKOB5+4_K;m(W zYlsmm+MN_rB`_Xvm0K$B7K#~|8m|}*+Ju4?xF)cYg+e4kpQ?sra69_eBv)BXhP|wc z%RQckK$J_$(UKM+9gA3845qvM*oQbscISO1W%Q6LLBg6#%58{gt?aYc>qU3#i2%2x zxbnUBTiY;vC;AWqf}23hRZ3xa*on#ltu#@oZ_1o5C!r#Nso2RX{bmXk%W}VY=rn;H8DHtPeV$8 zjjWEbrTa$Fy&}GkLb-Pj3K$er;og(&dVxS2ju);Xc3Gb|1+&L(&e3Bk@+H}KDyBe! zs1$l@E^<0&kd(q5TaA1R0%L^2_QA-zKi1^FAB!7^+&QdpC2q6gDw6Q|h|S9b5?{4b zD7%z`k(9+nkPBrAHBe#Q7kQm+V4ogrNTEc4FEaeq;R7fzhF?sfBA5V=4oSy6aO@@7 zt?QGcEp1sp(I(hVuhyr+4XhWNcGI9Q9Zzp;LfRVd1n$$XaektqNO1%!fmcJS7j9RX zKI{y1-ZorZ3eVw%vY9x|P}l+_R&WP6a{L@f8zUC<)57m61bYtZ6-CGw`e~!Ovf$DV z(;k*vAiO0O{GmLsCFc5e(6_OeP(ic_jr3ZRc0iyTb&f7s7*dcgD1m{E*#}obxygKO z*jQX-ccjOgO-p!La*1ZQ$qUg2;Lh1b)DKL2$tA4U=k+e?Z$VN7K5GkLL?$8Sr41#D zE7`~|Vz7_A#p_`N=&BO0Ng^my6O)y25nfJCBkVag>MC3#q^p9I$O0+dUs0|D9MKlw zB9Yx)uLl{(eLi=KoS(+Z{Ucgmu=ZMb)1P zcL4^t5@9QxHpNvqmoB(;TJqXLH?qBO3Fw-iz};V`d3KG9P3zCHkcXk<{Ub}yrJ{W= zD{sZJ+&QJoP*wsr2^n|@68up*t}{!0*56_j6mJ*=*}a*9(&&-62Qi$|4Osys+$e2r zbl!+g(WGW)N7#(t(MExC4Ew$jCA$=H17ye;VIES4INv12?u(#WO-%d?Dq2zUf?UGo zjFK}H6(F^(CspX6%)1Xq*nVd9t+b)YE=KQcDY`;c7O_}sQN5EOe%}|#8>-j1+{eYD zl-C0$;U-Wi%rkg`01gr)8sJtaD%JE;O+!>WqF8L$zg%Kf@#R7 zB2!8>X``}G^my#ctnHIRH)(L4U=i`3geFNju-7Og1j(MPXThCGKCgu|oQ-4;Ra>~N z!!%)@i1R1JF?e$Go5k#CEDHmtl14xaXxG5+$NIRy6zuf8ofoV_@rdYGaKpg*WCRB; z_iv@uLvlTCgNG8)Dyd&as%SdTsJ_^qn+>aYoy!N?_qL%p1=0$pA{0cn4^I7Li|YuT z9K{?FD2bzHLiNlTv;OmIb*o7)wgVAYX<1jcS4@a=G_yS!6tyI)U?twi%Hj zgAv<+{5nd6;g0b z>opg)NgFm!iDTLZMI?`P%fldoIcD05X1ZJgPD zP?B^fiiF_q(acut;ZbWt4dIw&$|WuQ9oH~arIWHLhJlwdfz8-7rbf(YNqKeMA?AUI0BNf*`Vy zI0BiWNIhaS21}hvYTJWbPoahlBr^GTVVTasPxe%BjX8?Mj=Jj4q9@4ntO;5=5}BMo zqhCLx#7=%6H6?ajqu)%YLVx83?Zy4bEPj9KRhMN<8T*s`RVqJk$DRL;)hATDjfl?$gb*^|DM19YFNhjobs(GByU zQnDCAv$rofF%*1?OKoKLLb+IoAya&bvn{PzbLfgCV|&+oBr(79m`Cv?xwsU#UTuQq zQ0>1vkNe`#@7*53{#U*yQxsOi|BBat^JC|XQ0UBdD1Vb(c#wtS>R~D^qjEv+H6kr7 zmy}7Vb{!=p$g`TqkwLk|uA*iFM}F_E{aP*oS|5?yRw`Z~1$G051|0@3DPA8CLMl@W z7oR9mblNh>_cbJ$b=ffKb_oSMdmriP^7OOF6R{W)52XDmpe-8fY6+5J^Df59)AT(2SnZ%m|H4qyNBQDw%ahX$8+jo1o2yFRsz20EX zIf^>T$3}AS7?Q)j&^fQf+z=@(Xc22Ce{J4Aa=cWzO`y0275(uwKV~WCyCoXFi-97= zf1J@-yjKd*DMSQt_Qw0u5u{TU@))rw5jLTkJX3__l z&oMi)Gz9=W(Ll7DPL zi9Yg9BT(h6wu`K?uoCS9<(rUx^F#4f_b~+z8egLi9EiHjeAKy~gWwNgBajuJ)zB^S z$6c^mFQ6|DV|wwHeC^HdeaiW^dymS0NJQFEOG`o3?)A(equ6ZXDGOZYlfmDWyy;_vjmZ0Vo?h(Z-+7f z3bM$Z+mSbsiV1d73kK|t%K#{LD#_qZu)%7EGP9nX5Q8?S3|!b$))D^HV{+~TDiC48 z%or-p2=aCIAzWVsG-+SQB*L|*hw>WP9JE#-+QKawf}5LlL{|h>E4ETXO?iBn;7{=h zmD3Q#ip9EQL~^jLtS}yQqf~Ds{C*06MK@S#~EP!|Z2-KEt%(K1j)g|o!o7OV?OHmJjkh7@I~6nLH}Pm7Ho2zaPo z&_kJrq~}XL6jOmnTeOLY-W?UBbnyAf)#*>>%d+#;TuQ+alJ@L7t#v*2c_TgA zgCMi;8vUqk#|M>>d$+t26p7YJUP#|PI)N(Jy<1|wSPX&mxHw2Z#2GoFNNAjdLKe=0 zI0&vb-He9lD?zmq7wG`*q_zS;2l(Gjy8!&d@VdI zmgn0Q)SRfYxPhI+UZR*=mE49aMpbhrN|8t!eD~L)da~R~yV#jFl>1nSFax4}#W7hQ zfHW4QlzkLWVU%w*^COXEp;MvlC=Bzh4ioQ!y{PCB^_RsgMC~80^jh8@`A%x75h97K z;}CA4$?Ic;K>FaR44FUbctKuZyVzYMy8r=#U+W4cqCW)gwdhe%WuP($x@4Ge3H4i1 z!NVI(J^-Bw!d4gIH2rbA=igcLNJsKCDZ@}`UmcG?2+p;b&xKE?*kSPg2W^MrXoos6 zQaVYO81{``HtKqTn37@?J!dHT=2f~P^el-%6zV7iqWwTWR%XjUtcMNo`hA#xtHo!!NI&f8^^47t*g%SKI+JCM8owS&V>*ip%2LL0tq1d z2Tim3HdChV(Q&f#NbpcPm|`HrCMuSC9bI5LBt~OHtPCnA3td_0je|Zy-HIZth6+Rp zX~@+gvqx7PE>H&MLqg_R)Z2-8ycQ`% zZXsIVWQh8=LRVjEB+G4?0V}e-+SNmM9tt8HEmBp<=ph5$hQV1LjbJ~~O^Db6Ra?ZE_zr3{G(zlw z0J;a1C~P2$y@$wKjK#4KX3i7q)0$iaFQ{H1K`JHz7aBTBc7^C#Q82}3qI*xwTwn%+ z@GoYm8(i3Fq|Aqk+7&?|@v;UPBg&m3_FRs%2KBz+ULHQ9vn@sqY%`Pq91R!(HoF;0 zVlSu}WG%e=YaJdzj1ff6gO848CipTiLHRbyP9Dmm(7Zkr0a$|-afmi>ncHQ>bas+PgBJ-^-4j8F>oJD1j28v;7c6;CqX?XBo_oKamM}*LFyJpizO+6xj#$zrwy6l?LTHM(IR> zBdtD>tzHW2Ca{;=_4`Es&~C~yheyfs9|WbBsM4?m?eQu}7e)JAiXodPwu*`gltk_B zi|}E!lveMi!)X&K@nRvamZbva%;IC!mTRe0S)JGWGc*xkI%9IGVMc84 zY|y|5_XXtZ2@f3!r6sJ8k}4>bqL@J4erQV*V?ahttrC*?R zO6PH3Vmok5?;_z3ry5*B778h}dHA}rV@@Sej53e##F~I1ttpO&Z-^!yw9V;ZohWS& z3^E>-)aEO?UFh9IN?#Er#_UG7q%x6mX5nELeW{6oxKIbb<*;x@>pN2sL`(@aJ)_ z#H!pBxV(<6SQG{Fw8aRQG?E1pl}V591yM)eEzk}9Ehz5X4LHUcq2Gw?q9Wc}CP_L> zM(WpO%A0nJhH4GNtfC{08wr1DBU)ZzAVgih6d6!4r;<;>&r?Rq3{AB;=(^G&b&|S{ zT2K+mwC+{|4}c9rg9_9r7)((oiF61w6DDPw(ISU78(jFs)Rm4TF}hFP{Hcy#JS7G# z4J3>(<4{_Atk1#Ksu67x$`ho8S#XFFw;j<*92s@dN=;J2LLh^P=n$?o6fmN2%D*8B zEcn!NwcCB_n02b4UPR>U5KPK0vDn2|HzArGj@SyDM>UWs8&Me?xjKaIEycL`+yv6( zVg$Ps87;B58@(}oln^N-QgWXF_7#F&crg}=;-1lBbSrY;CC4VBXQl!rB7N#NhA9@( z>CsYvkQ)oR1u09=ca-N|22T*K73hZuy|MD-t9p6RR=5_Jg8E!rqDGId!@Fo5(3V7E zpRspfn2R*j5m;>}_85DIv(TFi89nrA2T!(W1tspPgn?nR+jyW_aUKrw7VKdil`uIN z12IFe9p-!%+$uX3h!ZU=$L8QW(n9FP=X~khU!j&m=WJrnl-p32g(aX=FB%zZt}lrW zuh;son`dtQfVP)7P0dK8L*&slhbc*NJ3=^sZ$G}N`v~8zs4RO!Ei&;%R3U;i1Ti>6 zka`3)@b4`ZhBWh0hC6rqj85#mAp2(*n7Zsuw`ku>zd&l*(sKd_14)e%9tg>**Bt!4v(^oF^&)A@73z7_uw!8*^h$^6P zQjlMCh4*S$lzg_>`%Q0qJrVNYIKd&NB*#(&v3w+&z(_{6so9OJRup;SNWm*genN58&}q>G9HgHd^hXuIAqkUG z4{l1wi(mi|qMDhMTe%AX1{5d@&ZHboUw>qvYiuAXIQd;@U@;PLAhlvh z9iZhvMy`*3rEdk^%WUL!$rk&~Dn{ZT7ZS>!A~Qv-T?Vq}XsIBXng#A>6Qxntfq9#G z3O%A?c??$Un6*kg8Nxry=9E`z5QqjdgXc>bTSVt+$gLK@_lK#25+|L0zRF?;Xaw20 z)Y47a6%}E$9^T-7gLeq~kbsLN;yE-lc$4H_;Fmdwo#&z_@C8+(pxdd%07X6&3!5SM zfODkBOnogDvvxVuKTM zL5MZaw|wpePFpTOcL&8FJr&Fkt)Ep5KcWIfkp_&SF8N_oSqc4+0X;2+sJ|k5q}WhE zh$W`aQCX%|Y(eogHnUNg8m}W8w3wYkM-tjjk!m=`#)zI56541l5F6-HORe1u1!Yi- z>YFuGoTOYJl5Gzypcn%8zNQ?AYev{y+zo^GOFjRvDf92sFOEP8WNOAie-Z14vSRF! z((Zf1=5d^|gNB>u@n)`vo?KzE2u`X@tf$~r3NA{}ba>suihd@MHadna1fApcCpNfd zEWB&u--z17CC%OKw{4V6ILWEf%2?_l&fdrf?q$m>7-biDB zC2zzgOYw52jHcKT%)=2Oz4>J|RCtAsrP^xjeN-Ia!2pB+<3UqZIV(R03%xy{mQW1#?YN7BWT`A3Y;!zC+xuQ>1Mpd&xS0}RJ z$lJv05Xao44oaNCYK9QZv5 zE+=em7bGciA0o#WSJrqfopvwWV)9f1JZg{|glNDMK=Ye;eY|F&GqmQ!hMuyhavP`q zYL<*n2eCK9YX}Ho*+r~lip?;TvY(^`?NCd`KGR;cTI*S%F>VDn0g0nF)Txh}GN?(v zQre7P5I(028%RQmUTlOxhGM(eGCIRL1wZLP+L>i1Brtu5dM5Bi4Ma4PA-;3r5}m(t27X#oIjKh zj6K8;WMuaDnr6Ubq$sfp*>L>YzP`-$+qjN>t74=+#Owk4!pAlrfTi@dr0B zb7*T@Oe;8nRA|4o;#o0~v7rEfl|z=!vLjE6cWEGbiZ7pnS#9*HTG{r}apw zB-km#N*mcKw*Dc6y{3X9dNf~Z&YKAwUFu} z(IiuV0woADsdT|F?4T$?h(gqe(fqLzvOoqYpD2(GHdC969+zZ|i-ucYfvz)9t0hBG zXa{^`5^BZA1=|Mi)V;EU`Z61R{>V&zCMCJt@Y z)QiE!;1@f{s5RKZu!GD( z5o?Xc6a8T{8jN5S==K$V@B$-8R`=y$Ty5N*wZ&`hU4$hU4H@_W&lla;9&f%9JsB!V zP+}ZP4dlv&Qw^U<>`P*{6G30Zw>7luM$u z=!D3!!f!(xP<iyA&f~G79R{hPAANfEH}KwT}j15M

`;bOHcPz{QL=o$vZ0AjhBb;vTptw2XXvZBey5lSOiKl;VA zQ^E&uryJY_{uZJV;OVDF&~cWpW}^bLXF$S;=TJr6A@1E$5pG5LU&uO0gE9 z0p8X*NaV@gd>N1!Ftk&mrz`JpVjk4B$6Yc}7947mP`a_f4xX(OEXW$)ux10d=!D~+)iozU2t37?&4T3zQR;hi zm0f7O8qgt7_MQ^~h%{~=^Fi{Aa#ZqORqG=k-IZs_;lAp}pGiKlFA8pKRlnA&!7Q#ta?Oh}GnYc!=C+?nJXyR2P)U+5x%Uk1)N~!`%m2 zm6NT6V=Yw!TyNsOdR6QgzROi4MWv>5_flMW(*H+s0s?Vu?E@UEE4nVI^%K3o*HI<^ z%7udKk49KvIh^c3r!NULFrR6l#j_SXp&(jIJK3~RaWs6uScetg0)%2GydGNO9jGBv zAdRQ^a1x!M&}tw^3tS&Fo(3tzIJG&1BgMbIyix*;8`Ypq5|MWXd;(-%J8A|0Hs~=i zdZ16=E4mW`Yr4w&dv8I3V*H3->s6WO)fk!=sYGA;$$dpILs4B*!hN-%Z@^OjKs|79 g=Jg84K5KAVEYsFE-#(mZz(&~FGsXGO@+xor1Nl&)*Z=?k literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/dagger_b.glb b/src/assets/game/models/claudecraft/weapons/dagger_b.glb new file mode 100644 index 0000000000000000000000000000000000000000..069b08a3fff7cff2ec3bbc7a717f6b3939110ff7 GIT binary patch literal 38868 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/halberd.glb b/src/assets/game/models/claudecraft/weapons/halberd.glb new file mode 100644 index 0000000000000000000000000000000000000000..fdcb041f6f2c4c32d9f9bb8354ff44d834cf90ab GIT binary patch literal 82980 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�p{_%DG*l@*CQRc9o7DZ*^W0p{;}^_ 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/hammer_a.glb b/src/assets/game/models/claudecraft/weapons/hammer_a.glb new file mode 100644 index 0000000000000000000000000000000000000000..5dbb4cb3a27ebda2e682c0ee61aac77cbecb498c GIT binary patch literal 42108 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 UjO0bk%%9G==e?gS{Jnnuf3Lp1nE(I) literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/hammer_b.glb b/src/assets/game/models/claudecraft/weapons/hammer_b.glb new file mode 100644 index 0000000000000000000000000000000000000000..67c844b2d1bab3e3d8438121b75948facba8e8d9 GIT binary patch literal 47848 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 z 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 `-i_swEN4z;g~t_eE5R5*5$ zpDj*tk2NVN;PaBJn8s}rv`#@!I%E2|UoO+<^KnR-EZERu_v}nATF!LGa;Z60v3l6M zviB8o?>`tok?V$8n1+N#JFf?;+a)*ryfRWgWgW%3w)by#4-tb7S?~tVK`hO|T+t|L zjDcxdKFuXhmr4k%iX}@}7?;aJ;5?`JD1(On^|R*Poq# z!P;ZM1~y7MQyKx;Vp+AM4%uKz@Uu@C^LBPaWwybLyFyuXF#jd6kkoh@j>hQ*V!{!7eamt+`Lk&d+vJgrffIh; zDQqS7A+)?id*x1V*p%Ii%Z-c@W+A#*%cnV64Jq_pjk7e?u}(v6+Bk8{1Io}JY#+9w z(VekxJ7(p!Sei}rH*`(J*VzqS*uA5)6r$$Hfp_(?BEhye;^s9n!%C*)8KwhHhCM0a|K^*x85 z6nX?4avAVNHp(C@Y2rpA?VAJ95l;HD*Hoe0t zMPomall49qqodif>?Vt*(xie(=pH7ace3B-jq{9qX)MPT-pQV(Ju*#?d0Xe%o*x{# zea|jD%@tVZt+~yx0}gZxT2?FwlW_+J)(d;)#+XRsSCvbQKm!u*Vf$DVi{*+>*D;dc zU#}u;%F-ZAXg6CVVgDXwk1w%5^4}{q?ZR0-OAHfS&wasmFK9Y6v`2m8`vPu~4u6SX zj{VS*1kw0rA)`=v*_d#N(duM;n{lJklI4ijpUgT?OybS&ir~2KH`Fq|8eva~;x?vt zo-(g}%-)e!*Fya#?zv5NG3*O4#>yZZC*-2O@g1qtUHdSY5pbEznDaS)8BBVJm!r%u zTIyU6+Z}{7{I6_xb{?GD)EBZBIoB(%IWHcTKK`}f8Q$&g516XYDSAHV^Vjkpf)hWU zA8lGEKjq2a~bSc6toV=y3SQIRA+ zmjpO0XlV{62&_dcb1=H=)E5`*E|X3dAAPNF;~z$L$&IvSCpv$Nd^_7y7qpC~>MzEP zej;9(sK@M-jV;>O&xakuSSyDOGCF5{u3g=oj19!Q2lfspT@H6(n4>Rjjz7^k2ZMOU z*XkYYuxDb28?xirntx@PUJ)2I8d`SWZ*3AZ+$~30;}gSkF4vx#P#Z5fXKWl@PZjTU z?U(qKUlq^h826fR%~NXog{N3QVJ!AWnD7uMG@)awlo@V69>b!qc*XdRifMiA=`SvE zXUE>WHya(9P&+zdIK3grqtJ?EO&g5Y&)DC-0zV^#`{jzHL+{7kX<& z5TUP>6?1(rhJBiI(#iiR;_Tz9D${;{ug$BWf?2*~#C9~blY$A6S;>0FrZhpRtQpe8 zPQ1wI&KS|j$&uA_{DhV-R2Bj{ZHok($_5F_n()))!S`e~qKCUhez8uIu-<@+_`kDxyxs~^qq0TPI52;G?v9s3H{`#7) zHAo1e5CLenT0Cp}K0E1DO2PTw1T&lZvDKzFgB7-t=$iw^yIUX|3F9&$%XW0eTJ=#Z zfYm3UX}87==~;}quqj+hdiNIF#n6CD4~}mfuI46k+uk?({PpJ>QkpYWmz-ofY2=~g zi@~GKrX-(?P|NSISM`UDIS7EAt^pz(vE0|j(O=?VUpdfCgYw2Ql#1`{STjFmWp5Ad zI|FFXRTub;FYk7=%J#u0-x?}fzyYw&0cfP;ow|0VG?)YOyc~9l zw)XiSx4Y9kl^Njr8G$+QG1_gs@yzNW&W)dhUgv(u&g={OaDgThz?@evvPoJ%n4r&9 zzax@nv>)Oiz3zhiOF1w>1eltyrKwEUC{?=HJg9TNHg=kms|JGbrpy;H z`buSYSCEQP=q@aV=pkzQ%k&{!&Zay<2ES9&G|q!LFkY);X^Wk+%k z?V-drjA?KwI^&20-YUk9TOh_+D{Yvu)bB*8J%Lch1C@2+S0 z1C)e`ACon>y+3>mXBo76y%`w<*7sam2-w#QwyI6Y%#5=DBG{mtt21K2xO1z)I3dVX zPR@AuX{qqoT-Ew`gvvN|ml>1eW4gHK`ztqtQPa7T=6vplF3%~6J;4jJl1!2=-D+b) z%(8?-2mt+~S)&k*5UVK{*qt6TuH1z*ocDRX_!3oHYV7TFd^_pSR@I$$LZZDw_M|`| z)RQ`Pl*$^Ul?x4<^S_fk-T2>Wkx(De}tALI_ufnY%ohl-e4O2ns{9ufBQg zvuAE9vCI-7$Z0}|2R`wH_>W83UM~eG*D~cJR!6*)Zn#>u~BmP!~NK;==^ z$#$*4_>&6_x`~h#pb1m5J*5EZ2WM!UV2KJKdOa@8tx{abP?&XaB)qh zC%%7|?4Zn@TP;!akV+7&4R>=Cmko_My2aLT>s$)3q-${4%{#^_{!U|v6v$0F*E%IM z+*LQ&<_m)}Q5>bZ1iYgw4MseWr!B`H9#c)BTw0art=oMc5R!IGsl=+{oJi|`{ShT( z!m@nY^#d})pu|$5X?_vx^k_&D=CEq+im%%H&Nz>om;Q zFLD$FZ%Q8<-xOt*&vzr4EQjJJ`wMpsS%xH_9`+VSO1%lYH%sp_t%|oXFoaZ5_cFUK zLbL&1lp;?#M0Sd?XS~{Eb1A`+>aEX|i9mFUzO@2P=Tk%}YHTBV3&a>I9FvXi{$qXf zt;JLz+Bs8rkZLxMBf_`B%`Yu1zdX*gCZ1v=Ma6~W(ky8M{b4iFb>3k6Y__3Fgn(2O ze($P#gcyU1g(|WMAUZ_H7Ithv=2qvD4Iz>0ZaotspYqx@z`*e0T4JQqFXIkw8zJ4A zpGkkmE}D9-UYO#1)eWkV)(hy708-#S+ovz#Mee4$=uyyvkU^Czw8n4Gu)a*X(GO7U8vcI?8E7ryukR++}%^b2#2-_e+9)T88Iv}$Q z!|(B_2<7eifd=726Pw^ z@6U!Lt<&?p@6u@1FvGCoA5`7IBoufK5_nWzcdDv6Kg;n5jn~CNo-xuC8NGlyC^W3; zr~(LXB3oYrZ?aAsq|Gj09r3$eV@>QB_3ls+yUdb%oG3eI-+oOYRTsO8RTIxojK`ms;fstPSyRd=r8 zRQcz;arsR)jUCk&=NGfX(SQ>K<6>#4r;gUA*);6h`xyAZV6O|flRZhbz*q&aa9-Io z^i-5mnMvEMc8z#q`odz|dE4tLCnh5QQ#Qig#Jt8x$aJ3DqOvE*`TRB~YB^$$xGlzx zdG)R&cz#BS0?9oyvT)aMr;D9(&!-FciopHcp8^)LGwno6b^!B8*01fm;MP|{4&r-z zYQquNOSUFjuvKZlN?OrVvz7RZHyg{Dik(I)+g>qNcnYVLO@#`g_5tdbI69$Daato{ zrICy@>^#U>ozjLrud-JQIP(@ZTyn%r4w^k_5!66Y{DT$+fk3Hi8?aL%QnY$CrvrHQ zt0)s>0=69`GO6fGOct~)A3zjf+=e=RqbUZrFUWyvb)uXN2@c{ zVL=b!KuFawA9yFhg)8koDjWuslA(V8+J^3(bOSlF@=Hgx1P61G@ri~n&~VBDd9J*p zac=Qa*;APySP#p(D>5s?c5Gd2j!$i`8v?rsW&vz2#6eUK^9nMC3T!xTBGKviRM!!6{kGp{bZDAV$cizSTB$I?pKP z7%KB6-ff+~=!0)Ul&aX0=vHY*xQ_XfKwKfN$478G-2Rg*jTb-o=j5}@|G{BSv)v2+ zDz#sKf5M?WdgeInZNEdnmm?U@(y!uTv(zPfbP=UO|8Zzn1 zp$!ygGsN{=(YN_Ty#48C{=l}pw8ajNb3<6CU~I&}1Bk<^^qk9D8T4UoF2NP)y6|Zrw3SAr*P%)~k88L2c&U=R9mjJ3;{akD z@%8rheqK_zH>0-UwUk8+mkNqm`oUqrF>YFk0NSA{yHy%Dt9n2!fl$9chlYN&XlgGp z4rR8v)ZF0TOQQ*7KOHU*_hRMyH;T|F=yWwMr#HI5;|SIBLG&i%w=~Sxz;A^-)Tds< z!0Grl&){?YhXp^wj6fC7jhbHY{+mp#i}Z{ATwYhnEBC6?&l#!XhKc=$5E(7Z|7_jX zNi8{1!kD-{lv+0gjr9LRYcSu??1}$>6cF&as+|{fq~dj}|mh!v%Ir1cSK~e-DjaEE#G-{pNFp&1?$@33hW) z;9N`9c<|JZ0`3>~!>|}LLB|=AABf2@W0_S(8mXCe+C}n-wea>U?k+2O(H(hp~WZsXs*k6O&}qqjvh<2vE_k8)2$~ z`^7~T(1_`WT`1B^pyqFm8^{T00a^TcKZuYcR~fR2eW_lr?NT8}i1z9&Us$;M@1ZvF zAkHkPF->ASb8$va_av8@J# zr81((mEcxVc*DeV5{~+^N_KpM{^w(F9C@jB{a;EFP*622W zoyYMVPtLr=%=J``HM(#$v#=r&4c_|s58=+{!RX?w2+=p&0c`sDUq6#0$J9_p<9H!ehUS% zvzIEAKS{ix3+&YCBIj2eAaHF0n@Bzcc&!nYI0J*3^pbfg$MA1;g9nXP?xSb2u-*>d zW){``%)g4x2IM>~J`^ zQ6py>{SBT#2bm>JqVQ4L^!y-?b$C=dcZPR}p{`p``VRn&0ETuvva~UiFa~^*smyz? zv;PkOvvWDG8UF30KvFM0@>M;!hVfodbf^!@a1jA0=7;MWW{$LK-VPYfBM3Yq2MY&+ zn|!8fI!S>A5Jro0LW3=*fm9aRIQkL3l`yO*6P8emR*SMHB@P5S#Ptwl&ae4#yHbB> zvf-p3iaJNj7b28tfsko9UCRx20WBB>frUihoUr~K^y*_noz0^TF-3NI6A}YQ zXb`K2NO>yBkg;y_08Xs#;JF&HK+q(}3+OiEM(;+ywSILj#f80~4V$hJypWKq>{Bcn z4F%0pOIvzB6eEO{<k9EpTn&nEV}WHE*@fK=A8{G*M=tDn1SK)?Az$ape~6PuDj?UT`lENVN!< zA54_InG+Z-WJ+a`-qXqjR|p>dwW?-oygGh8cdjsi zFt(xuwm~ri$i@;i5ppfkJ`dh@m_b~ivTtXE_-TYEgQ*ivb*j|gFa709L>Q`AyhUBE z2y-07dqsW_?MAZ>C^pl#IVQc2WT$jAP$aE-8l)rPfWnqZ7DC^6m>eCyv@p?rIhbCO z?znXnnlk2e=28me>uw0VD|-Yh^nF`HMb^_Fz&Lrq}5NWxz96x7RWN8yN*NnAgCZR#nJcd|6#sfYZ{_; zJQC0cPP+ONYU4VlTZ$UkX}_cKVf2&F$DXac@2@FVvTkYILZ0!soGxMeMpaPF(@{|^ zpp^UeG=gD$BCAgh@U~?GqEqQn;~D1MK`IW1CI@F@;FpO&W>)qPZ~ta0jGp6RN(E~E zer{oquJ4^E)D+F(P|ad#gVRy-Cs_PoJEaw3VrO<8@P=#>tJ2$Y=bs1>aJg&ZRMes3 zJCCvkbEy}WZyE?4LFtdGT|^bEQp^NN`w>gi91*8Y6i%3X5D+#g(18|cvRjMd3yp@v zP=Ze%$foXT9E{F*TfhLh8oZXnpeMvQv4M!Ag}xVD7)eFpeud5D$HG;{Y>{8+I;U0| zFY!14(~}7PfNFLL6$%M$C4W6QzG+ZaOq55E*a$XcjPQ7{At4@Ob2igSWP5%?hR2}9 zd^PMMyN9H2uF;(R8Z0^4WRvLtf8b3e3UX#JuG7v3uS7}=8AhP5Na;!E3gs|NSQhM0 zbRYVu2>OD2IUN~l?ma)W#~H{9jT+Q9|K1Sho+3J+=ZHrY_xbnBAn1+DMV0ZBU=8AJ z_6o5IrByuP^uFQlWDMMEViuDhOc6{;k_1aZblI73DDN5q2hUSi+Le0}$Q!4+^idF` ztr@O#BnLvJ`^VFnlIZJU-N?6K?wnkBKn*2tBy-U$4Rii-(is^wugECBe)0xdtBb2@ zAWcPrm$ngkWkBd&z77RcYbvcmetuHY%m~#sk-7>Xoz!fW2r5iw%w#_ZK+PkdLK|g+ zg>}l2&Y?4LQS~YjIbt>x{8~w+BNCH7oxVVP{H`Ypi8RPzWQ^0a-VXZ`sO8r)3421i zu*%M{sCL*%i41i;r50OE2oyxvA<&uz3=0?i4Hnq;T*$`hy^@%n(l1#=^g76-^O!1L zdwe@=_93_x504wDtf69qqt!v(M`_;tWrlQe&0yD|Xz6S>*_gX5jub*l^Ev+QWk@e5 z#$k3%+o0L4-hlx_JwaxsAB)JYc|CZEe0h5!1yF8oG(FSmCU?{_qYoqqv|{~W=)O8Y#`msLdEYA(J#yOFOB|zV z;t3W5MlBzkh+A<4K>QlduvsijgJGZ*f}M=>TwqnSO6QI8FY$0-N4XJMd?Quf`(?Zw zdCw6s%6bD^mPsH|FF{6s=N~t$3Lo?H_n-FsmDo#3z5ik4koz|H_lrn!Vs*+s{`R-u z2kz%@|2)`tRU$H_9pVsi8Zs4Nh*J-#0pEw1L7BN)QF8}B4e0EJ%)jXLn&(g7k~2^g zFZ#&cOO3|$_6z)Z%>y(w)Truhk}M~FuqhFhq4-7#thVEWPyxG1f&kQrG5m_3r+Y=2 zsUiNB6^Z9YNh{;ZyW@HBXOzpN}JaGZ|0Uf_qrF6rbCi43=5mR>jiUJ;G)q#i@=U1x~$Ky$o(_v{px59{1+x|RUVkF_TuKYT2|z3&$2v&t>;l4Zf5Af8U`^y9 zw^=6Qd%m`JFoEQ}$`CHjjpTu|&U>TgLczpM(h`gF?54jrkE84EISM|PU^2$4=;uGT z7!|C}5(0PRY)Ax&$j@ow`P;`DQt`_f_&j~K;rvI8%E02{ue#q3H2y?B9WUn^p)$)y z!=l7l%wG}7nO($?EEwzGgQ~SgkT?>&+5I=n)nL+M1Uo379L&N6h;?&u9ZTWU01-BB zy4`Y3k^$Csj8q1CI=3g<8cE(WzVL;Y6X{I?(5ezeEm}lKQ8(ci<*+>_X=-E30p^{o z2H8Ad%J@dM1W(o*p(EXrU&PIHHxIrE!JvVv?8Bnb%v^v1?T_H3@a8A!z?zA~N?Qp^ z2Sg5>0)6ZkQv3P2!$YSZi{yD5x)A;;NSePeGqX7Q5b8OR3OQ4`?EX<%X?z{VTh=FJ zM72G}6gv?1nSL|qU&sxjD_xgPz$}P-MX`mQ*P^Yqf&CeEG;!(aM?8*QtPyAD*r@5& zBphL)hu;}rgASPwWfYeFz1XNAniP1!mpO3f6=VW0iW5a|mxuu-A7Zb^`B&R zI^9l}ATnJSIwrzh%uF(^9f*(gcL&s(axNxKKpX+MP@6>`x{=&;ifDxXQzK3b^qS?5x_ZfnA!{F4!LXZR#YtJg1t_^Zyr zn&pz_5~M%v?*Nua2o?)Lw%VIB@BvFqkTZ^@p8_Lk+h`RZ< zp$s=){-osmGSjZ@7-f@5>=s$BW}PurT7yq)@L%>;dNs$egmb$pB9u@A$=JAT0-qt?97_anL z@m7m_aU4J}{1XJ@hwEcR5YXo(sjZ&)r1r8TZYgKhn#C7!FpS&H{BSLO94R~@5}=q! zVyNYTBNIestUK!$lB+RQpSWgojz6IuLboXmz+=9b1f@@x5Zo&_P$CX?rij%dP9P)` z&;nm~{>}kFmSl}Td^%B{953>f0K4~82UR3)y<-eF=AekRDL^jo2siVTlwu!ZBwF|0 z7p!}#C>aV}{C{zN% z(O609<0-S6@|Du7d_njhQP@}#Y4kb>gF>-WM@F9xtKyRmWM`@Nwy$k z+~vu|oO?unK$iZIA@F3IS81qCR22c8MMpGR0;j_P1d zWOnVf^NDd&A1vttR8I6gjyh{^@Qsw{Qo1#(>f?i2N0y$X&zPKk{lN`mj#yjKbgqR8 zKTc>Gs*{ozm!tDE3b@^a`Hj%CIy};O)SMgv8A8X8LKr)VG8X6Bg>&y>c2)=-A#cm zLS@Kk&qfrMb%N1NfqTCL^CA0{`{x8DXFM`2)CR@*~*96m=a>ulEqMq_+keomqh(uAg#1^YE;_D za)tsBXsZNAEY;dNgO~nTcDV{_pSjkC~8?i zzK?A4SFZD~&?n$R%CRWtqPs(+hcoi?Pi{}di=x^KAaq)PcQ?SrJ2Ulngv$?L?7nk# z;p*_4p%m1z-ghruz|GQezG-XBLDzfAVHSrGJhAr^G?=gYN&D&#UKnyRbqDrS+EmY` zuAGg7{NoSW#E%aK&T-3?`-`uOnaimnVMiVnRL3MBJ2@*})~@LQtGvL2{m zXdiGqxdmZNR_`-aOLHj?(;&pzu<8WeK>TGa1dK=aK(BTgB<8TqJdk_dAjkc78MLdB zq_j&>gy$bT1rPy4zk zRIL=lOeEUU#0NKcQ;OJcPwjT^$hnXDT?gfp<|dqLUO7FNV7UbF<oQu6ki;Yq(*+dk4 zt^-I61k0zY9r_(dd_x{-(o5uPrp9_$60U9xlF!2 zLkW|dLzcKhcJ0M*(XHfsHQ(BjufY|%%7Q3H1Kl?7T?tPH@iLMa8d@)Tq;P8n1L-0i zXVYu5(MnJ9UGcSbW~TS!WSvJHC==mm#*w*c&1OX$Dg|}(l=ed%*xlKO?|m}Ha9E2)z1fF4XrHbS$w1b zEXInLH{E~0p?D2@0FDlU2VaexH zIN&jpy{hGuCoUJK?5r4KrJte2+z~-}2%uV@C2IYTj=a-2cI^m;JZ=`TqeuON(3p literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/hammer_d.glb b/src/assets/game/models/claudecraft/weapons/hammer_d.glb new file mode 100644 index 0000000000000000000000000000000000000000..a59ff671b409a1f4610259f77d560bb760cca3fe GIT binary patch literal 92400 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/quiver.glb b/src/assets/game/models/claudecraft/weapons/quiver.glb new file mode 100644 index 0000000000000000000000000000000000000000..2f5afe03e25f63f9a9174efdbc03ffec4a2dce4e GIT binary patch literal 11684 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 Fe*i%)pgI5m literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/scythe.glb b/src/assets/game/models/claudecraft/weapons/scythe.glb new file mode 100644 index 0000000000000000000000000000000000000000..3b6fe1b9f381a9e1e0920917c061ea0d78edc533 GIT binary patch literal 67708 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

-GCziOnhen@^tf=%T&2dTufi@sP!N~ki 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/staff.glb b/src/assets/game/models/claudecraft/weapons/staff.glb new file mode 100644 index 0000000000000000000000000000000000000000..ad4b3afa49f50f5999b9f04e5a7fe087b5d6d6e6 GIT binary patch literal 12336 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/staff_a.glb b/src/assets/game/models/claudecraft/weapons/staff_a.glb new file mode 100644 index 0000000000000000000000000000000000000000..0cc0a33eb440bd4b8de3665a42b7074eb3747628 GIT binary patch literal 39884 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/staff_c.glb b/src/assets/game/models/claudecraft/weapons/staff_c.glb new file mode 100644 index 0000000000000000000000000000000000000000..ab03aeb4c736c77a4e6e8de0384d5f0d5ccf39cf GIT binary patch literal 82344 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ⅅ&ihM`>`9@w$>WLadp-O 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 W8PeYc6JgU|51R7LYl}Xnum2CWwOI`S literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/staff_d.glb b/src/assets/game/models/claudecraft/weapons/staff_d.glb new file mode 100644 index 0000000000000000000000000000000000000000..f1b2ab59efbb475e34526dd4fb27188280ae228c GIT binary patch literal 94300 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%0A2YM_GS1*%0W&A{F=VB9y_77D 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/sword_1handed.glb b/src/assets/game/models/claudecraft/weapons/sword_1handed.glb new file mode 100644 index 0000000000000000000000000000000000000000..f450878ebbec958efc1bc3b19dd45a8daa0c38d7 GIT binary patch literal 10276 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/sword_2handed.glb b/src/assets/game/models/claudecraft/weapons/sword_2handed.glb new file mode 100644 index 0000000000000000000000000000000000000000..2262172dc993e6eeaec580da9bdad25f4715abad GIT binary patch literal 12196 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/sword_a.glb b/src/assets/game/models/claudecraft/weapons/sword_a.glb new file mode 100644 index 0000000000000000000000000000000000000000..b9ae905d8e7ed2c6a648bb4cd4ca6c9dacceb539 GIT binary patch literal 32360 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

  • )rZUYCU#5E(nk})?%j{#_tnq2#A%iGUTgH zs^z>=Rogr9jdYtPKg+ig{GnjO$lJpw4^ny#f6OTgJ=GaUM|0Kph^ro1!`?_E?c0o@ zJNKI?o;itD!mZ)h;39pPvVD@K&hJ{n!)oSFuprjng+Xt^BbiJwMzWl=>7-rl&gBZK zlA~!OuE(F7$&ss)4nVdB9hqyjcW`dc5w_P3Tb|jfuv86>O;0O4Zp9>8{)+JFt!d<4 zGi2UE%QG+|Z|?se#R7(CZF7%XJl$6Jk|XBaQSf!9gi?!rmGX%1k%pLo`Bs7-OB_H#8_7ZTkouH! z8KMy9h2bVl%C|0`wgbksL_(8{>o)_RK+K!Vtk8K6c}zqM=+p1{^|Jyuq6rI6zComp;vN_->1KX$n5(NJxIU_)ddHJ< TVtEm@;>GOcUoI_pPyYQMOL5LD literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/sword_d.glb b/src/assets/game/models/claudecraft/weapons/sword_d.glb new file mode 100644 index 0000000000000000000000000000000000000000..934a6a33214cbf41963381a919f310a4ee39ad5f GIT binary patch literal 44844 zcma%k2V4|M^LF^AWBfd99ZwnXF9Wjii$Z4=G_@o z%;6BTnDspM^iDA&u;0_OJG#Jr_x_Jx*Y3z3@v3I3X0ElG)EXZ$ zHYPke3QxG)WY=J02bu9XUEWDkLh_U%uV4 zg_{SvIW7w7JX*DEQY$ieBG_D;xwmYk@@V1V-onkZwW~)}qP3@+dyCes217zPY#G?piU@Yk&CLVMZmL!u5bL38f_r8m zDi8P89-gkAs@4!PbX;g?$k@K&Aroq~X@Sd0u_3;pp|V$8r!}#hmD~SiR{tTcg7p6OV`$HlK&r;0*|}3@NhNX>K!{ad^obl9R|e)j}2o<>8|qhkX`owk!HcJ z7@%@zCRqw_pN1L51V@gJK=R6I8W|kcIXogZ1exyH%FRxdfeDyup3&l?^jxThKiT(vEJ+sOjY<@WohzrjK{^rqC3ZrUzc|AS_A1iu}>YoeBRu!;|B8Yh%0m88KIpexKj zK^U;0oZ$TKrfC1!%?ken!Q;j-A$j^Mk@3Hp*5APNAK5TZuY1^5)?cq>oHUi&D9g8o zFX-efIsLg?x_EqZl`SM)C@ zE*izY$*|AOWS>wNpCJ4R`(j|<<(>h;uu=^S{mZ%YUCO(vQ~%Ivf_~{GxZib^-f8L! zUlP~zb@%SqGrYm}yXv4W!(=@0`$AD{R#&K_@et~m@%G#A>J@wd@RKH7Qsj>-@2Xei zyFvbY$j?e#FWx%0U(Mvl?!2phcVU$*Kk@hZe3Om!1Zi1IVM6N$8ipG@T-8B#J7j!q za#LaY`9{L&(d+n$X8e?f{zfnDAXvjK^S3Z=%HLY1{I$ZLKYuS=N>+E>5NhZ?z(MoQZH;DotxzHU zk7%BI+EgrGa-2{YADIt#P#;sbn7-4ndTVh_`l;0#b>ax2Zd3+eqf`*TVbM^bFdmT) z4|$QG-tm3Dp-kpmbvnON(?~Tz7`3JeudkmfCi#a8*6=l}&+#^mMSk+p0HN#Zeds?kVy)mo`S6`B_o@$X-=`j2!B&$Rv{v&mYlz@g z{sO-)u?}D2YM4+MADR!Zd}pP4XH-Q^>7CEjLF-p(X8sx}1iEv)dhr!8+jWFs4S%`G zz<KMf;2pJeKd$ublQ&f5HX+8yBQmI(3-vaNJEXtO6%~ z3k$V^hvmZ$Rz9e2BLy24eRj}j+Xic<7YP-F%hBSV`{VhORmTX0@sauP=H*xEZF{#c zq@=!72l<3*PW}@sWW6{q4xaM6xc)<+U=4RX+d!NcbQgQnFrn<(28LU!e$%W7>#Nqy zYp$8Raey%8$^@y(939WEVce8I3a-TeabO$0W&R{1e`X##B3i?(<&PzQ8uDi~^5aroW@2AepCJ^{7QawE)%G?%%8W&pYRQr#KO2Ke-vDa ze~+Y2ddvLTjQn}p{j6vWx0XMa{11NSkEGP+gd=%+%lbVC_4~y8u~u+X{Z?>g|7d)Y z7?l0vA@+}>ru{=Ij1LwpaizY0cywQ{)c@9~|5r`*U$TauNBzHr`fuN7grwBx4WAth zO8w?hzmKAROI62M!AA;~xUzp-a1L}XAss2md&_9$z|1hDuok8gzlF&b#F!c{s@F2kw--Z5RC;ErT3b5bY zKXgI=uonG8$*ZX2h47(*CC>VTB=ir>cRtrE{lif74{gyue0LooS;JqWfB204VNR_e ziS=jR=-&nma4@j`Z3p_dh3Makpnt1aa-0=>WImkrXYT0Vc5W?hQ2Muq=KgK{qM=e@ zJVLOF{|;rZ~iTQxLg1|C-@@?G_hU+>bCyb&cdseKQC1&6CcFh4H9-_1qdp-(NoLsLCsqL6U&41cNkHF5jV-jX%^ z;flV(gtd;6s_)ihYxw?Q0YdddSNO~d$9}}eu6>x=GJcB?zm{{-c_n^7aZ~&hT*;po znsBv}f5FJV52pMRtl^gVQy32x{+mC4{weVvz;{(E`BNYHvkLih;!GdG8g4CrEcrVj zf7T*@684|vmH5m%6|1(4UjX9QBI%qJ+!Q|rXZd4?{L6NZQY-m4ANjWv`FC# zi2SRM{7W_GUw-~k!hiFR5-j0V8k zSXIk^{@&3lzIpUi;a<~5a(}PzuX{L6t?>5-|7h@k_})>lhBN;AW`4!KN*Bv&6#E{- zzB4BKgr-r6YQy-Z;vYpnsZHA4PZ;5jn49bW9o!m>9o_dd;?!J_-f3UXj zMD+N9e=7P(uke=yKWE~X6!{ES_;ui~YvPyu&ezp2{l^dWG`~O0|NhS!Zu$Pt5+4+g z^}y^4R10aOs!&ZEFZNqq&%osS-RWh1KR7|~@83ak53rTmuRWU&k00J$=C5!-E$xb} zB9N!?VyC5vdd6RS>L60hd%Bl8!LeitWCt1W2kT1kq2 z&|l;lo$^Vo=-+78zjx6?NQmOGWzbelI`oLOU$#;={fFn{Ppe(U7(e6vgS zG z$(J(vEY|qmn=f{V_*1{utDms;_*MSGUju|$S!rUI?+3&;{Cv%+a{=Om&JXl0NI$`& z@>%}a?!LnIKW^{?d!~qE=dITK1N(*gAN5(+`w5A#U-}dwOl;Oz^T)DO@v+Zh1C!qg z@`tB%k>ywVYk;)-Lz)J0lYONtoo?{l(iHLFjMav>kgtV& zhbCQQ`CgwQq^)%uYur|*iW{mIGcf3M+_-!@Ck5}Sf4fYp|f4A=kcqP77 zV83O2BM{$f%ToE@itJTqElCio)oq{&pV>>;J^c{>$FOnygu9)k3RTJr{F6O;hSvbT zeTt8a?+zL#jsZRzxTC1jEMJ%)4%xg!pV6j|aD3V!e&?$)!l=KyO5esuiwfRq^Afd! z?*Tpnc<%UUUXkxsw}C;CzZmkzLjIU4<)!&%`xvg+cN+FhG}$MG>`@t(LB9&}-OhBE z_2<1TBkh9xDwCsm1s?|c6#N_Ty};{CjuuBctrnvewdYm21J#eabQZeL+{y3iR>5%U zPnDp@u!h%v;VCr8zU1jlU9I59`PzS#Gn|{Kl2-R_X%+wf@aq-*2Y%|8EcE}t8$th| zTbDemQdcW@KK)klg8c1z_{n=Y)4#j=cD|tgf_Opy6vVIeYa<++(_DDKFLs0dyyD+$ zPXc8BHhkeJ#eTTtS;OSNLOA4WEc{;pKhFO*zdV19h5r?Py`mrUm&!c2LbewknK57W>5d$nI1iTNk$b4L}EUl1=i|AM%seYu-IXqbP;9XqCf z^QHJK^QRx=g>JAYROjzg}2ZVD>3Ev!C%-^1P-o^IOFs zU%_ShLO|mm`in2U%1?fjZkXiTPMUSWq`x2@0emv>*%wUu6`bj3{J=G4oXOYx2QJ%Z z_Mcku|Er(=w}R*MKf@LOsulm=U(-EZl=rp>EE{Vog+4hwTnvpuWo@7!&@3W zf8bFick{vbH;N~ZiQ<~U)^L3x+$>+#&*XQ7{5p_712$O5mvJUP4DwS=@~z54(}Ey-8v@mL+EN=ov4!YW9H)kfqc;OJ&t%vi``u1_bb+L)NiGJ zNdE(u?PL1kU)g_5|10PZG3ggSHQU3R?bGLu=`S$c|M^~ing1Xuw)aW#@Tg?|Y3CNQ zeOtcu7gFXL_-hq<3cU`Mm68r;B`f?aehNPynwj#!wx#smwxwVVe{%J2-tPDyY4f`P zp5coB89(#CnO{)&3*wK!zX!aKAcsHa})xU-)-Ce7h7rE{y97;Y$81 z{^@7p=NZ3+e!XH}I_x{rxV;;TQDOWzUl6}_yZ4XyE{NO4-vYlk=5{XecR9Xn{j&UF z`q}y&1^tWt2MKHr%zpFwmi^E0g7GVee?feV#|8q9Vv{B+sEXG-rvZJkiT}YTt5on`a-x;ADDc${@zsE$g}!z*Hj-wr9L$6)>*C(7X2Gr z|Nn&x)^OApVG-&}Or6du=6|I=DEePQKdTQnk4Y@PGS2Gvp(7n-`;fol@}KoVRO&-< z*N$?1_@hEksrS$NpvYG%{8;~D5v+f#QMUdI;m9AAMg36XEAzAXV*QI2>%S0g=2z;+ z5C1d$=K3J+Mtv~Xj{>;7{{IuVv=8e`RO$npKdTQJnEzp{KXZL3fFI9?v-vCeso2N- zJK<-2D1giLAs?>fKjUZq2>?IVrcxgi{d&bdtbgf3V(DZSBMbY@xLhCd;YxjA{H#9Q z1V5_}iHFCs`k<^&IX~F?9|`>y^+8$Ra{L(n8SzurzrxSu-3YGPf4ai>an!PWIM3q4o_*hCfWpsyH_rZJ`~`5C{|7!XNGtcRTu3j2 z-qla;KLc0zXo6hBgoVNP@aHfJ;urZa;DcSmq@^ZY?w=U{=dV5rzrljP0Dh5=S>a<~ z{42oEjAZ>=$@*gr_saPSUC}?YdkZomgp$#xtl$@A{I`qZ)=QuV;9q=Fh@6FFX3a(dhrc3F6??ON8f7v~-{@_>oPsY#s(`Qlh8Nb9HV)_-F z={Ms{zl<|U3eMy+oZSPfjKjZ*e>95!{NW$7|M>Fi0n4}Y z%M)73_}FRN`6_K@r#!Sj$8UQuN?LyFr8>~&Hh*%?0z<9edkP1yE#{9dci?qd(ZYa- zNn-cv0U9R1-AyVMi|QrIPtTggGxD53HHW=U(W_fB!p7>H+=CKG zknLM^>!qH}pV>F;VG=(L_B~qeAa;O$=D&`xpXq1z!EXYy?{d~Garvf(vV9`_-)~A^ z*}kAx{e`EnugdHcab-d)*}lxGs|}OC_Lc4XzAZ>74*Nz|J8lI>zw~#Q!2B0b6YnqK zf9AhRBUidH|1_(;2g1P9o6@Ylyy@ICw)`zgX3*jKzoBUP%| ze~HLL#eZGlzZvk~Q{2%ihg>-RL~udLte@Ecnn zgW<<6uv^YQ=vUTn8vMrWV|jQSd@Q~z;74WsRy42Qq448v^ZH!{KPu}t0)A@*`@SF# z3*&qEUho_9UmpCp6!oWi=6$~5lj82IK8+=d`5HGpQ&@f9GJ;cm4qkt72>km5dKk{~ z)6w9W!u)$3dAiB-$U$a5%TuMkjqLnuN(k()fIQ9IIXaoy&+=53kNh0&ygh}jpRLH# zPA6w3v-KTw6IVi@Y!}`&gb9-_=82Kew>1S$*6F|DMOXX7-^z@}n#Ekk?N$tZQc9Ao%w$tZQ?9 zEJKEl6+@jd)Vtm~qvKVRVAp3u+o(+hEC`kDWpqApH`eS46fVW^AD zzTwEv%rZW*ea%o8nSCzEPt-?|*(bpMYMi%hpEv5_6WG@d`H8#~nSBQMzt1Kw*}gQ? z#cHrmkNhl*@8JidE|!FSy^)`%Fn?y>667hHKeMkF_7%3j+((|ahkj;XXV}m5v--0a z`${0}V|mK<6=vU2_&;@BciBGVrNsQ_ggjk=eT~_73;C1a(p|O>c`4O|ebLC%!nj-? zrK+&62l5noC)?M}uH!-Ge`X)@PGsw+Ove%_Y+qpZ2SuY-P8ACY&m|7??M9YTGS?Xy3!Jh{=DZnAyIJE;Ne`&So|QW)REpSGVO zHHLi$msLE3`Y78Mk33ZT7Xbe;``%-J;m!W@Lmnvp+YSE>hJE*uhYR38W?vfQDE=D= z|2>EQSRO7h`;X-rv!AVxEcnj__RT^b7RLARpRg}8`y!Etoy_^?gHI*@5|Do^zQ^%7 z_-FnZl>B4+@*vpfiqE#NpV`OqK*_%j=KMo_l#ZJ7kL8(?f5*)EcOIXG@jZM`JWs>u;3xdkcPJ{=@neHL#n-R}1~h`ptyjhQL1N$AjQw`L_gqRMu||`0WAw z$NHP}pX*mlFt6W{@LOH@?<@RR7~jMD!*8~*5Bs$A_aD{VnSBo-N2z~JR-Q~@{!2$5 z-hT40JIn823>;&RuGTHx~W$#QTz=1S>0v0(jPH?%l_FK&iW@C@UuF>>N4Y3`WMDu=E7K3|Kb)D(oGV!B|?eC0V+5ZdLZw>zj{fs{jeqj4A z<5%|Qg8bHSbN?u!f0Xw}#;@$3jK7+N|JnXo5Jz9fSBHM&kGwxIT-m>>fPYCsa!Li@ zznJ&0f_Nnh`D|S?9PyX;7p8w6{L1)0nfI51xMh6Vx?{Lf{~7;9#1;Lgc*0!&3*wgi zto|}wsn3i*2YH45T4a5MHQbV))kns!)KA7g-BdrtOKV!o^|c^g$|65l{bIOM-x&XU z=*RjL7n|!_LA=a`tRMNw>I=h_`os7Qn14y|EA^)!9@b}7BPGAt`e!&>f2=+*e%7}$ z|0B=j`cM$RTfsx6@U!*JaAo~7{_Y2UYxrMy5!43{$e#*7l^;@CJa;sLcN$WfU&4C{ zJ=ELyuXuMJzU&v(Tf74)h<^+^kq@8uk4ENq?YB^BI|Q2d7LjrWC{(za@V3)x2b;f7@~&xqoE-8~)4D zlqSL6Dm*6?#2LRO-l5+*1LL0q`xEx~$oALz_p~??{vQbY3*wC55}yq|#xKARw8TiFqjc z1ED{8S4)*6=1~x5{Fb#sfRP}bi7=*{ulnOqNc6vP?7CBA*3 zPTpT8G_5MWS=Fo1{$CI$-~QqURXidt_B$*}++d-`ytgucm3eOsiM{=0e#`n?5NG^x z^&3iyz>iO1Qq!tDt6!TJ>I{{p_L2FkTh!-*IOB(2X$koKD<0wJey%U`pI%$ZUG?0= zU)6%YAkO%Qw~m&W{cAt_q+s4M|6ce((Qj#gL7ef6X8nQiLq+raYvR*#{;Wnoj;sc&xggH?1E6;f_)o!(D|?3}GylzlAK3a~^}jOs75xQq z#=kjWsp>cIGe2P7^8Ul-*=R!u%&9?->6^_<^kR`oXX4Uj=c-k9o`dzRVAw>&yH(4TdH^SDE-rTg1O0&iFBJ zVLtS~>-9}F)~tUw{GjMBXR*H*#2LQ<`sadw6#P)eJbyM%W&V4;`{Qr-oBG#+IOCse zp8srouKnqMv9a0zH!Sw|f;i*%hF<1B=7*qPO#45ZrxO1vwZ(>t{|n-be--kc#cvq= zaAsz*X6WUO{HdAA2G{+)1=X0xd}3rVsrRQk(x%8_0`J>i#={oAHcS?J$@s9FD@1?b zaSv3&hShuB8UN15x$^mh@t0~`E`{O2X&r=-3t!9pp1$o3i?;g9{JIA!iQ$mL?|Gan zk zIhOYK0{;xinf;=5GP8d_>|p+5_6z<0RWTg)OWk2N!!du&`nx@3JQj8{`=^2b9Q?*` zX2**WJ~Gby#_;vf9}d4UoY^sVthX|M84rg2yWuy6zbMf#*{+%i4}#yA{thWORm05u zoneQ@;d*up zHvdYuGx_W9W*lVsd-ShA4!9kCtL_rsl0W-mj=tDaf5FM`8gI~#;b*UQ5z2hmNQLny z{G1wb;_eppB#-P?Qm46`W>P|DoC)W)IB?WosIWOBAHOyH9OTb}{IcIQLiZJS)W`p= z1Ac9b$@YDOzK)Oc$+w12gZ@3x-!i+E;9<7^+>1GC#ePfqh4Cl6CI7~KZv2d<-!#Ks z==60qdkaqQF7qX3ALb={TOlX9i&PlD&tKGE6y3XXQs12&r1yy-ynjZRCbj8Qz1OvF z!tSf*_&c}C3#~e;_+9I|TfuMhIWCo?iU)$k)#W=&52j4un^l{Izbm0T*!@OVq1nXq zd}PD2!qqG<{`V$cR`Kz`p8}uX#7n3;zXP9D#gU!K5B5p%5?o7O<)?X_;jc%1<-`B& zDp|v41J48=@^4q+yJsK%V{8Xm|KVfZg{f6<^UuaslctQ)h{1N9tm2u#rve{g*GanG zyB6Oo@{!E{e!h>;H7k>^o_0*E)6G%ZxT~F2ybAETz&Gt`Cv1lQb?|@x7do}#|BCRx z2>*YJ?qUTu`QH`(@3*sq@B#k!ga7qSr>YhIKZE~0;r}GOFD#7T=Iv`%l5FAsrR6&d zr#6M~4keo!`Zb!WZ-Mx&v`OV}NM(f;5i0RPEpMqXp3VoYttfSyK7{v4>MT)pGk#Fw zGehE{8G6sB-h%ClF$CmDV_hzuZnbJQ8)frRwuz4eg}9W@N-$6WdAyu{I6F0 z{|Nry2mjx!G!spQ4!loh z?4R>r=BO3_SlVw5x3sS?evp3;|9HVamG)E>4kWzSwC^@j{92nDyl(pnBYSM*+ho?_ zAAC>=bz@wm!uSFHlsA=T&mz*!qwbQ=ts;Dl&pxt$Cp-BHN2hM)M;xid=Ui_oh}B)K z;$wgtf$OWg3ZuB|nyKfvFC{Ryyt{!w?~#h#Mr8y__G+l${$@6bA*;lygx|c8BhKOfu-KBbsRf08qn18?70CH)2a zU&4NZ`78E!g8dC)|Mch0gu?iKzIyvygAVp5S5XPcC7k);6{d?0vlr?69P<%e&TruR zylI5rU+)x41viokbZ3U2hn{-5|h z-V$%5uESppoFT{ORkV+wjo83%j|voKzWOF6J}hx))|oe&H!YpSN(r;{tG@XP_ccfP z>gVn7chLV54ewh^h4J%z)y`kVNuA&GL*vzwe;rA4r*=p2DC*ZEHU3;BpThfo=Y?a= zyA3z1HMD}K@qylIY0cis`dXu!O3@eYYIe8i#{A<}N#`f*y_d|doYX=1=iOEPoThaJ zYxpYQjo-_-l-|sq*Ckh!`IkHT3axh@;g3Fk%e$YV(&HzdR`D~y+X27+#8Vg-m#BHY zkZ1l${$9S95Q^W$a zLwjj*o11*OuWG>>?gBgzc;m0?AO3Ysn59++NZ!IYP|B0XH+rs}V;eVfa zwV?PP`-fWbe-HToKKy^DT0<*%8b2QX-wgj39o1B*(!1_HaapFI`Z}`tAGY}YYQ{`{ zz>ULVn|Jp#WMQOLy#0;CJP+J?lxS*c!TcltOZ}2cq7`4Gh!|b!b zPxSxQOITlN0pH*!zhsFk@-6ut+LqYI^dCuBsAm3S{JUaB@lJzF>LJi?i8K3|eOqEh z{x-wxc&FIn*GE%-$^cSCVC#M2$Z#)VdY8HUL-1#UKXFy$5BUsNu>0zc9PI?El8&MbRSuKkzRBCjTRT((s@0SMU@6R~-4J$Y=i1EB=3xgufpQ`~Ti{ zT)g|!|3C0`<4yC2|D**!{V(IpzyDYK9^~IM$LIa#ujXCJ1ES_42kw8X}2r?9OC>cPa2uB=2 z9Z3*b4K0m88$pK~aYFjKVYra-WC}ROV~iki#1F|3NQ#0MCA~;j z-01+?fq0Of#DSCmEkOiWU5CsDolQ2tq8el|=wdPxKTF6|(5cX|kW?jhpmxyF3KCpF zT`>nODG7_T7;Yq-3?zY|fn*dJNrsTYpo2*mi6+4$1T=(5doC(dL7WRC(JMJAJpkR1jZMq-H$vd4gq zA-|FtkR1scNv6RXFX9L42aAS~9>fRK2Nnerf6^bcKP(zf0!UxbzObk}X^nsEusen? zo>r5#pl!j?o3taHK|7Ovc)Anm2-*=GO<{XO(1xTrX$ITtg4QLCi92ks4O*MDBr4eM z4C+i;V73!r`7DeDRel3TPF?sVpgjj46w;l9VUqkb&hf){@Gk60)r_Mgplw zDj@0=F*e}o^2p{g7^`r%EVwFQYy@XzJXsDS9-I}C!Id!9lWK&LO`w~|0aA<?h|y&ymBplR^FfO($2#PO_Vv0zE|}+_?rM1z0-SLbektK>ZBd2nRl-bv7tcq#?=uS4QFasl)LIf9=Hco(ONrlw27-ukxE0{+*=6wS6 z1i1euS>!e7Yw{=gNV3Uo(A%UQeM9~tcA$3j6M07-lAEA6$v5(fyeD5kzmO-S9qmH1 zNGAD*d;tAG>e3?g8Tm?z(68h(p6fz=>0R=G{7dd)yv4n*#E%xEr6>o=Q5#x@7NuHh zB;}|Br~|d9RcH<&poA8ux#T;k3|g6%r{Bp-l0$My9(fM@F`?kL#q$o3;7Ci*lC(4} zi{~oPO0+Vqisy6jzg>_A69a(e{ny+dk4<9$*ew7#sr z5p4}=tzm;F_?a9J$g2*`Z9v;VstR{%fYzYSxblR~wir#RD`wFMv=MA+01b>bhODNr zsTOE0Sl|g+8a(X@+8R&ta(o1Msv_cE3EmUvJ7kp)ZI4|0M0(I{WtA5Ki%mEXnF_j-NE+}IsKma;_1)GQZI0P!n$aO41WuYI?#VeM?CLOd(*zuA0vQn zq!Vc$x*1mipaHZ$u#LFW8>26-0_Y&nL39YN`p^NO1L#29?JLuPK>9$!0DKOlL7+i2 z7*`W%AUFo$`w)y_zy{#XKn#Wl(GbuO8j9@7}0$3pK1Yv~WDwK`_9YrH? zHJpZnhSQO_8zR$@K!!s?IPg()H0WqL7Dzaa0*#{4xI0p&(LloSWE4K5X)I_g9fzxl zGzJ`_@qH}D1Yl9P6OF;}SUMSWGM$3!iF6Y1ariy~V;Zm++=<1QjH@YB2dbmL;A#?` z4mzF2;qGLa#sQfG3GCi<`YY(KbSCb_$#f>2g}W1R?-$&g1v-n)!I(+sfzG4zflQ=x z<$Lo%=hFqie#M=c81pbD(uKG;pDqSnOn;Z}Ed*Ui7vb(anJ$u%#dJCF<#Z+PEu>39 zm(pcG7RhuOkcE)86raoJYS7hm4X!5ARp3~T?<+Clfi1RCz_t8D{CO#S6LwC{J_+)e!-9hitOu7Yh3q4IA z;NA|<9dsLggnOq!Pt#OLJOho_FjDaJ1E850TVTN-&~gdm0DXdIQ$bUyNT1=8QIYCl z*)y5yVcinCf<6U(N-yKvTKX9DF|O9q4fG-CLtKrgQ|VREtMnr6M+ejUp!exLI)l!p zX`pE|9ZzqfcR=reV=x^`Z-L&zQ=9PYJ;+ z7=$|od!OU_Ij)?z(p)+2Px=?N1-0euILg^@ML>&iWw{UZ6Dco}e{-$qfQP85C z1NWWg($b)%VaHqgFD(XIj4R6JQX?$`S_anr3x7L;I&#H0Bc-s)NXv3%@KiBaUK-!Z z;hQ698DQn`tvIM1tSN`E zDsvUM8lW|}`dlP8f^!3P;7Xgl; z7_QtXE(%YM!f45j=EAv-pdGocTo@M&?l6oNoQ7-1d4YOy-MCJiz;y@h&h@}k9XTIR zAI^`{a-BI}P+!g)PkC|vp#EG>Jk_1+1=@@2i>G|JKA?TLet6243jht^dgH1OHyCs< z7l^9>ZV2cQZXme*Wjc`S5AI&v0MG$=Y6v%o3j#6(v_C$BaNQp?gp207fOg^9<68(k z9gPvm4d+H;#=|j&VuqtJ<8X{o@cuB&G87{WPYlP*Mq+g0CU9}wY|z=qE_iQE!yDdIqr3KxA;hr^8#Lp= zbhPd+q+7>!{uAk&2h!`PF6GZXF+566ueIZs0GCbuM%Fw~e14hTkr@si9-rIy8Zg=J z*okV_4ltE zr20AgCrtnNEW38s?y^} z@pJ7Jm3fF_25l}IFEwGe{3jZHli3%iC%MuUPMc5?-xE)A+I8&JqDxwibea8cytK2v z7*&pB%xh;eS{^rbwvJ>{U-tZI_Lw#y%bt)C`*8!(>IX$uAg^QDMbQf6=`H5LnX8Sf zG_Omsd&S-`x_v)&VM@KUxTw;EJnv5-w&Vw++XMz{b=Aq3MeNI*S8?%n_@XP;n1oj# zSs(E|Jvc8gl)CizW+&fh6ImAjVc%5@=Gda`_yaa37h`73(w`G!z#wag3^b&lG& z^vI69N;<5{w%65Q?isq*EgFw z3u&TH&X_9NwO=yCbbQ7Zk?mHu2(DX_Xs4k(Vv9 zJFLR{ib+PB5eLbgdbzDjkc0Q>l5I3;5*@Kjh;10BtJ8(Jcz=Qh^2=1xHKu4#Le{?P z97#O;veaOt)&eAgZsVouBr;=8vrmsa&Kld4Nc71gKG{Z9bF$>mlDgKfFmYVRy*x;k zzi`m5%l0iR5E9!Dzo%@R8kxmn61!vi%PJae=13Br5e>aN;&k;U5bauoxZ3Hsgd%<( zPZwV&_nwzZPclNyh8Ga~>Cp=ZhuC8_=}|x}1#{s}rZK+0^B{txFG^ z1~IlSS4*b1DdFN{Pgpc>#I5xr8@=t9$ZjIq#fXSX=N6`=iRUn{f5_Yqv_X$N((QO$ zgX=cSq2v@+iq7RU{>5#WU6!05Yp+fCTGQ*xBaeC{QY5!82tLMA!LOyuIi-**+pin9 z&2vudW-JvlS?A_R{E&K>wDvb7T5}S4n6zJ2Tj#Vj2Z8dqsyll@LZZS$x;hBVmEulh zi&$GK9Z@VqRXPrTY8> zExIxvj2>-MGmy{Qa-OLO#KA6>ALE|-#@G^;rbIKIJYOT{f448C(w`%i?~kzc>gcLn z_K$#YeW<82Wf5umBF?mU1MB6ogq|n(olD63#G0-a^%RD)wPD!CGVvOmCoZ8^nVg`Tdx^!htu$< zbDFf(k;HTGHvHCXM#M6n*sdW-$51m-37dM5o8cscrO!Ye#ePv*u3_COupAnE(X{cn zTrIxeC(mhXUgq;seTL=PW4VyYmyORaz!mU-y)~ODlbyg8>aqVS-ZAkstUz(d0YbOA`?#MHz7ABQPOp6(V+ORD49pt z8bf7e?mT|>dtCfiN=9BLna=}FC1W7gZZ`A85)T%ijI>hy7m;?WvgRP0)4IV#J>gVL z##FjsnV?;shoys=ZtrcB-N#CsX^(q+4?%49sZ{VqmP*g;oJiuCH>KEW7?*__gTM#3 z(Y0nZ3?WTxh-}s&ZeLlk%UVE4CuI5DjNEMtE0DoM$Wv6}C2lX`BD1g;osNt5O8uJ| zOqTbj+=vY0k$KJstI_i)xF0uOD(#}H_aH7FtM0Yb!?6KEqrJ46p1mMkDaP_1ds1SX zZ>#R2j2F?V%4%QsLiu@CA>HHKIY^mGZP{k``mzx-Cz`WHHl6C&Jse%g$4bU$&jVTJ z)>4Ii!O~d5+69PY~0%FB>m5lSg`7*iI+(8k?5TbCS7Ys-I%O7P2_5WiW@I)I;ej-;&VKXEk{$i!CFYKXLLh)Or?953FJnl zT$XIB$St;C>N>1yOYS8(Z=>2<9az8d&iDsh_%u4kHpwU3X{$|FcT@oBGQxLQht1CA zA|d@R3Rj{@%_|N|uxj8X6k2!y`)KQLN7fi^epXX0+1?@>3n%1R*`NgUA#8hzb~pao zq%CSC(W*vI_BY`c~fPzU6tKg=;GzUkv}S-#he9ad*E zx)PxY&6(50nLJHnTQTBpM;wxESkHI1N>F@xm||1sRURgaREu~XxQ#AH z$udy&o~?-E{sM}R%hjBHJ#4z7TCjX)yB~4roy+pt34FkPZJ0Gn+Xi8WWTpC5UZyum zMA0QalZ_s-Cy}9Y+)R;2H-!2&G%o-lZ#^&-buG%&3w;n?A}~p)8U1N@Y|N}l+wN}k z!sdlSqFo8cuvlP6GG0$}4zurf@4VE*2CV{o0nvVcVt>yQu5d0eJJBcG)B!btMdWn~ zg13D%TALktNJ0pBhQz6=(XK19Bow;{nhhj1+1c3?BQrZ0?#YqhA!u&dzT6B=^ZED1 z90p^bMG}Yv3yl2YP41;_2!+*^Y2rn0bb=|}v0ET=tUIz%eRI^_U&|$Z<0{pUBMxqP zs~{b_J84=$R})@x0yA0;BssH}O%Y4%Rvf{Wd){4k?Q%+x!E3z8-z+n=oOW?_m-Ii8 zQ14k&hh${QY;;4I%}MT#C4=>gHS__c(X%sKuh`hwPLT7g38C6W*HGqioR2SO0VWr3 z2>$GYwU#NX^R{?$*bSYCZDn|w~ ziqKOOp$}5)Z`&4{dTDIOl|D#r<{p`q#~-cp9Jf!U`iv(xUKuIrwcFSZ9k9B#c1=+& zim`Vo(jPr@GZfOebrqI`VggH^qJ4D0W*PrA6D#>+W;MF~x((8Vb+dN5qle@kpC?;> zo-4K|U3l!o-!Sc&{N`eSM!;t%sUT@Gt&CHc>blUv3W8X?pfe;L!LuM-nX2JpSmphoDukFpYTh{Bg3b^*zxdH^SC~7w&8)s z*Q0{Gcbma9HT zX0oVZbHv$EyQV|xk}c>XONF#yjpn%5&IgyXQ&R>j`CxZ?;pmru@>j|)#1`p?67wPs zZLJZL8aG*&a>x$bDmxPhGuIgNNAC{!-yeDV@Kcxctv0%5ZRP2|4dME2{cexCY15>t zqZaiS4JQsceI0d8Za%^uw5|q$0orADiLIS=aF}*=9ts5ZB9cA~;kx+U8Qo>#rDCJ+ z4Qe)HRBvNs5pup)Ze-RRH;r};E7jjkjs6lGnOMZ^<;E8C8|*>OVy#s`gN)!L8SNV! zBqSJT5A+TfoKJZZ)aV;K;?DQV!y=B)sp)Nxo{5}lLe8Nz&mrmVp;$FGY}@^~ySX;J zcAmV)FA2%JU2nRs9u7H|ZJk^$7W2FJdt5?}?Abh1uL;+@VspRo3i~IFMf-w+hdf~g z9j#J&h~q>oi$UBS(-{?IedDF?&aqdK_4*C{4J=&p z0_C!#`M7jg^QpemHk$@j-`jMoOfqhj)}GJEJ$IK|iEKf{Gt*FalRRyP)Hk}lI?^9U z44brY;h2*s zWLKe09FG|NgB(K^v>ifl+D4}>(V1@8J+upo2HhQWRd)pEf;!G*<0v0=iW|`1%sggy z+$TGI%Xb_m*A-!oMj1BVn@Xcl$C+*D5QHlg6H$wH;6Qx@4rB)k6uTY9HfR*S2gX-- ziG=1gm9+^ubpCx>u6~|MVkZTP!K((GHV+$m;7!Xh+x_gM>yU-8=6QT_uFuwhUns%;7(0ngO1nlPH(1Ta zM#>!e_8LjNSdujOP;|Rb4kD{-@HvA8$t1SYEMSAFh+l{t8^Rgyo889AEA8rM{5tMJ#uN9Czrv(hx6p7;_O{O zGFknijJ5%0GEcgKqw7?;<+*VIr!dl>OB&UaJ2K<2d%h1MBb*Ys3d}H{>HuQe`8WU(&!mufxi$=nu|cJ)CvSwdP`l z_KyU&A=^!>`5Dr0I*ym@^gEI2kc8T6EocO43~a@wZO?jzn4W(y8)dF(jO_R_l)`v9{S$&N*Pc*^U=i9X-9^+#Xt zme&F2F_hb8G%BX3%gkf8#S*(417Fzf?;#&1(B>?Ck08k}5NgYL^?`FhG1`ZN1FLoa zFeDyr0($|Ia*Y)gSGh}NRjjScz0PNLxz*R+n@UFFTzwJU05ZzPDHi?G;FvuP ziGO93A^Sv+=DIO$(DiTgDu+h$(StP-p26hHkFQe%s7iT0&)+&@(VMawNG->hrZ|_{;bQwQMyFD3aJcF`S3jWV4jXsX1!#n~fJsldjiov@6h~eq`@9)>hYs=fNb_$6zgd$bR&q)o6Ud zkpw%w)9|m0KNP8iTt>HBM(bc|tlMV!qS=>Eu-mV*!;IT2G(@JC3x!dNUNA-gy`OKHt{Zofk)gNQR@FaGU zdvd9*Zlhh>JQXhhK2(H{Y~&8{AeIYGck(t98Rm=Hjf<$W9+mE-vY7J z#vw)UhT%$%bfxm>msM8jw5!-5nbmpwO03Q7f^B@Lw=r{(T_Sd9NDb+%Q@ujfyb||# z<7EfG7tPs`2m1@oRfjjzq;F)o$88+QRG*MSNQHTFwV$*10I|iJ$vH3|MGhfrhMJ4I zG9Zq2XUBTU=i8B={T!K_Wk-(Z6xn;FmOe?^6TnQo%MA&$A040F{6keO{zN-U?I9bND{MC)<8bD&K{>n>;2|KKyHNh@^oBgEP| zU0h(4+pWI^v?cexmZ-M8GQmB$hY)bM@q?^q>=+(-euJDi!YIM&EZAiLr=iY*>3 z&-KvUB2EXfP_m-kvyZIPpoK&^#!{(HmV`R!8co2PklI0GP_8?(Jq$-;wnTk$*$X>) zYf!P1L-fEdXe)}L9lUh5AzM@1)))^}&doHxl?ktncb!zb;yqG<^_#4Y)x7MejrU5# zE0Ym8;(O%AB@_+9D*@6oJI;ZvH+H($u4$#!C2htpFxa6A>z?gh?{UyZ6>^^#9u-1qWjsg`Gn1-u<<7rS1}}K)gH0 zF8md%S$hjF;WKbH&+?7`R8_01^9+GxZ*g(RMX^K~_+Zx>?{!upd`5&}J9`n}jW_(+ z(QVnm7=1B2Dk2Ez=vdls5GnA zc*bvwaZZ!J@qcvD;uUfxx*Aq`(RRHsMxH>;TZx6sj_2q?$#Bf33-$uMSiu^gtnsVP zsxi)jJ~`;SZXi6njoRCIU+9xFt{w?_RwL+T7Kh_43&B6S9n0eGu7-b0$}d!K+C)YA zA|2i#uoacY7Fi@-LB3}%FkqNd$X66M7jJvg2MgH^ugA*KFf!u3cbEZ+W{7$Fc}<&bf|FWFTI7<%Bbj>qyVpaj+wO`~qwOY^kzXvy5PGYU0kX zz4ZTTI{&yT%lnPr=O8~EDwt)OGmmNJUMe^tXI65)*-95Gm9?2A?v~>s4N6^QISxq+>l{e3txSkl89qq8c^y`OlMgtCLGT>&-Z=ZhyI#F&Uv2uzCS;% z&vjk5fpQM?;hl??9|jshS7ke}K+c3q;yO5yP&0|7zPrw48w8&}IVSMa&zVeWLe7-6 zmLFTOVIZ`)o-fkd=ny55oQXJXP<+Wvm(!nLMug2zd@+q40bVBiTP@TitM)>MBp*?S*_ zH$Wgy7~mI;UO*mT7#a^N0O2Oux(B>zpH{2PE?O4;MQSF9;s1xf^zI^|pZ)3Uzyn zc-G@_@3Hxl&R47tu1V=2u|(O9m-rtWFHxr)k5 z7$-gO!3tAuNvo=sdTj2;m|ly`{jHvCk_rcRTvjZ%lvwO?F^TKU2{7P9~c}A z0C#dmnMNj70T$wwLxWF+DOFF};2g4uC;3!ta#`NSTGEM*$p4h~FFQcLMo7r^on2>g zMkqc%&rMte*(0^ZvuS#5Ae}jXPO$;WJvN|lYd>FrlPbH1DiB%&?&tX=u!fy+#~X7Z zh)3GL<}ad0U%_yY(s8UN4syMCeVW6xs`6`8ihe!CNMHQ1XAxbo#nZ&Gmk-vQLbP(I zjDoOzfcl5MEsRdXEJwsnLm64vxtmxW-3&i(a+V341qQ~cRM5m z1f|Vwz)rEKX!Anu5#ZSkiGGj?*mj_3Qt?Z47PvB+rN|igD)JjO!G>HzM}jFR@l?<8 z%%s}fv&TmhTMxgEL?}o0)|QDDa@eG81x0dGLk>yP>wb5}lL|)#jH4eEC>a3wXb-$*pMdesL<*ue}*- zwzHF}orr{-Zq?pv&FQMxsD`jJrt)A2yptM0l`h+_83vRx$Nc8S)oq)p28vmIXuqZ4 zm|WC8;qV0wNAJ{o%tO0+GhIZz04`rdAewtw zg4$4l%Zz5|)YU56X`Xc&HR6fLiZ3&shzy=x?#_!4O<3h>ZE8agupp)rQj^ zbH`Xf{&m@q{|C)f+EBIocJ zu;B2;Lz%&>(DWmXTpNUheuBXNq5B!#Kq_-}SKs;DLc{BIc;nO_I{FPV&Om5(Ixj7U z4f!YfPxsW9_1STJCx{&DOo|# zlur_f(eP;>wZWXs^Ar(=#{Vp9TNz&S?q@N^l&?!GGc)$_J^p6}aW%Le?$6^;``edz zo__bso6gYxyZZY4hl1d*68EKdM(oL>W)4CAR$ut44smsdq-B%~!D}Y1kf&^~sP>3R z2_dT`9yOF><}jKGK)(Fg_K>HPs?X%MN#X?**cUV!90r}5*Kf^L$qWJUwG&5n$CPDW zCX&s_^>dH6Y2Yb8<0^S>@iy8|8Hk4yg!t+NO3p1_gmV99YNMAW83HI&g`NZ^3D1F7 zKTCqT7W2U%;T{mxVh`js_fJFsyWe{;Z0i~d}NCX(}LoFMM`ig&IN(I<4e43#sORM741W&4Bp zCg`^rh_8`fYIvy4?7_fU=r)g`bA88#pQcB^iZ4tYU+~T=bglE$iyeHu&64MDGbexV z$sE#G@*hNGk|zH%yVj3t%uUq9#N!#MZ9%X||2s;9|7ANp@&ES&0zNV|)1#_fL3dwh znW*GP;nyLHozZX$G(&V0OWyj27&e&}ixz%i2r>YIEZljL&_p#~uuT>W`cBHN6n056 zc0Z_vo11W(PpBGGedqPIoC6Zfu`p9!Yx3HIOH|; z9IO>gTV&B)WNzJGBS$!D|0WS?&flkJ%+!1$IgKgSigguCa`aYnsDynJ)ms4W*8pr` zumxFn`&*;s5_wje-v-7!O|NM26paek{M&0v54hN4dijX6$wNi2-UBWp5Te<(7YTTO z0yN-gXaNf7F>^;raMrCCL}!IW;h6Rfoxy9o}P|npj*P(mYcn1 zB$5p}ylte&Qk5W2kNtoD_DF++ff zPO|oXH*0%5sA%goOsc~DQjrB35&e)0MZH9-|K`0KpMWKhrO$^!goIp0%v#Q+YL%5s zH5@_Oo7Y0Iapq5bT90b%>W+dV(L9Hb5`bu-sjC=1cVk`DI$fVmGvgZM>lq4PN{H@i<>YS5nb}cyS;3 zj2VRXXTvJoSt1KURy*ZWuPmYS~5N3 zAo{H>@SxF(H>sIyZ14!HnM`&+_MbIpB8sQEd!j=7QzZ;SfNRU=g6aHQqv(I?W`JW7 zIx&<^*+Ij5<7JcUg)miNw8)u4e|2}{2%aT7M4_WJ+x~$cYwy4;p3K}KLEV;~tgiqX z5d`fNXlc(R%oxm*bY<4P(*0imX3HX$8Ta|9NO}i4@)g^-`m!!YbVeVx;XDLT@>f^Y zO&Z`-zaH`U_CfH74%Q6BY{EUV7F=Kvh|!|lUeS_Mhbs$j9Q6p@N)uM1pDCdRt`=rb zDGmhM!}meRe6#$&8;yDVfXht!SFv-Hd<{a`4hWfo)4IsRDZmB8U|hC4C0K~{&BxTg ziCTTIkFR0i9=gcpU}{=q9|dA57OCzEJY?+Kbbu4P+jX{D5(u0GegRu%%HX@r>2bDD%5*b!8~j zS%zVZ#GNvGu7cK(_l3ON@j)b^6%FhKiU2@^Mqm~VKoUDIGss%d{pZ)8A&eO!mw?eR zGl6AjK~3?_4QWmlm3NvZfLM;Am}!G#29OQLYNFv<)IKlXyq88?U~?#50XjM#MFhUBbMI6GC*K#`Q{aSR=)I}NhT zL=E)yd-2inp;>9}b5Zw_s^j{BaLS0&y%sfH**C|-*hRUrWd*(EDGNllB>5@e6#p$S zM_p~_f-$C&lH5@00oQL-;HzwhL>_5~97>4MI=2grY^J$ds|DjdMl(^WAB!v|r6ril z<3nAd=aDuPX&!#*6$wtlS*!xrYarKQ-OCb8)jF457;;G}Y1N}<+7q;&nc60J*C7xe z7!^cQ?0?(+C;hdjXo%~$FJc2uRsA9J)JnQrhUwhwzG3lU(j!j{JyUV}UoxDDwxsb0 zz2jj;m!^GBWt7dcR#8i!Yze(u4~7kiR=+92YD+uCQ`uJ2-PgAXR~!mWF3Lvd4;=x| ztj-W^|Hot)HK&^{6{-Hm*;&zXefxBcrf?2>s^_WAh52)spV44vqs9+c|KU$xtKGahY`a-@$#~d^5)~^l?^FS-qTTtKd8;>UU4Do>7`?^ih zsqhXpf{tMeVP(QNSfhBGvx2OGX_Y3Nb*f+4L`$oJy01pRbLI8K&9;xUM2lC)Ll$ zg32T_=%z3Tz)Z)WLK)?NHFYYabE!;xsd)j595x#geuZ4=ki@u8$Iq0G-}*=)mIehQ zZJeU@dh91at+4G$$P=o&dYVwX67(IO2IS+fUrHH3GG#ImJa2jgGJ^=BHyu&k+Fg-K_%Mfg)oi7AdC7CQ% zlz)Mj13T(Lc=0`%TK98kIa+7`WaGSyEK4U4)r*la-158COXCK8fAoW6f5G-rTpPX@ zI^_1%;Thh_-L_ysi2-yuJ$r4#j1%DqKqUxAZN&_sXZC zreo6QcxaF^N~AH81=qa&<%rODaAw*vs2#FqSgbX1Rxg4 zv63NnN&)7$KO>>6c1H4&8yr96d%l%-5P=l0YQjYe6L{eT-vkq~4q!x--H+29Bg$xk%#{)Yy8l<|vb_`H3N$M>&fGY5%_ zAGN(6ss9>(I$BQ8K;_?ZEmM?~#r(wy#O!REWWnI@tFT(jg~XxY{cV3kT#Y&{`g4N1 zaxfVcU}za%Zfz-C4G^($-54h@N)4>}Ag&DfbRLhjH37e8O5u~wrBRy%&??i!7R|<_ zXp8XE3bqI2ruMAcNx#!>;LQW346o-%&}4)0*3vEiB5Gz?L)R+|7!**GvsWC=q?s_# z;dmm2mA6xY?Ibc(WhF`-U^(y!`nXSKUgY=o_8EULf%mOzh4?2SDgK(w{6$H7V9&8s zD5e&2`unxh=sL8w1@Ge#welEUY-j9;c4drzF;|OM`cW1JvncWvW6N}21Gm}?_NUcR z#8uPxb$hq6N5sy-f#aP~D8g6|zwrDUbVz?FC9&-1h9*YQq~HZNbC{hM;|V-3C5qZE zivc1ZV&MRW55Tp5>oxv_i@itV!3~^EO-9oC&5MOlx_f($*RW?0=FItZ6g+(VvkS#M zB*3#)Yzu{kr+|`|Urv!>4wH%aO^3&&;HeF=E({c#? zmFZElVFn|XrO#28WyQ|$KhMi-aH>lp>Om{aCpeO@okBJ2G_x)8g3!iZU=4KDCt}A( z1!ZpPuqqhi)9P6)-gqQonTccbit; zQM7w`V2Q`$Twm*1l6GZrnky)5MR1ietEcd*8ZO!}9kzIT>6%2%{e>oTpzf$qw@eoK7pe34=;OCpWr(S-UF%lu7a>XR8*rp?J3D;Wba8A(v^=v4K zT143zq_u^;Xqtp*8Fw2>QS)Iw6jDS&y_+fqM{>oQENyaE-mlq_ z!()j=Z_}x;gjZ^;wAG>x6bBFtf5m|D)m6zN1Z=#dH`ijHv|5(zmWo+tGB@IwFvj@v z<1F>Dukg4iK#52a)bhZQsiGNooe68mwV3LZRI{mvA2Ppyw<(D*#~hV|GGtST*=u2> zSPFKw$ZC-j5Xl&{z}GFm@&b_MtO>`B$I6o%guW86d)pi#i=@_j2k~G6Mbf5#T;4t& z)?HkJe29?f+;)33@5!PpV1&Ispa*Z5d%SsFB(Plrx0bySs~I}W&m4fDHS_TlCfeW6|bg&_8ws*L%*l64)TJ6r@kZ`PL~pfD@Z(7wDNm;0r6EpX_%qbdGk#DCIE<= z$G~cyUFDfc5u(`>er%&jK6%w15TmBVZrE_{h#=%f@|8-8p!Z{5Bb|zR7PAD?r!3)I z>GZ!8$p{R)bW;&=59trcvX@Nk1rHDkv-+lPgHv%rp@++K?9PEmSv|b3CcXGdU`DRD z2(ulSP)lTeLC%%37!X5?Dn6ez=w6j_DR9)@Ng}8+jbZ4+fT5ZfVZI;%#dn~YF!%Ec zi?+ZzI3wv@+ueL@+~fyaIun)?eveRR?>*d+5-(*-v#C1VWqD-PBpYLjetY4DHixZk z&3L{G7Ji7vG*~C2g)4L(g#mYuA$~*jEQ?FdMySF8*#wh?eH<6xq1_AN20;k5kx_Kj(1QDl1o1D)mH%j%fLTe9sv!bLj~ zcHg)%Ygycn87atRd+tH1Kp9&{d{frwgMqh=*IyKi@x<`Q(SzyY%_z7VofiFf@Gn3euWiizQww% z{eTUF`(Vb?BQRsKdmow_ip!Q*3qpK#OOI0xq%R{OAUtvgcDG9zV)nZ913mL{1 z&tSsjb66mC$gMdaC*Df&)o^2Dz6DqCDl-#l4OH8_w`87-;$VX9vLMReENk5 zqnmkalRL4mlMZV9(5xHaruTIw{X2fqWDJ3~xhKXt>Hm?X{?C2qmm<6lKq2s!&j>+? zo7q2c1J{|ni20;wEc2dpi@`+=+Y{ zxgJj?r2L4)8h%gAUmNI_BlFCrmhj(&HY0p3pBi4@3vL*Ddt7uOc^?yxS-HL5YZwBe zIac!{|2Aw5FZ6cOKgc+J;qnfeXVxfp_}OyzbdZZ?sV#IBxdAK-2C>WIoJL0tL%UyukGwNU7qeKeK0=x<8t_Q zrsQuj2=L^af~N6A_ppU1eb+GqyUkDO;S<^lPd6bgg(vDoL%X{B_zHO9KOmvZEj5LD z#79^|B8@k?!|U0JVl1@1S;$68pYct_6yn=^ZU zg@*63$0YDCfAlQay)FS^gI uUxIyuOzk8**toH0vKPksn+}8?NL;n<>FWm#;6=>zpU=GW-8<(0>Hh(y*#i^+ literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/sword_e.glb b/src/assets/game/models/claudecraft/weapons/sword_e.glb new file mode 100644 index 0000000000000000000000000000000000000000..dc85b3dcfaec7e3fdb2e2f14434dfca933cb8797 GIT binary patch literal 48736 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 F|34*co|ymu literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/sword_f.glb b/src/assets/game/models/claudecraft/weapons/sword_f.glb new file mode 100644 index 0000000000000000000000000000000000000000..4307b8acab1400d38e53b461908fb6dab4ca83be GIT binary patch literal 54696 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 M1Lu7AQs}n-58awQ_y7O^ literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/sword_g.glb b/src/assets/game/models/claudecraft/weapons/sword_g.glb new file mode 100644 index 0000000000000000000000000000000000000000..98e1f5cad20f46fb069a68e81ebf8f76776ab6cb GIT binary patch literal 70608 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 VifHyXTO$?v{FlS9Mn4qV{0|NP<;nm6 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/wand_a.glb b/src/assets/game/models/claudecraft/weapons/wand_a.glb new file mode 100644 index 0000000000000000000000000000000000000000..553cc5a89ffc371bc4019b4fa768fab6cc520ea2 GIT binary patch literal 38504 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�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 } -function arenaWalls(room: BossRoomDefinition) { - return ARENA_WALL_SEGMENTS.map((fixture) => ({ +function arenaWalls(room: BossRoomDefinition, portalOpenings = false) { + return ARENA_WALL_SEGMENTS.filter((_, index) => !portalOpenings || index !== 0 && index !== 8).map((fixture) => ({ ...fixture, scaleY: room.wallHeight / 4, })); } -function LegacyArenaArchitecture({ room }: { room: BossRoomDefinition }) { - const walls = useMemo(() => arenaWalls(room), [room]); +function LegacyArenaArchitecture({ room, portalOpenings = false }: { room: BossRoomDefinition; portalOpenings?: boolean }) { + const walls = useMemo(() => arenaWalls(room, portalOpenings), [portalOpenings, room]); return ( @@ -196,9 +224,9 @@ function namedMesh(scene: THREE.Object3D, name: string) { throw new Error(`Dungeon kit is missing mesh ${name}.`); } -function DungeonKitArchitecture({ room }: { room: BossRoomDefinition }) { +function DungeonKitArchitecture({ room, portalOpenings = false }: { room: BossRoomDefinition; portalOpenings?: boolean }) { const gltf = useGameGLTF(KAYKIT_DUNGEON_KIT_URL); - const walls = useMemo(() => arenaWalls(room), [room]); + const walls = useMemo(() => arenaWalls(room, portalOpenings), [portalOpenings, room]); const meshes = useMemo(() => ({ pillar: namedMesh(gltf.scene, DUNGEON_MESH_NAMES.pillar), wall: namedMesh(gltf.scene, DUNGEON_MESH_NAMES.wall), @@ -232,9 +260,13 @@ class DungeonAssetErrorBoundary extends Component<{ } } -function RoomWallFallback({ room }: { room: BossRoomDefinition }) { +function RoomWallFallback({ room, portalOpenings = false }: { room: BossRoomDefinition; portalOpenings?: boolean }) { const walls = useRef(null); const previousCameraPosition = useRef(null); + const fixtures = useMemo( + () => ARENA_WALL_SEGMENTS.filter((_, index) => !portalOpenings || index !== 0 && index !== 8), + [portalOpenings], + ); useFrame(({ camera }) => { if (!walls.current) return; @@ -252,7 +284,7 @@ function RoomWallFallback({ room }: { room: BossRoomDefinition }) { return ( - {ARENA_WALL_SEGMENTS.map((fixture, index) => ( + {fixtures.map((fixture, index) => ( }> - + }> + ); } -function OptimizedRoomWalls({ room }: { room: BossRoomDefinition }) { +function OptimizedRoomWalls({ room, portalOpenings = false }: { room: BossRoomDefinition; portalOpenings?: boolean }) { return ( - }> - }> - + }> + }> + ); } -function RoomWalls({ room }: { room: BossRoomDefinition }) { +function RoomWalls({ room, portalOpenings = false }: { room: BossRoomDefinition; portalOpenings?: boolean }) { return LEGACY_GAME_ASSETS_FORCED - ? - : ; + ? + : ; } function RoomMarks({ room }: { room: BossRoomDefinition }) { @@ -384,7 +416,7 @@ function RoomScenery({ room }: { room: BossRoomDefinition }) { ); } -function RoomFloor({ room }: { room: BossRoomDefinition }) { +function RoomFloor({ room, portalOpenings = false }: { room: BossRoomDefinition; portalOpenings?: boolean }) { return ( @@ -401,14 +433,384 @@ function RoomFloor({ room }: { room: BossRoomDefinition }) { - + ); } +function HockeyGoal({ z, color }: { z: number; color: string }) { + const goalWidth = HOCKEY_GOAL_HALF_WIDTH * 2; + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +function HockeyHealingRoom() { + const goalSideWidth = (HOCKEY_ARENA_WIDTH - HOCKEY_GOAL_HALF_WIDTH * 2) * 0.5; + const leftGoalSideX = HOCKEY_ARENA_MIN_X + goalSideWidth * 0.5; + const rightGoalSideX = HOCKEY_ARENA_MAX_X - goalSideWidth * 0.5; + const wallMaterial = ; + return ( + + + + + + + + + + + + + + + + + + + + + + + + + {wallMaterial} + + + + {wallMaterial} + + {[HOCKEY_ARENA_MIN_Z - 0.15, HOCKEY_ARENA_MAX_Z + 0.15].flatMap((z) => [leftGoalSideX, rightGoalSideX].map((x) => ( + + + + + )))} + + + + ); +} + +const BLOCKBREAKER_BIOME_FIXTURES = [ + [-12.75, -12.5, 0.94], + [-12.75, -1, 1.08], + [-12.75, 10.5, 0.9], + [12.75, -12.5, 1.02], + [12.75, -1, 0.88], + [12.75, 10.5, 1.12], +] as const; + +function BlockbreakerFixtureGeometry({ fixture }: { fixture: BlockbreakerBiomeFixture }) { + if (fixture === "crystal") return ; + if (fixture === "forge") return ; + if (fixture === "spire") return ; + if (fixture === "monolith") return ; + return ; +} + +function BlockbreakerBiomeFixtures({ biome }: { biome: BlockbreakerArenaBiome }) { + const mesh = useRef(null); + const transform = useMemo(() => new THREE.Object3D(), []); + + useLayoutEffect(() => { + if (!mesh.current) return; + BLOCKBREAKER_BIOME_FIXTURES.forEach(([x, z, scale], index) => { + transform.position.set(x, biome.fixture === "reactor" ? 1.45 : 1.75, z); + transform.rotation.set( + biome.fixture === "reactor" ? 0 : index % 2 === 0 ? -0.08 : 0.08, + index * 0.73, + biome.fixture === "reactor" ? 0 : index % 2 === 0 ? 0.06 : -0.06, + ); + transform.scale.setScalar(scale); + transform.updateMatrix(); + mesh.current!.setMatrixAt(index, transform.matrix); + }); + mesh.current.instanceMatrix.needsUpdate = true; + mesh.current.computeBoundingSphere(); + }, [biome.fixture, transform]); + + return ( + + + + + ); +} + +function BrightArcadeRoom({ + variant, + biome = BLOCKBREAKER_BIOMES[0], +}: { + variant: "blockbreaker" | "aether-assault"; + biome?: BlockbreakerArenaBiome; +}) { + const roomWidth = HOCKEY_ARENA_WIDTH + 10; + const roomLength = HOCKEY_ARENA_LENGTH + 12; + const roomMinX = HOCKEY_ARENA_MIN_X - 5; + const roomMaxX = HOCKEY_ARENA_MAX_X + 5; + const roomMinZ = HOCKEY_ARENA_MIN_Z - 6; + const roomMaxZ = HOCKEY_ARENA_MAX_Z + 6; + const wallHeight = 4.8; + const wallMaterial = ( + + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + {[HOCKEY_ARENA_MIN_X, HOCKEY_ARENA_MAX_X].map((x) => ( + + + + + ))} + {[HOCKEY_ARENA_MIN_Z, HOCKEY_ARENA_MAX_Z].map((z) => ( + + + + + ))} + {variant === "blockbreaker" ? ( + + + + + ) : ( + + {[-6.7, -3.35, 0, 3.35, 6.7].map((x) => ( + + + + + ))} + {[-10.7, -8.45, -6.2, -3.95].map((z) => ( + + + + + ))} + + )} + + {[roomMinX - 0.18, roomMaxX + 0.18].map((x) => ( + + + {wallMaterial} + + ))} + {[roomMinZ - 0.18, roomMaxZ + 0.18].map((z) => ( + + + {wallMaterial} + + ))} + {[roomMinX - 0.36, roomMaxX + 0.36].map((x) => ( + + + + + ))} + {[roomMinZ - 0.36, roomMaxZ + 0.36].map((z) => ( + + + + + ))} + {variant === "blockbreaker" && } + + ); +} + +function HealingHockeyPvpRoom() { + const width = HOCKEY_PVP_ARENA_MAX_X - HOCKEY_PVP_ARENA_MIN_X; + const length = HOCKEY_PVP_ARENA_MAX_Z - HOCKEY_PVP_ARENA_MIN_Z; + const goalSideWidth = (width - HOCKEY_PVP_GOAL_HALF_WIDTH * 2) * 0.5; + const sideCenters = [ + HOCKEY_PVP_ARENA_MIN_X + goalSideWidth * 0.5, + HOCKEY_PVP_ARENA_MAX_X - goalSideWidth * 0.5, + ]; + return ( + + + + + + + + + + + + + + + + + + + + {[-11, 11].map((z) => + + 0 ? "#64e7ff" : "#ff7688"} transparent opacity={0.38} /> + )} + {[HOCKEY_PVP_ARENA_MIN_X - 0.15, HOCKEY_PVP_ARENA_MAX_X + 0.15].map((x) => ( + + + + + ))} + {[-HOCKEY_PVP_ARENA_MAX_Z - 0.15, HOCKEY_PVP_ARENA_MAX_Z + 0.15].flatMap((z) => sideCenters.map((x) => ( + + + + + )))} + + + + + ); +} + +function HockeyPvpScoreboards() { + const localGoals = useGameStore((state) => state.hockeyPvp.opponentGoalsConceded); + const opponentGoals = useGameStore((state) => state.hockeyPvp.localGoalsConceded); + const texture = useMemo(() => { + const canvas = document.createElement("canvas"); + canvas.width = 512; + canvas.height = 192; + const next = new THREE.CanvasTexture(canvas); + next.colorSpace = THREE.SRGBColorSpace; + next.minFilter = THREE.LinearMipmapLinearFilter; + next.magFilter = THREE.LinearFilter; + return next; + }, []); + + useEffect(() => { + const canvas = texture.image as HTMLCanvasElement; + const context = canvas.getContext("2d"); + if (!context) return; + const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height); + gradient.addColorStop(0, "#061d28"); + gradient.addColorStop(0.5, "#05080d"); + gradient.addColorStop(1, "#2b0a17"); + context.fillStyle = gradient; + context.fillRect(0, 0, canvas.width, canvas.height); + context.strokeStyle = "#89efff"; + context.lineWidth = 5; + context.strokeRect(5, 5, canvas.width - 10, canvas.height - 10); + context.fillStyle = "#a8c4c9"; + context.font = "700 20px Inter, sans-serif"; + context.textAlign = "center"; + context.fillText("HEALING HOCKEY", canvas.width / 2, 31); + context.fillStyle = "#74ecff"; + context.font = "700 18px Inter, sans-serif"; + context.fillText("YOU", 132, 58); + context.fillStyle = "#ff819f"; + context.fillText("RIVAL", 380, 58); + context.font = "700 94px Impact, Inter, sans-serif"; + context.fillStyle = "#eaffff"; + context.fillText(String(Math.min(99, localGoals)).padStart(2, "0"), 132, 151); + context.fillStyle = "#ffedf4"; + context.fillText(String(Math.min(99, opponentGoals)).padStart(2, "0"), 380, 151); + context.fillStyle = "#f3d87c"; + context.font = "700 56px Inter, sans-serif"; + context.fillText("–", 256, 137); + texture.needsUpdate = true; + }, [localGoals, opponentGoals, texture]); + + useEffect(() => () => texture.dispose(), [texture]); + + return <>{([-1, 1] as const).map((side) => ( + + + + + + + + + + + ))}; +} + /** Main-display room projection. Gameplay stays in the shared arena domain. */ export function BossRoom() { const bossId = useGameStore((state) => state.boss.id); + const blockbreakerSeed = useGameStore((state) => state.blockbreaker.seed); + const hockeyMode = useGameStore((state) => state.activityMode === "hockey-healing"); + const blockbreakerMode = useGameStore((state) => state.activityMode === "blockbreaker"); + const aetherAssaultMode = useGameStore((state) => state.activityMode === "aether-assault"); + const hockeyPvpMode = useGameStore((state) => state.activityMode === "hockey-healing-pvp"); + const rpgPhase = useGameStore((state) => state.rpgRun?.phase ?? null); + const rpgBossRoom = useGameStore((state) => state.runMode === "rpg-roguelike" && state.activityMode === "boss"); + if (hockeyPvpMode) return ; + if (aetherAssaultMode) return ; + if (blockbreakerMode) return ; + if (hockeyMode) return ; const room = bossRoomFor(bossId); return ( @@ -418,7 +820,15 @@ export function BossRoom() { - + + {rpgBossRoom && rpgPhase && ( + + )} ); } diff --git a/src/components/BottomScreen.tsx b/src/components/BottomScreen.tsx index d8281d0..ae4cec8 100644 --- a/src/components/BottomScreen.tsx +++ b/src/components/BottomScreen.tsx @@ -1,12 +1,38 @@ import { ABILITY_ORDER } from "../game/data"; -import { HEALER_CLASSES } from "../game/healers"; +import { HEALER_CLASSES, resolveSlottedAbility } from "../game/healers"; import { BOSS_DEFINITIONS } from "../game/bossCatalog"; -import { GLOBAL_COOLDOWN_SECONDS, abilityRemaining, barrierProtects, upcomingEncounterMechanic, useGameStore } from "../game/store"; +import { GLOBAL_COOLDOWN_SECONDS, abilityRemaining, barrierProtects, healerFieldContains, upcomingEncounterMechanic, useGameStore } from "../game/store"; import { runAbilityCastTime, runAbilityCooldown, runAbilityManaCost } from "../game/roguelike"; import { PARTY_ABILITY_NAMES, tankAuraProtects } from "../game/partyCombat"; import type { BottomTab, PartyMember } from "../game/types"; import { useActiveHunter, useFrontendStore } from "../frontend/store"; import { DEFAULT_CONTROLLER_GLYPHS } from "../input/controllerGlyphs"; +import { + HOCKEY_ARENA_CENTER_Z, + HOCKEY_GOAL_HALF_WIDTH, + HOCKEY_HEALER_GOAL_Z, + HOCKEY_NPC_PADDLE_HALF_WIDTH, + HOCKEY_NPC_PADDLE_Z, + HOCKEY_NPC_GOAL_Z, +} from "../game/hockeyHealing"; +import { + HOCKEY_PVP_GOAL_DAMAGE, + HOCKEY_PVP_GOAL_HALF_WIDTH, + HOCKEY_PVP_GOAL_Z, + HOCKEY_PVP_SIDE_OFFSET_Z, +} from "../game/hockeyHealingPvp"; +import { bottomTabsFor } from "../game/bottomTabs"; +import { + BLOCKBREAKER_BREACH_DAMAGE, + BLOCKBREAKER_BRICK_COLORS, + BLOCKBREAKER_DANGER_Z, + blockbreakerColumnX, + blockbreakerRowZ, + blockbreakerTimeMultiplier, +} from "../game/blockbreaker"; +import { aetherShipColor } from "./aetherAssaultVisuals"; +import { ABILITY_CONTROLLER_BINDINGS } from "../game/controllerBindings"; +import { RpgRunTacticalPanel } from "./rpgRoguelike/RpgRunTacticalPanel"; function RewardSummary() { const rewards = useFrontendStore((state) => state.recentRewards); @@ -30,14 +56,15 @@ function PartyFrame({ member }: { member: PartyMember }) { const selected = useGameStore((state) => state.selectedMemberId === member.id); const selectMember = useGameStore((state) => state.selectMember); const time = useGameStore((state) => state.time); - const renewRemaining = Math.max(0, member.renewExpiresAt - time); + const activeHealingEffects = member.healingEffects.filter((effect) => effect.expiresAt > time).slice(0, 3); const knockedRemaining = Math.max(0, member.knockedUntil - time); const barrier = useGameStore((state) => state.barrier); const tankAura = useGameStore((state) => state.partyCombat.tankAura); - const tankPosition = useGameStore((state) => state.partyPositions.brann); + const tankPosition = useGameStore((state) => state.partyPositions[state.partyCombat.tankAura.sourceId]); const combatant = useGameStore((state) => member.id === "aelia" ? undefined : state.partyCombat.combatants[member.id]); const position = useGameStore((state) => state.partyPositions[member.id]); const protectedByBarrier = barrierProtects(position, barrier, time); + const linkedBySpirit = barrier.kind === "spirit-link" && healerFieldContains(position, barrier, time); const protectedByTank = tankAuraProtects(position, tankPosition, tankAura, time); const currentAction = combatant?.visualAction && combatant.visualAction.endsAt > time ? PARTY_ABILITY_NAMES[combatant.visualAction.abilityId] @@ -56,9 +83,14 @@ function PartyFrame({ member }: { member: PartyMember }) { {member.absorb > 0 && } - {renewRemaining > 0 && {Math.ceil(renewRemaining)}} + {activeHealingEffects.map((effect) => { + const label = effect.id === "renew" ? "R" : effect.id === "regrowth" ? "G" : effect.id === "rejuvenation" ? "J" : effect.id === "lifebloom" ? `L${effect.stacks}` : effect.id === "wild-growth" ? "W" : "T"; + return {label}; + })} + {member.reactiveHeal && member.reactiveHeal.expiresAt > time && {member.reactiveHeal.charges}} {member.debuffs.length > 0 && !} {protectedByBarrier && B} + {linkedBySpirit && S} {protectedByTank && T} {knockedRemaining > 0 && KD} @@ -78,7 +110,7 @@ function PartyList() { function AbilityButton({ abilityId }: { abilityId: (typeof ABILITY_ORDER)[number] }) { const healerClassId = useGameStore((state) => state.healerClassId); - const ability = HEALER_CLASSES[healerClassId].abilities[abilityId]; + const ability = useGameStore((state) => resolveSlottedAbility(state.abilityLoadout, abilityId)); const time = useGameStore((state) => state.time); const cooldowns = useGameStore((state) => state.cooldowns); const globalCooldownUntil = useGameStore((state) => state.globalCooldownUntil); @@ -89,15 +121,21 @@ function AbilityButton({ abilityId }: { abilityId: (typeof ABILITY_ORDER)[number const activeCast = useGameStore((state) => state.activeCast); const castAbility = useGameStore((state) => state.castAbility); const runModifiers = useGameStore((state) => state.runModifiers); + const healerMechanic = useGameStore((state) => state.healerMechanic); + if (!ability) { + return ; + } const remaining = abilityRemaining(abilityId, time, cooldowns); const manaCost = runAbilityManaCost(abilityId, ability.mana, runModifiers); - const castTime = ability.castTime ? runAbilityCastTime(abilityId, ability.castTime, runModifiers) : 0; + const baseCastTime = ability.castTime ? runAbilityCastTime(abilityId, ability.castTime, runModifiers) : 0; + const castTime = ability.id === "shaman-healing-wave" && healerMechanic.resource > 0 ? baseCastTime * 0.5 : baseCastTime; const cooldownDuration = runAbilityCooldown(abilityId, ability.cooldown, runModifiers); const globalRemaining = Math.max(0, globalCooldownUntil - time); - const noDispel = abilityId === "purify" && selected.debuffs.length === 0; + const noDispel = ability.pulseKind === "cleanse" && selected.debuffs.length === 0; const invalidTarget = ability.targeting === "ally" && selected.hp <= 0; const disabled = phase !== "combat" || !healerAlive || activeCast !== null || remaining > 0 || globalRemaining > 0 || mana < manaCost || noDispel || invalidTarget; - const resourceCopy = `${manaCost ? `${manaCost} mana` : "free"}${castTime ? ` · ${castTime.toFixed(1)}s` : ""}`; + const resourceName = HEALER_CLASSES[healerClassId].resourceName.toLowerCase(); + const resourceCopy = `${manaCost ? `${manaCost} ${resourceName}` : "free"}${castTime ? ` · ${castTime.toFixed(1)}s` : ""}`; return ( +

    {hockeyMode ? "Defend the wide goal while healing through two bosses. Left-stick direction sets every puck return; the moving enemy paddle tracks it and strikes it back. Each fallen boss awards loot and rolls its pet chance before replacement." : pvpMode ? `Face ${opponentName}. Both parties use normalized base gear and fight the same boss order. Every boss kill adds 5% global healing Dampening. Aim returns with left stick. A goal deals ${HOCKEY_PVP_GOAL_DAMAGE} damage to every member of the conceding party. Boss kills still award loot and pet chances.` : blockbreakerMode ? `Aim the puck into advancing five-column color rows while healing through two bosses. Orthogonal matching clusters break together. Rows start every 10 seconds and accelerate. Misses safely re-serve; each breach deals ${BLOCKBREAKER_BREACH_DAMAGE} damage to every party member.` : aetherMode ? "Move freely through the full runway while your arcane focus fires automatically. Heal with your normal kit. Dodge enemy volleys and diving ships; ship hits only threaten the healer. Bosses and rewards continue independently." : <>{healer.description} {definitions.map((boss) => boss.briefing).join(" ")}}

    +
    Prepared skills6 equipped
    @@ -195,6 +259,7 @@ function EndPanel({ onExit }: { onExit?: () => void }) { const hunter = useActiveHunter(); const phase = useGameStore((state) => state.phase); const runMode = useGameStore((state) => state.runMode); + const activityMode = useGameStore((state) => state.activityMode); const round = useGameStore((state) => state.round); const endlessMode = useGameStore((state) => state.endlessMode); const endlessBossKills = useGameStore((state) => state.endlessBossKills); @@ -207,38 +272,44 @@ function EndPanel({ onExit }: { onExit?: () => void }) { const startEncounter = useGameStore((state) => state.startEncounter); const totalHp = party.reduce((sum, member) => sum + member.hp, 0); const totalMax = party.reduce((sum, member) => sum + member.maxHp, 0); + const hockey = useGameStore((state) => state.hockey); + const hockeyPvp = useGameStore((state) => state.hockeyPvp); + const blockbreaker = useGameStore((state) => state.blockbreaker); + const aetherAssault = useGameStore((state) => state.aetherAssault); const showEndlessChoice = phase === "victory" && runMode === "rogue-trials" && round === 5 && !endlessMode; - const endlessDefeat = phase === "defeat" && endlessMode; + const hockeyDefeat = phase === "defeat" && activityMode === "hockey-healing"; + const blockbreakerDefeat = phase === "defeat" && activityMode === "blockbreaker"; + const aetherDefeat = phase === "defeat" && activityMode === "aether-assault"; + const pvpMatch = activityMode === "hockey-healing-pvp"; + const endlessDefeat = phase === "defeat" && endlessMode && !hockeyDefeat && !blockbreakerDefeat && !aetherDefeat; const endlessHighScore = hunter?.stats.highestRogueTrialsEndlessKills ?? 0; const endlessHighScoreLabel = `${endlessHighScore} ${endlessHighScore === 1 ? "boss" : "bosses"}`; return (
    {phase === "victory" ? "✦" : "×"} - {showEndlessChoice ? "ROGUE TRIALS CLEARED" : endlessDefeat ? "ENDLESS RUN COMPLETE" : phase === "victory" ? "TRIAL COMPLETE" : "FORMATION LOST"} -

    {showEndlessChoice ? "The trial can continue" : endlessDefeat ? `${endlessBossKills} bosses defeated` : phase === "victory" ? "Five souls endure" : "The vault claims its due"}

    + {showEndlessChoice ? "ROGUE TRIALS CLEARED" : hockeyDefeat ? "HOCKEY HEALING COMPLETE" : blockbreakerDefeat ? "BLOCKBREAKER RUN COMPLETE" : aetherDefeat ? "AETHER ASSAULT COMPLETE" : pvpMatch ? phase === "victory" ? "PVP MATCH WON" : "PVP MATCH LOST" : endlessDefeat ? "ENDLESS RUN COMPLETE" : phase === "victory" ? "TRIAL COMPLETE" : "FORMATION LOST"} +

    {showEndlessChoice ? "The trial can continue" : hockeyDefeat ? `${hockey.returns} pucks returned` : blockbreakerDefeat ? `${blockbreaker.score} points scored` : aetherDefeat ? `${aetherAssault.score} points scored` : pvpMatch ? phase === "victory" ? `${hockeyPvp.opponentName} fell first` : `${hockeyPvp.opponentName} wins` : endlessDefeat ? `${endlessBossKills} bosses defeated` : phase === "victory" ? "Five souls endure" : "The vault claims its due"}

    Duration{Math.floor(time / 60)}:{String(Math.floor(time % 60)).padStart(2, "0")} - Party vitality{Math.round((totalHp / totalMax) * 100)}% - {endlessDefeat ? "Endless kills" : "Boss"}{endlessDefeat ? endlessBossKills : phase === "victory" ? "Defeated" : "Standing"} + {hockeyDefeat ? "Puck returns" : blockbreakerDefeat ? "Bricks broken" : aetherDefeat ? "Wave reached" : pvpMatch ? "Goals" : "Party vitality"}{hockeyDefeat ? hockey.returns : blockbreakerDefeat ? blockbreaker.bricksBroken : aetherDefeat ? aetherAssault.wave : pvpMatch ? `${hockeyPvp.opponentGoalsConceded}–${hockeyPvp.localGoalsConceded}` : `${Math.round((totalHp / totalMax) * 100)}%`} + {hockeyDefeat || blockbreakerDefeat || aetherDefeat ? "Boss kills" : pvpMatch ? "Boss kills" : endlessDefeat ? "Endless kills" : "Boss"}{hockeyDefeat || blockbreakerDefeat || aetherDefeat || endlessDefeat ? endlessBossKills : pvpMatch ? `${endlessBossKills}–${hockeyPvp.opponentBossKills}` : phase === "victory" ? "Defeated" : "Standing"}
    - {phase === "victory" && } + {(phase === "victory" || hockeyDefeat || blockbreakerDefeat || aetherDefeat) && } {showEndlessChoice ?
    :
    - - + +
    }
    ); @@ -275,10 +346,182 @@ function MapPanel() { const time = useGameStore((state) => state.time); const phase = useGameStore((state) => state.phase); const bossId = useGameStore((state) => state.bossId); + const activityMode = useGameStore((state) => state.activityMode); + const hockey = useGameStore((state) => state.hockey); + const hockeyPvp = useGameStore((state) => state.hockeyPvp); + const blockbreaker = useGameStore((state) => state.blockbreaker); + const aetherAssault = useGameStore((state) => state.aetherAssault); + const hockeyPvpOpponent = useGameStore((state) => state.hockeyPvpOpponent); const bossDefinition = BOSS_DEFINITIONS[bossId]; const playerX = 120 + playerPosition[0] * 7; const playerY = 143 + playerPosition[1] * 5.3; const bossMotions = [bossMotion, ...additionalBosses.map((entry) => entry.motion)]; + if (activityMode === "aether-assault") { + const mapX = (x: number) => 120 + x * 8.5; + const mapY = (z: number) => 140 + (z - HOCKEY_ARENA_CENTER_Z) * 8; + return ( +
    +
    + Aether Assault +

    Arcane Formation Runway

    +

    Focus fire stays automatic. Move anywhere in the rink, heal freely, and evade red volleys plus amber dive warnings.

    +
    Party Bosses Ships Shots
    +
    +
    + + + + + {aetherAssault.ships.map((ship, index) => { + const color = aetherShipColor(aetherAssault.seed, aetherAssault.wave, index, ship.kind); + return + {ship.phase === "diving" && } + + ; + })} + {aetherAssault.playerShots.map((shot) => )} + {aetherAssault.enemyShots.map((shot) => )} + {bossMotions.map((motion, index) => )} + {(["brann", "nia", "orin", "vale"] as const).map((memberId) => )} + + + {barrier.expiresAt > time && } + + {phase === "combat" ? `WAVE ${aetherAssault.wave} · ${aetherAssault.score} SCORE` : "FORMATION PREVIEW"} +
    +
    + ); + } + if (activityMode === "blockbreaker") { + const mapX = (x: number) => 120 + x * 8.5; + const mapY = (z: number) => 140 + (z - HOCKEY_ARENA_CENTER_Z) * 8; + const colors: Record<(typeof BLOCKBREAKER_BRICK_COLORS)[number], string> = { + cyan: "#36d9ef", + amber: "#f1b74f", + magenta: "#e85aa9", + lime: "#93db54", + }; + return ( +
    +
    + Blockbreaker +

    Advancing Color Wall

    +

    Match orthogonal colors. Crossing bricks disappear and deal {BLOCKBREAKER_BREACH_DAMAGE} partywide damage.

    +
    Party Bosses Bricks Puck
    +
    +
    + + + + + + {blockbreaker.bricks.map((brick) => ( + + ))} + {bossMotions.map((motion, index) => )} + + + {(["brann", "nia", "orin", "vale"] as const).map((memberId) => )} + + {barrier.expiresAt > time && } + + {phase === "combat" ? `${blockbreaker.bricks.length} BRICKS · ${blockbreaker.score} SCORE` : "WALL PREVIEW"} +
    +
    + ); + } + if (activityMode === "hockey-healing-pvp") { + const mapX = (x: number) => 120 + x * 8.2; + const mapY = (z: number) => 140 + z * 4.85; + const localWorldZ = (z: number) => z + HOCKEY_PVP_SIDE_OFFSET_Z; + const opponentWorldX = (x: number) => -x; + const opponentWorldZ = (z: number) => -z - HOCKEY_PVP_SIDE_OFFSET_Z; + return ( +
    +
    + Healing Hockey PVP +

    You vs {hockeyPvp.opponentName}

    +

    Matching boss order. Each goal hits all five allies for {HOCKEY_PVP_GOAL_DAMAGE} damage.

    +
    Your party Bosses Rival Puck
    +
    +
    + + + + + + + + + + + {(["brann", "nia", "orin", "vale"] as const).map((memberId) => )} + {(["aelia", "brann", "nia", "orin", "vale"] as const).map((memberId) => )} + + + {phase === "combat" ? `GOALS ${hockeyPvp.opponentGoalsConceded}–${hockeyPvp.localGoalsConceded} · LIVE` : "VERSUS RINK"} +
    +
    + ); + } + if (activityMode === "hockey-healing") { + const mapX = (x: number) => 120 + x * 8.5; + const mapY = (z: number) => 140 + (z - HOCKEY_ARENA_CENTER_Z) * 8; + const npcGoalLeft = mapX(-HOCKEY_GOAL_HALF_WIDTH); + const npcGoalRight = mapX(HOCKEY_GOAL_HALF_WIDTH); + return ( +
    +
    + Hockey Healing +

    Rectangular Boss Rink

    +

    Party fights enemy half. Moving Pong paddle tracks each return and strikes it back toward healer.

    +
    Party Boss Paddle Puck
    +
    +
    + + + + + + + + + + {bossMotions.map((motion, index) => ( + + + + + ))} + + + {(["brann", "nia", "orin", "vale"] as const).map((memberId) => ( + + ))} + + {barrier.expiresAt > time && } + + {phase === "combat" ? `${hockey.returns} RETURNS · LIVE` : "RINK PREVIEW"} +
    +
    + ); + } return (
    @@ -362,33 +605,132 @@ function PackPanel() { ); } -const tabs: { id: BottomTab; label: string; icon: string; key: string }[] = [ - { id: "combat", label: "Heal", icon: "✦", key: "" }, - { id: "map", label: "Map", icon: "⌁", key: "M" }, - { id: "pack", label: "Pack", icon: "▧", key: "I" }, -]; +function PvpPanel() { + const opponentName = useGameStore((state) => state.hockeyPvp.opponentName); + const opponentParty = useGameStore((state) => state.hockeyPvpOpponent.party); + const opponentGoalsConceded = useGameStore((state) => state.hockeyPvp.opponentGoalsConceded); + const localGoalsConceded = useGameStore((state) => state.hockeyPvp.localGoalsConceded); + const opponentBossKills = useGameStore((state) => state.hockeyPvp.opponentBossKills); + const living = opponentParty.filter((member) => member.hp > 0).length; + const currentHealth = opponentParty.reduce((total, member) => total + Math.max(0, member.hp), 0); + const maximumHealth = opponentParty.reduce((total, member) => total + member.maxHp, 0); + + return ( +
    +
    + Opponent party{opponentName} +
    Goals{opponentGoalsConceded}–{localGoalsConceded}
    +
    Boss KOs{opponentBossKills}
    +
    Standing{living} / {opponentParty.length}
    +
    +
    +
    Rival health feed{Math.ceil(currentHealth)} / {maximumHealth} total
    + {opponentParty.map((member) => ( +
    + {member.name[0]} + + {member.name}{Math.ceil(member.hp)} / {member.maxHp} + + {member.className} + + {member.hp <= 0 ? "DOWN" : `${Math.ceil((member.hp / member.maxHp) * 100)}%`} +
    + ))} +
    +
    + ); +} + +const tabPresentation: Record = { + combat: { label: "Heal", icon: "✦", key: "" }, + map: { label: "Map", icon: "⌁", key: "M" }, + pack: { label: "Pack", icon: "▧", key: "I" }, + pvp: { label: "PVP", icon: "⚔", key: "P" }, +}; + +function RpgBottomDisplay({ run, focusedId, paused, onExit }: { + readonly run: NonNullable["rpgRun"]>; + readonly focusedId: string | null; + readonly paused: boolean; + readonly onExit?: () => void; +}) { + const party = useGameStore((state) => state.party); + const selectedMemberId = useGameStore((state) => state.selectedMemberId); + const mana = useGameStore((state) => state.mana); + const maxMana = useGameStore((state) => state.maxMana); + const cooldowns = useGameStore((state) => state.cooldowns); + const globalCooldownUntil = useGameStore((state) => state.globalCooldownUntil); + const time = useGameStore((state) => state.time); + const activeCast = useGameStore((state) => state.activeCast); + const spellResources = useGameStore((state) => state.rpgSpellResources); + const selectMember = useGameStore((state) => state.selectMember); + const castAbility = useGameStore((state) => state.castAbility); + const dispatchRpgAction = useGameStore((state) => state.dispatchRpgAction); + const setRpgFocusId = useGameStore((state) => state.setRpgFocusId); + const restart = useGameStore((state) => state.restart); + return ( +
    + + {paused && ( + + )} +
    + ); +} export function BottomScreen({ onExit }: { onExit?: () => void } = {}) { const activeTab = useGameStore((state) => state.activeTab); const setActiveTab = useGameStore((state) => state.setActiveTab); const phase = useGameStore((state) => state.phase); const paused = useGameStore((state) => state.paused); + const runMode = useGameStore((state) => state.runMode); + const activityMode = useGameStore((state) => state.activityMode); + const rpgRun = useGameStore((state) => state.rpgRun); + const rpgFocusId = useGameStore((state) => state.rpgFocusId); + const tabs = bottomTabsFor(runMode); + if (runMode === "rpg-roguelike" && rpgRun) { + return ; + } return (
    IHI Want To Heal{phase === "combat" ? "Encounter live" : "Field console"}
    -
    {activeTab === "combat" && } {activeTab === "map" && } - {activeTab === "pack" && } + {activeTab === "pack" && activityMode !== "hockey-healing-pvp" && } + {activeTab === "pvp" && activityMode === "hockey-healing-pvp" && }
    {paused && (
    - + {continuation === "create" ? "Create hunter" : resolvingOnline ? "Loading online save…" : "Continue"} {continuation === "create" ? `Use slot ${selectedSlotId}` : continuation === "online" ? "Download online copy" : continuation === "choose" ? "Choose online or device copy" : `Slot ${selectedSlotId} · ${selected.local?.hunterName}`} - - uploadSlot(selectedSlotId)}>UploadDevice → server - downloadSlot(selectedSlotId)}>DownloadServer → device - openSaveDialog("copy")}>CopyDuplicate save - openSaveDialog("delete")}>DeleteErase device copy - navigate("login")}>BackLogin screen + + uploadSlot(selectedSlotId)}>UploadDevice → server + downloadSlot(selectedSlotId)}>DownloadServer → device + openSaveDialog("copy")}>CopyDuplicate save + openSaveDialog("delete")}>DeleteErase device copy + navigate("login")}>BackLogin screen
    Autosave OFFLINE FIRST
    {notice || "Lower display shows selected save details."}
    {dialog && ( @@ -381,9 +413,9 @@ function SaveScreen() {
    - { void continueWithOnline(); }}>{resolvingOnline ? "Loading…" : "Continue online copy"}{formatSaveTimestamp(selected.online.updatedAt)} - { setDialog(null); playSlot(selectedSlotId); }}>Continue device copy{formatSaveTimestamp(selected.local.updatedAt)} - setDialog(null)}>Cancel + { void continueWithOnline(); }}>{resolvingOnline ? "Loading…" : "Continue online copy"}{formatSaveTimestamp(selected.online.updatedAt)} + { setDialog(null); playSlot(selectedSlotId); }}>Continue device copy{formatSaveTimestamp(selected.local.updatedAt)} + setDialog(null)}>Cancel
    {versionError &&
    {versionError}
    }
    @@ -392,19 +424,21 @@ function SaveScreen() { New offline save

    Name your hunter

    This name identifies the character in local and online save lists.

    controller.select("hunter-name")} onChange={(event) => setHunterName(event.target.value)} onKeyDown={(event) => { if (event.key === "Escape") setDialog(null); }} placeholder="Enter name" /> {normalizeHunterName(hunterName).length}/{MAX_HUNTER_NAME_LENGTH}
    - Create hunter - setDialog(null)}>Cancel + Create hunter + setDialog(null)}>Cancel
    ) : dialog === "copy" ? ( @@ -412,9 +446,9 @@ function SaveScreen() { Copy local save

    Choose destination

    Destination local save will be overwritten. Online copies stay unchanged.

    {slots.filter((slot) => slot.id !== selectedSlotId).map((slot) => ( - { copySlot(selectedSlotId, slot.id); setDialog(null); }}> + { copySlot(selectedSlotId, slot.id); setDialog(null); }}> Slot {slot.id}{slot.local ? "Overwrite" : "Empty"} - + ))}
    @@ -422,8 +456,8 @@ function SaveScreen() { <> Delete local save

    Erase slot {selectedSlotId}?

    Device copy will be removed. Existing online version remains available for download.

    - { deleteSlot(selectedSlotId); setDialog(null); }}>Delete local - setDialog(null)}>Cancel + { deleteSlot(selectedSlotId); setDialog(null); }}>Delete local + setDialog(null)}>Cancel
    )} @@ -470,9 +504,13 @@ function SaveScreen() { } const HOME_MODES: { id: GameModeId; icon: string; label: string; copy: string }[] = [ - { id: "roguelike-pve", icon: "✦", label: "PVE", copy: "Randomized roguelike runs" }, + { id: "roguelike-pve", icon: "✦", label: "RPG Roguelike", copy: "Draft party, spells, gear, and route" }, { id: "rogue-trials", icon: "Ⅲ", label: "Rogue Trials", copy: "Four rounds, then a boss trio" }, { id: "dungeons", icon: "♜", label: "Dungeons", copy: "Choose your boss encounter" }, + { id: "hockey-healing", icon: "◌", label: "Hockey Healing", copy: "Defend goal under boss pressure" }, + { id: "hockey-healing-pvp", icon: "◇", label: "Healing Hockey PVP", copy: "Online mirrored healer duel" }, + { id: "blockbreaker", icon: "▦", label: "Blockbreaker", copy: "Break color walls while healing" }, + { id: "aether-assault", icon: "⌁", label: "Aether Assault", copy: "Auto-fire through arcane formations" }, { id: "roguelike-pvp", icon: "⚔", label: "Roguelike PvP", copy: "Draft, race, sabotage" }, { id: "stadium-pvp", icon: "◉", label: "Stadium PvP", copy: "Prepared 5v5 rounds" }, ]; @@ -482,28 +520,34 @@ function HomeScreen() { const accountId = useFrontendStore((state) => state.accountId); const selectMode = useFrontendStore((state) => state.selectMode); const selectHealerClass = useFrontendStore((state) => state.selectHealerClass); + const openAppearanceLab = useFrontendStore((state) => state.openAppearanceLab); const navigate = useFrontendStore((state) => state.navigate); const actions = useMemo(() => [ - { id: "roguelike-pve", run: () => selectMode("roguelike-pve"), neighbors: { right: "rogue-trials", down: "roguelike-pvp" } }, - { id: "rogue-trials", run: () => selectMode("rogue-trials"), neighbors: { left: "roguelike-pve", right: "dungeons", down: "stadium-pvp" } }, - { id: "dungeons", run: () => selectMode("dungeons"), neighbors: { left: "rogue-trials", down: "stadium-pvp" } }, - { id: "roguelike-pvp", run: () => selectMode("roguelike-pvp"), neighbors: { left: "roguelike-pve", right: "stadium-pvp", up: "roguelike-pve", down: "profile" } }, - { id: "stadium-pvp", run: () => selectMode("stadium-pvp"), neighbors: { left: "roguelike-pvp", up: "rogue-trials", down: "settings" } }, - { id: "profile", run: () => navigate("profile"), neighbors: { up: "roguelike-pvp", right: "gear", down: "class-priest" } }, - { id: "gear", run: () => navigate("gear"), neighbors: { up: "roguelike-pvp", left: "profile", right: "settings", down: "class-druid" } }, - { id: "settings", run: () => navigate("settings"), neighbors: { up: "stadium-pvp", left: "gear", down: "class-shaman" } }, + { id: "roguelike-pve", run: () => selectMode("roguelike-pve"), neighbors: { right: "rogue-trials", down: "hockey-healing" } }, + { id: "rogue-trials", run: () => selectMode("rogue-trials"), neighbors: { left: "roguelike-pve", right: "dungeons", down: "hockey-healing-pvp" } }, + { id: "dungeons", run: () => selectMode("dungeons"), neighbors: { left: "rogue-trials", down: "blockbreaker" } }, + { id: "hockey-healing", run: () => selectMode("hockey-healing"), neighbors: { right: "hockey-healing-pvp", up: "roguelike-pve", down: "roguelike-pvp" } }, + { id: "hockey-healing-pvp", run: () => selectMode("hockey-healing-pvp"), neighbors: { left: "hockey-healing", right: "blockbreaker", up: "rogue-trials", down: "stadium-pvp" } }, + { id: "blockbreaker", run: () => selectMode("blockbreaker"), neighbors: { left: "hockey-healing-pvp", up: "dungeons", down: "aether-assault" } }, + { id: "roguelike-pvp", run: () => selectMode("roguelike-pvp"), neighbors: { right: "stadium-pvp", up: "hockey-healing", down: "profile" } }, + { id: "stadium-pvp", run: () => selectMode("stadium-pvp"), neighbors: { left: "roguelike-pvp", right: "aether-assault", up: "hockey-healing-pvp", down: "gear" } }, + { id: "aether-assault", run: () => selectMode("aether-assault"), neighbors: { left: "stadium-pvp", up: "blockbreaker", down: "settings" } }, + { id: "profile", run: () => navigate("profile"), neighbors: { up: "roguelike-pvp", right: "gear", down: "appearance" } }, + { id: "gear", run: () => navigate("gear"), neighbors: { up: "stadium-pvp", left: "profile", down: "settings" } }, + { id: "appearance", run: openAppearanceLab, neighbors: { up: "profile", right: "settings", down: "class-priest" } }, + { id: "settings", run: () => navigate("settings"), neighbors: { up: "gear", left: "appearance", down: "class-druid" } }, ...HEALER_CLASS_ORDER.map((classId, index) => ({ id: `class-${classId}`, run: () => selectHealerClass(classId), neighbors: { - left: `class-${HEALER_CLASS_ORDER[(index + HEALER_CLASS_ORDER.length - 1) % HEALER_CLASS_ORDER.length]}`, - right: `class-${HEALER_CLASS_ORDER[(index + 1) % HEALER_CLASS_ORDER.length]}`, - up: index === 0 ? "profile" : index === 1 ? "gear" : "settings", - down: "change-save", + left: `class-${HEALER_CLASS_ORDER[index % 3 === 0 ? Math.min(index + 2, HEALER_CLASS_ORDER.length - 1) : index - 1]}`, + right: `class-${HEALER_CLASS_ORDER[index % 3 === 2 || index === HEALER_CLASS_ORDER.length - 1 ? index - (index % 3) : index + 1]}`, + up: index < 3 ? (["appearance", "settings", "settings"] as const)[index] : `class-${HEALER_CLASS_ORDER[index - 3]}`, + down: index + 3 < HEALER_CLASS_ORDER.length ? `class-${HEALER_CLASS_ORDER[index + 3]}` : "change-save", }, })), - { id: "change-save", run: () => navigate("saves"), neighbors: { up: "class-druid" } }, - ], [navigate, selectHealerClass, selectMode]); + { id: "change-save", run: () => navigate("saves"), neighbors: { up: "class-chronomancer" } }, + ], [navigate, openAppearanceLab, selectHealerClass, selectMode]); const controller = useMenuController(actions, { columns: 2, onBack: () => navigate("saves") }); if (!hunter) return null; const activeHealer = HEALER_CLASSES[hunter.activeClassId]; @@ -516,15 +560,16 @@ function HomeScreen() {
    Welcome back, {hunter.hunterName}{accountId ? "● SYNC READY" : "○ OFFLINE"}
    {HOME_MODES.map((mode) => ( - selectMode(mode.id)}> + selectMode(mode.id)}> {mode.icon}{mode.copy}{mode.label} - + ))}
    - navigate("profile")}>Hunter ProfileStats & collection log - navigate("gear")}>Gear UpgradeSpend group drops - navigate("settings")}>SettingsAudio, display, controls + navigate("profile")}>Hunter ProfileStats & collection log + navigate("gear")}>Gear UpgradeSpend group drops + Appearance LabBuild and preview your healer + navigate("settings")}>SettingsAudio, display, controls
    @@ -544,105 +589,301 @@ function HomeScreen() {
    Choose healer
    {HEALER_CLASS_ORDER.map((classId) => { const healer = HEALER_CLASSES[classId]; const progress = hunter.healers[classId]; - return selectHealerClass(classId)}> + return selectHealerClass(classId)}> {healer.icon}{healer.name}Level {progress.level} · {progress.inventory.length} items{classId === hunter.activeClassId ? "✓" : ""} - ; + ; })}
    - navigate("saves")}>Change save slotLast saved {formatSaveTimestamp(hunter.updatedAt)} + navigate("saves")}>Change save slotLast saved {formatSaveTimestamp(hunter.updatedAt)} } /> ); } -type ProfileStatId = BossId | "roguelike" | "rogue-trials-endless"; - function ProfileScreen() { const hunter = useActiveHunter(); const accountId = useFrontendStore((state) => state.accountId); const navigate = useFrontendStore((state) => state.navigate); + const uploadSlot = useFrontendStore((state) => state.uploadSlot); + const collectionView = useFrontendStore((state) => state.profileCollectionView); + const groupId = useFrontendStore((state) => state.selectedProfileGroupId); + const selectedStat = useFrontendStore((state) => state.selectedProfileStatId); + const setCollectionView = useFrontendStore((state) => state.selectProfileCollectionView); + const setGroupId = useFrontendStore((state) => state.selectProfileGroup); + const setSelectedStat = useFrontendStore((state) => state.selectProfileStat); const collections = useMemo(() => hunter ? buildCollections(hunter.collectionLog, hunter.stats.bossKills) : [], [hunter]); - const [groupId, setGroupId] = useState(collections[0]?.groupId ?? ""); - const [collectionView, setCollectionView] = useState<"loot" | "trophies" | "stats">("trophies"); + const bosses = useMemo(() => alphabeticalBosses(collections), [collections]); const collection = collections.find((group) => group.groupId === groupId) ?? collections[0]; - const [selectedStat, setSelectedStat] = useState("roguelike"); + const profileView = collectionView === "loot" ? "loot" : "stats"; + const activeSection = profileSectionForStat(selectedStat); + const activeSectionDefinition = PROFILE_SECTIONS.find((section) => section.id === activeSection) ?? PROFILE_SECTIONS[0]; + const selectedBoss = isBossProfileStat(selectedStat) ? bosses.find((boss) => boss.bossId === selectedStat) : undefined; const [leaderboard, setLeaderboard] = useState(null); const [leaderboardStatus, setLeaderboardStatus] = useState(""); + const [leaderboardUpdatedAt, setLeaderboardUpdatedAt] = useState(null); + const [leaderboardOwner, setLeaderboardOwner] = useState(null); + const [refreshingLeaderboard, setRefreshingLeaderboard] = useState(false); + const leaderboardRequestId = useRef(0); + const leaderboardRefreshActive = useRef(false); + const displaySurface = document.documentElement.dataset.displaySurface; + const rendersTopSurface = displaySurface !== "bottom"; + const rendersBottomSurface = displaySurface !== "top"; useEffect(() => { - if (selectedStat === "roguelike" || selectedStat === "rogue-trials-endless" || collection?.bosses.some((boss) => boss.bossId === selectedStat)) return; - setSelectedStat(collection?.bosses[0]?.bossId ?? "roguelike"); - }, [collection, selectedStat]); + if (!isBossProfileStat(selectedStat) || bosses.some((boss) => boss.bossId === selectedStat)) return; + setSelectedStat(bosses[0]?.bossId ?? "roguelike"); + }, [bosses, selectedStat, setSelectedStat]); useEffect(() => { - if (!hunter || collectionView !== "stats") return; + if (!rendersTopSurface || !hunter || profileView !== "stats" || activeSection === "bosses") return; + const requestId = ++leaderboardRequestId.current; + leaderboardRefreshActive.current = false; + setRefreshingLeaderboard(false); + const cached = leaderboardCache.read(hunter.slotId, selectedStat, hunter.hunterName, accountId); + setLeaderboard(cached?.result ?? null); + setLeaderboardUpdatedAt(cached?.updatedAt ?? null); + setLeaderboardOwner(cached?.accountId ?? accountId); + setLeaderboardStatus(cached + ? "" + : accountId + ? "No cached rankings. Refresh when online." + : "Sign in once, then refresh rankings for offline viewing."); + return () => { + if (leaderboardRequestId.current === requestId) leaderboardRequestId.current += 1; + }; + }, [accountId, activeSection, hunter, profileView, rendersTopSurface, selectedStat]); + const refreshLeaderboard = useCallback(async () => { + if (!rendersTopSurface || !hunter || leaderboardRefreshActive.current) return; if (!accountId) { - setLeaderboard(null); - setLeaderboardStatus("Sign in to view overall rankings."); + setLeaderboardStatus("Sign in to refresh online rankings."); return; } - let cancelled = false; - setLeaderboardStatus("Loading overall rankings…"); + if (!networkAppearsOnline()) { + setLeaderboardStatus("Offline. Cached rankings remain available."); + return; + } + const requestId = ++leaderboardRequestId.current; + leaderboardRefreshActive.current = true; + setRefreshingLeaderboard(true); + if (hasPendingSaveSync(hunter.slotId)) { + setLeaderboardStatus("Publishing local records…"); + const uploaded = await uploadSlot(hunter.slotId); + if (leaderboardRequestId.current !== requestId) return; + if (!uploaded) { + setLeaderboardStatus("Local records queued. Refresh when connection returns."); + leaderboardRefreshActive.current = false; + setRefreshingLeaderboard(false); + return; + } + } + setLeaderboardStatus("Refreshing online rankings…"); const request = selectedStat === "roguelike" ? onlineRepository.roguelikeLeaderboard(hunter.slotId) : selectedStat === "rogue-trials-endless" ? onlineRepository.rogueTrialsEndlessLeaderboard(hunter.slotId) + : selectedStat === "hockey-healing" + ? onlineRepository.hockeyHealingLeaderboard(hunter.slotId) + : selectedStat === "hockey-pvp-wins" + ? onlineRepository.hockeyPvpWinsLeaderboard(hunter.slotId) + : selectedStat === "hockey-pvp-boss-kills" + ? onlineRepository.hockeyPvpBossKillsLeaderboard(hunter.slotId) + : selectedStat === "blockbreaker-bricks" + ? onlineRepository.blockbreakerBricksLeaderboard(hunter.slotId) + : selectedStat === "blockbreaker-time" + ? onlineRepository.blockbreakerTimeLeaderboard(hunter.slotId) + : selectedStat === "blockbreaker-score" + ? onlineRepository.blockbreakerScoreLeaderboard(hunter.slotId) + : selectedStat === "aether-assault" + ? onlineRepository.aetherAssaultLeaderboard(hunter.slotId) : onlineRepository.bossLeaderboard(selectedStat, hunter.slotId); - void request.then((result) => { - if (cancelled) return; + try { + const result = await request; + const cached = leaderboardCache.write(accountId, hunter.hunterName, hunter.slotId, selectedStat, result); + if (leaderboardRequestId.current !== requestId) return; setLeaderboard(result); + setLeaderboardUpdatedAt(cached.updatedAt); + setLeaderboardOwner(accountId); setLeaderboardStatus(""); - }).catch((error) => { - if (cancelled) return; - setLeaderboard(null); + } catch (error) { + if (leaderboardRequestId.current !== requestId) return; setLeaderboardStatus(error instanceof Error ? error.message : "Leaderboard unavailable."); - }); - return () => { cancelled = true; }; - }, [accountId, collectionView, hunter, selectedStat]); + } finally { + if (leaderboardRequestId.current === requestId) { + leaderboardRefreshActive.current = false; + setRefreshingLeaderboard(false); + } + } + }, [accountId, hunter, rendersTopSurface, selectedStat, uploadSlot]); + const sectionMetricIds = activeSectionDefinition.statIds; + const bossGridColumns = 7; const actions = useMemo(() => [ - { id: "view-trophies", run: () => setCollectionView("trophies"), neighbors: { right: "view-stats" } }, - { id: "view-stats", run: () => setCollectionView("stats"), neighbors: { left: "view-trophies", right: "view-loot", down: collectionView === "stats" ? "stat-roguelike" : undefined } }, - { id: "view-loot", run: () => setCollectionView("loot"), neighbors: { left: "view-stats" } }, - ...(collectionView === "stats" ? [ - { id: "stat-roguelike", run: () => setSelectedStat("roguelike"), neighbors: { up: "view-stats", down: "stat-rogue-trials-endless" } }, - { id: "stat-rogue-trials-endless", run: () => setSelectedStat("rogue-trials-endless"), neighbors: { up: "stat-roguelike", down: `stat-${collection.bosses[0].bossId}` } }, - ...collection.bosses.map((boss, index) => ({ - id: `stat-${boss.bossId}`, - run: () => setSelectedStat(boss.bossId), - neighbors: { - up: index === 0 ? "stat-rogue-trials-endless" : `stat-${collection.bosses[index - 1].bossId}`, - down: index === collection.bosses.length - 1 ? `group-${collection.groupId}` : `stat-${collection.bosses[index + 1].bossId}`, - }, - })), + ...(rendersTopSurface ? [ + { id: "view-stats", run: () => setCollectionView("stats"), neighbors: { right: "view-loot", down: profileView === "stats" ? "section-roguelike" : undefined } }, + { id: "view-loot", run: () => setCollectionView("loot"), neighbors: { left: "view-stats" } }, + ...(profileView === "stats" ? [ + ...PROFILE_SECTIONS.map((section, index) => ({ + id: `section-${section.id}`, + run: () => setSelectedStat(defaultStatForSection(section.id, bosses)), + neighbors: { + up: index === 0 ? "view-stats" : `section-${PROFILE_SECTIONS[index - 1].id}`, + down: index === PROFILE_SECTIONS.length - 1 ? "section-roguelike" : `section-${PROFILE_SECTIONS[index + 1].id}`, + right: section.id === "bosses" ? `boss-card-${bosses[0]?.bossId}` : `metric-${section.statIds[0]}`, + }, + })), + ...(activeSection === "bosses" ? bosses.map((boss, index) => { + const rowStart = index % bossGridColumns === 0; + const rowEnd = index % bossGridColumns === bossGridColumns - 1 || index === bosses.length - 1; + return { + id: `boss-card-${boss.bossId}`, + run: () => setSelectedStat(boss.bossId), + neighbors: { + left: rowStart ? "section-bosses" : `boss-card-${bosses[index - 1].bossId}`, + right: rowEnd ? `boss-card-${boss.bossId}` : `boss-card-${bosses[index + 1].bossId}`, + up: index < bossGridColumns ? "view-stats" : `boss-card-${bosses[index - bossGridColumns].bossId}`, + down: bosses[index + bossGridColumns] ? `boss-card-${bosses[index + bossGridColumns].bossId}` : `boss-card-${boss.bossId}`, + }, + }; + }) : [ + ...sectionMetricIds.map((statId, index) => ({ + id: `metric-${statId}`, + run: () => setSelectedStat(statId), + neighbors: { + left: index === 0 ? `section-${activeSection}` : `metric-${sectionMetricIds[index - 1]}`, + right: index === sectionMetricIds.length - 1 ? "refresh-leaderboard" : `metric-${sectionMetricIds[index + 1]}`, + up: "view-stats", + down: "refresh-leaderboard", + }, + })), + { id: "refresh-leaderboard", run: () => { void refreshLeaderboard(); }, enabled: Boolean(accountId), neighbors: { left: `metric-${selectedStat}`, up: `metric-${selectedStat}` } }, + ]), + ] : []), + { id: "back", run: () => navigate("home") }, ] : []), - ...collections.map((group) => ({ id: `group-${group.groupId}`, run: () => setGroupId(group.groupId) })), - { id: "back", run: () => navigate("home") }, - ], [collection, collectionView, collections, navigate]); + ...(rendersBottomSurface && profileView === "loot" ? collections.map((group) => ({ id: `group-${group.groupId}`, run: () => setGroupId(group.groupId) })) : []), + ], [accountId, activeSection, bosses, collections, navigate, profileView, refreshLeaderboard, rendersBottomSurface, rendersTopSurface, sectionMetricIds, selectedStat, setCollectionView, setGroupId, setSelectedStat]); const controller = useMenuController(actions, { onBack: () => navigate("home") }); + useEffect(() => { + const hoveredSection = PROFILE_SECTIONS.find((section) => `section-${section.id}` === controller.selectedId); + if (hoveredSection) { + const nextStat = defaultStatForSection(hoveredSection.id, bosses); + if (nextStat !== selectedStat) setSelectedStat(nextStat); + return; + } + const hoveredMetric = sectionMetricIds.find((statId) => `metric-${statId}` === controller.selectedId); + if (hoveredMetric && hoveredMetric !== selectedStat) { + setSelectedStat(hoveredMetric); + return; + } + const hoveredBoss = bosses.find((boss) => `boss-card-${boss.bossId}` === controller.selectedId); + if (hoveredBoss && hoveredBoss.bossId !== selectedStat) setSelectedStat(hoveredBoss.bossId); + }, [bosses, controller.selectedId, sectionMetricIds, selectedStat, setSelectedStat]); if (!hunter || !collection) return null; const activeHealer = HEALER_CLASSES[hunter.activeClassId]; const activeProgress = hunter.healers[hunter.activeClassId]; const earned = collection.drops.filter((drop) => drop.count > 0).length; - const trophiesEarned = collection.bosses.filter((boss) => boss.pet.count > 0).length; + const petsOwned = bosses.filter((boss) => boss.pet.count > 0).length; + const hockeyDuration = `${Math.floor(hunter.stats.longestHockeyHealingSecondsAtBest / 60)}:${String(Math.floor(hunter.stats.longestHockeyHealingSecondsAtBest % 60)).padStart(2, "0")}`; + const blockbreakerDuration = `${Math.floor(hunter.stats.longestBlockbreakerSeconds / 60)}:${String(Math.floor(hunter.stats.longestBlockbreakerSeconds % 60)).padStart(2, "0")}`; + const aetherDuration = `${Math.floor(hunter.stats.longestAetherAssaultSecondsAtBest / 60)}:${String(Math.floor(hunter.stats.longestAetherAssaultSecondsAtBest % 60)).padStart(2, "0")}`; + const metrics: { id: ProfileStatId; label: string; value: string; copy: string }[] = activeSection === "roguelike" + ? [{ id: "roguelike", label: "Highest round", value: hunter.stats.highestRoguelikeRound.toLocaleString(), copy: "Best run before defeat" }] + : activeSection === "rogue-trials" + ? [{ id: "rogue-trials-endless", label: "Endless best", value: hunter.stats.highestRogueTrialsEndlessKills.toLocaleString(), copy: "Bosses defeated in one run" }] + : activeSection === "hockey" + ? [{ id: "hockey-healing", label: "Hockey record", value: `${hunter.stats.highestHockeyHealingReturns} returns`, copy: `${hockeyDuration} longest survival` }] + : activeSection === "hockey-pvp" + ? [ + { id: "hockey-pvp-wins", label: "Match record", value: `${hunter.stats.hockeyHealingPvpWins}W · ${hunter.stats.hockeyHealingPvpLosses}L`, copy: "Lifetime PVP results" }, + { id: "hockey-pvp-boss-kills", label: "Boss race kills", value: hunter.stats.hockeyHealingPvpBossKills.toLocaleString(), copy: "Lifetime PVP bosses" }, + ] + : activeSection === "aether-assault" + ? [{ id: "aether-assault", label: "Overall score", value: hunter.stats.highestAetherAssaultScore.toLocaleString(), copy: `Wave ${hunter.stats.highestAetherAssaultWaveAtBest} · ${aetherDuration}` }] + : [ + { id: "blockbreaker-score", label: "Overall score", value: hunter.stats.highestBlockbreakerScore.toLocaleString(), copy: "Highest single-run score" }, + { id: "blockbreaker-bricks", label: "Bricks broken", value: hunter.stats.highestBlockbreakerBricks.toLocaleString(), copy: "Most in one run" }, + { id: "blockbreaker-time", label: "Time survived", value: blockbreakerDuration, copy: "Longest run" }, + ]; const selectedStatValue = selectedStat === "roguelike" ? hunter.stats.highestRoguelikeRound : selectedStat === "rogue-trials-endless" ? hunter.stats.highestRogueTrialsEndlessKills - : hunter.stats.bossKills[selectedStat] ?? 0; + : selectedStat === "hockey-healing" + ? hunter.stats.highestHockeyHealingReturns + : selectedStat === "hockey-pvp-wins" + ? hunter.stats.hockeyHealingPvpWins + : selectedStat === "hockey-pvp-boss-kills" + ? hunter.stats.hockeyHealingPvpBossKills + : selectedStat === "blockbreaker-bricks" + ? hunter.stats.highestBlockbreakerBricks + : selectedStat === "blockbreaker-time" + ? hunter.stats.longestBlockbreakerSeconds + : selectedStat === "blockbreaker-score" + ? hunter.stats.highestBlockbreakerScore + : selectedStat === "aether-assault" + ? hunter.stats.highestAetherAssaultScore + : isBossProfileStat(selectedStat) + ? hunter.stats.bossKills[selectedStat] ?? 0 + : 0; const selectedStatLabel = selectedStat === "roguelike" ? "Roguelike rounds" : selectedStat === "rogue-trials-endless" ? "Rogue Trials endless kills" - : BOSS_DEFINITIONS[selectedStat].name; + : selectedStat === "hockey-healing" + ? "Hockey Healing returns" + : selectedStat === "hockey-pvp-wins" + ? "Healing Hockey PVP wins" + : selectedStat === "hockey-pvp-boss-kills" + ? "Healing Hockey PVP boss kills" + : selectedStat === "blockbreaker-bricks" + ? "Blockbreaker bricks broken" + : selectedStat === "blockbreaker-time" + ? "Blockbreaker survival time" + : selectedStat === "blockbreaker-score" + ? "Blockbreaker overall score" + : selectedStat === "aether-assault" + ? "Aether Assault score" + : isBossProfileStat(selectedStat) + ? BOSS_DEFINITIONS[selectedStat].name + : "Hunter record"; + const leaderboardValue = (value: number, secondaryValue?: number) => selectedStat === "hockey-healing" + ? `${value} · ${Math.floor((secondaryValue ?? 0) / 60)}:${String(Math.floor((secondaryValue ?? 0) % 60)).padStart(2, "0")}` + : selectedStat === "hockey-pvp-wins" + ? `${value}W · ${secondaryValue ?? 0}L` + : selectedStat === "blockbreaker-time" + ? `${Math.floor(value / 60)}:${String(Math.floor(value % 60)).padStart(2, "0")}` + : selectedStat === "aether-assault" + ? `${value.toLocaleString()} · Wave ${secondaryValue ?? 0}` + : value.toLocaleString(); + const selectedStatSummary = selectedStat === "hockey-healing" + ? `${selectedStatValue} returns · ${hockeyDuration}` + : selectedStat === "hockey-pvp-wins" + ? `${hunter.stats.hockeyHealingPvpWins}W · ${hunter.stats.hockeyHealingPvpLosses}L` + : selectedStat === "blockbreaker-time" + ? `${Math.floor(selectedStatValue / 60)}:${String(Math.floor(selectedStatValue % 60)).padStart(2, "0")} survived` + : selectedStat === "blockbreaker-score" + ? `${selectedStatValue.toLocaleString()} points` + : selectedStat === "aether-assault" + ? `${selectedStatValue.toLocaleString()} points · Wave ${hunter.stats.highestAetherAssaultWaveAtBest}` + : selectedStat === "blockbreaker-bricks" + ? `${selectedStatValue.toLocaleString()} bricks` + : `${selectedStatValue.toLocaleString()} ${selectedStat === "roguelike" ? "round" : "kills"}`; + const selectedSecondaryValue = selectedStat === "hockey-healing" + ? hunter.stats.longestHockeyHealingSecondsAtBest + : selectedStat === "hockey-pvp-wins" + ? hunter.stats.hockeyHealingPvpLosses + : selectedStat === "aether-assault" + ? hunter.stats.highestAetherAssaultWaveAtBest + : undefined; + const leaderboardMeta = leaderboardStatus + || (leaderboardUpdatedAt ? `Cached ${formatSaveTimestamp(leaderboardUpdatedAt)}` : "Offline cache empty"); return ( -
    Hunter profile

    Collection log

    - setCollectionView("trophies")}>Trophy Case - setCollectionView("stats")}>Boss Stats - setCollectionView("loot")}>Group Loot -
    navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back
    - {collectionView === "loot" ? <> +
    Hunter profile

    Records

    + setCollectionView("stats")}>Records + setCollectionView("loot")}>Group Loot +
    navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back
    + {profileView === "loot" ? <>
    Shared group drops · Core: {collection.coreMechanic}

    Group {collection.groupLetter} · {collection.groupName}

    {earned} / {collection.drops.length} discovered
    {collection.drops.map((drop) => ( @@ -654,56 +895,77 @@ function ProfileScreen() { ))}
    -
    Boss pets stay individual.Open Trophy Case to inspect every guardian pet.
    - : collectionView === "trophies" ? <> -
    Boss pets · 1 in 500 per victory

    Group {collection.groupLetter} · {collection.groupName}

    {trophiesEarned} / {collection.bosses.length} trophies lit
    -
    - {collection.bosses.map((boss) => { - const owned = boss.pet.count > 0; - return
    - {BOSS_DEFINITIONS[boss.bossId].icon}
    }> -
    {owned ? "Pet secured" : "Pet undiscovered"}{boss.bossName}{boss.kills} kills · {boss.pet.chance}
    - {owned ? `Owned${boss.pet.count > 1 ? ` ×${boss.pet.count}` : ""}` : "Locked"} - ; - })} -
    -
    Each guardian keeps its own trophy.Defeat that boss for a 1 in 500 pet roll.
    +
    Boss pets live in the Bosses record grid.Every boss, kill count, and pet status stays together.
    : <> -
    Lifetime records · Overall leaderboards

    Boss Stats

    Endless best {hunter.stats.highestRogueTrialsEndlessKills} kills
    -
    -
    - setSelectedStat("roguelike")}>RoguelikeHighest round before defeat{hunter.stats.highestRoguelikeRound} - setSelectedStat("rogue-trials-endless")}>Trials EndlessMost bosses in one run{hunter.stats.highestRogueTrialsEndlessKills} - {collection.bosses.map((boss) => setSelectedStat(boss.bossId)}>{BOSS_DEFINITIONS[boss.bossId].icon}{boss.bossName}Lifetime boss kills{boss.kills})} -
    -
    -
    Overall Top 5{selectedStatLabel}{selectedStatValue} {selectedStat === "roguelike" ? "round" : "kills"}
    - {leaderboardStatus ?
    {leaderboardStatus}
    :
    - {leaderboard?.top.length ? leaderboard.top.map((entry) =>
    #{entry.rank}{entry.hunterName}{entry.username}{entry.value}
    ) :
    No ranked hunters yet.
    } -
    } -
    {leaderboard?.current ? `#${leaderboard.current.rank}` : "—"}Your rank · {hunter.hunterName}{accountId ?? "Offline hunter"}{selectedStatValue}
    +
    Lifetime records · Controller-first collection log

    Hunter Records

    {petsOwned} / {bosses.length} boss pets found
    +
    +
    + {PROFILE_SECTIONS.map((section) => setSelectedStat(defaultStatForSection(section.id, bosses))}>{section.icon}{section.label}{section.copy})}
    + {activeSection === "bosses" ?
    +
    Boss pets · A–ZEvery guardian{hunter.stats.totalBossKills.toLocaleString()} total kills
    +
    + {bosses.map((boss) => { + const owned = boss.pet.count > 0; + return setSelectedStat(boss.bossId)}> + + Kills{boss.kills.toLocaleString()} + {boss.bossName} + {owned ? `Pet owned${boss.pet.count > 1 ? ` ×${boss.pet.count}` : ""}` : "Pet not found"} + ; + })} +
    +
    :
    +
    All {activeSectionDefinition.label} stats{activeSectionDefinition.copy}{metrics.length} record{metrics.length === 1 ? "" : "s"}
    +
    + {metrics.map((metric) => setSelectedStat(metric.id)}>{metric.label}{metric.value}{metric.copy})} +
    +
    +
    {leaderboardMeta}{selectedStatLabel}
    {selectedStatSummary} { void refreshLeaderboard(); }}>{refreshingLeaderboard ? "Refreshing…" : "Refresh online"}
    + {leaderboard ?
    + {leaderboard.top.length ? leaderboard.top.slice(0, 4).map((entry) =>
    #{entry.rank}{entry.hunterName}{entry.username}{leaderboardValue(entry.value, entry.secondaryValue)}
    ) :
    No ranked hunters yet.
    } +
    :
    {leaderboardStatus || "No cached rankings."}
    } +
    {leaderboard?.current ? `#${leaderboard.current.rank}` : "—"}Your rank · {hunter.hunterName}{leaderboardOwner ?? "Offline hunter"}{leaderboardValue(selectedStatValue, selectedSecondaryValue)}
    +
    +
    }
    -
    Rankings update with server saves.Top five always shown; your row stays visible at any rank.
    +
    {activeSection === "bosses" ? "♛" : "◆"}{activeSection === "bosses" ? "Move through the grid to inspect a boss below." : "Every stat in this section stays visible together."}{activeSection === "bosses" ? "The lower screen shows kills, pet ownership, and collection details." : "Select a record card to change the online leaderboard."}
    } } bottom={ - -
    {hunter.hunterName} · {activeHealer.name} statsLEVEL {activeProgress.level}
    -
    - Total boss kills{hunter.stats.totalBossKills} - Flawless clears{hunter.stats.flawlessClears} - Allies saved{hunter.stats.alliesSaved} - Healing done{hunter.stats.healingDone.toLocaleString()} - Highest roguelike round{hunter.stats.highestRoguelikeRound} - Endless best{hunter.stats.highestRogueTrialsEndlessKills} -
    -
    Mechanic groups{collections.map((group) => ( - setGroupId(group.groupId)}> - {group.defeated ? group.groupLetter : "?"}Group {group.groupLetter} · {group.groupName}{group.bosses.reduce((sum, boss) => sum + boss.kills, 0)} kills · {group.coreMechanic}{group.drops.filter((drop) => drop.count > 0).length}/{group.drops.length} - - ))}
    + + {profileView === "stats" && activeSection === "bosses" && selectedBoss ? <> +
    Boss record · {selectedBoss.bossName}{selectedBoss.pet.count > 0 ? "PET OWNED" : "PET NOT FOUND"}
    +
    +
    0 ? "is-owned" : "is-unowned"}`}>{selectedBoss.pet.icon}Boss pet icon
    +
    {BOSS_DEFINITIONS[selectedBoss.bossId].title}

    {selectedBoss.bossName}

    {BOSS_DEFINITIONS[selectedBoss.bossId].summary}

    +
    +
    + Times killed{selectedBoss.kills.toLocaleString()} + Boss pet{selectedBoss.pet.count > 0 ? "Owned" : "Not found"} + Pet copies{selectedBoss.pet.count.toLocaleString()} + Drop chance{selectedBoss.pet.chance} +
    +
    Encounter read{BOSS_DEFINITIONS[selectedBoss.bossId].briefing}Move on upper-screen grid to inspect another boss.
    + : <> +
    {hunter.hunterName} · {activeHealer.name} statsLEVEL {activeProgress.level}
    +
    + Total boss kills{hunter.stats.totalBossKills} + Flawless clears{hunter.stats.flawlessClears} + Allies saved{hunter.stats.alliesSaved} + Healing done{hunter.stats.healingDone.toLocaleString()} + Highest roguelike round{hunter.stats.highestRoguelikeRound} + Endless best{hunter.stats.highestRogueTrialsEndlessKills} + Hockey PVP record{hunter.stats.hockeyHealingPvpWins}W · {hunter.stats.hockeyHealingPvpLosses}L + Boss pets found{petsOwned}/{bosses.length} +
    + {profileView === "loot" &&
    Mechanic groups{collections.map((group) => ( + setGroupId(group.groupId)}> + {group.defeated ? group.groupLetter : "?"}Group {group.groupLetter} · {group.groupName}{group.bosses.reduce((sum, boss) => sum + boss.kills, 0)} kills · {group.coreMechanic}{group.drops.filter((drop) => drop.count > 0).length}/{group.drops.length} + + ))}
    } + }
    } /> @@ -755,9 +1017,9 @@ function GearScreen() { const infusionEquipped = hunter?.gearProgress[selectedOwnerId].infusionAbilityId === selectedInfusion.id; const canInstallInfusion = Boolean(hunter && activeUnlocked && anchorUnlocked && !infusionEquipped && canAffordGearUpgrade(hunter.materials, selectedInfusionCosts)); const passiveUnlocked = Boolean(hunter && passiveInfusionUnlocked(hunter.gearProgress)); - const healerOwner = selectedOwnerId === "priest" || selectedOwnerId === "druid" || selectedOwnerId === "shaman"; - const passiveHealerClassId = selectedOwnerId === "priest" || selectedOwnerId === "druid" || selectedOwnerId === "shaman" ? selectedOwnerId : "priest"; - const passiveChoices = PASSIVE_INFUSIONS.filter((passive) => passive.abilityId === selectedPassiveAbilityId); + const healerOwner = isHealerClassId(selectedOwnerId); + const passiveHealerClassId = isHealerClassId(selectedOwnerId) ? selectedOwnerId : "priest"; + const passiveChoices = PASSIVE_INFUSIONS.filter((passive) => passive.abilitySlotId === selectedPassiveAbilityId); const selectedPassive = RUN_BUFFS[selectedPassiveInfusionId]; const healerAbilities = HEALER_CLASSES[passiveHealerClassId].abilities; const previewEntryId = workshopMode === "upgrade" ? "upgrade" : `infusion-${infusionChoices[0].id}`; @@ -823,7 +1085,7 @@ function GearScreen() { { id: "back", run: () => navigate("home"), neighbors: { left: "workshop-infusion", down: `owner-${GEAR_OWNER_ORDER[0]}` } }, ], [canInstallInfusion, canUpgrade, healerOwner, infusionChoices, installInfusion, installPassive, navigate, passiveChoices, passiveUnlocked, previewEntryId, selectInfusion, selectOwner, selectPassiveAbility, selectPassiveInfusion, selectSlot, selectWorkshopMode, selectedOwnerId, selectedPassiveAbilityId, selectedSlotId, upgrade]); const controller = useMenuController(actions, { onBack: () => navigate("home") }); - const passiveContext = workshopMode === "infusion" && healerOwner && controller.focusedId.startsWith("passive-"); + const passiveContext = workshopMode === "infusion" && healerOwner && controller.selectedId.startsWith("passive-"); if (!hunter || !slot) return null; const currentBonus = gearBonusText(recipe.statId, slot.level); const nextBonus = gearBonusText(recipe.statId, Math.min(MAX_GEAR_LEVEL, slot.level + 1)); @@ -832,13 +1094,13 @@ function GearScreen() { -
    Group drop workshop

    Gear & Infusions

    selectWorkshopMode("upgrade")}>Upgrade selectWorkshopMode("infusion")}>Infusion
    navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back
    +
    Group drop workshop

    Gear & Infusions

    selectWorkshopMode("upgrade")}>Upgrade selectWorkshopMode("infusion")}>Infusion
    navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back
    {GEAR_OWNER_ORDER.map((ownerId) => { const highest = Math.max(...GEAR_SLOT_ORDER.map((slotId) => hunter.gearProgress[ownerId].slots[slotId].level)); const upgradeReady = upgradeReadiness.owners.has(ownerId); - return selectOwner(ownerId)}>{GEAR_OWNER_LABELS[ownerId]}Highest slot +{highest}{ownerId === selectedOwnerId ? "✓" : ""}; + return selectOwner(ownerId)}>{GEAR_OWNER_LABELS[ownerId]}Highest slot +{highest}{ownerId === selectedOwnerId ? "✓" : ""}; })}
    @@ -846,7 +1108,7 @@ function GearScreen() { const progress = hunter.gearProgress[selectedOwnerId].slots[slotId]; const slotRecipe = GEAR_RECIPES[selectedOwnerId][slotId]; const upgradeReady = upgradeReadiness.slots.has(`${selectedOwnerId}:${slotId}`); - return selectSlot(slotId)}>{slotId === "weapon" ? "⚔" : slotId === "helmet" ? "♙" : slotId === "chest" ? "▧" : slotId === "legs" ? "Ⅱ" : "⌁"}{GEAR_SLOT_LABELS[slotId]}{GEAR_STAT_LABELS[slotRecipe.statId]}+{progress.level}; + return selectSlot(slotId)}>{slotId === "weapon" ? "⚔" : slotId === "helmet" ? "♙" : slotId === "chest" ? "▧" : slotId === "legs" ? "Ⅱ" : "⌁"}{GEAR_SLOT_LABELS[slotId]}{GEAR_STAT_LABELS[slotRecipe.statId]}+{progress.level}; })}
    {workshopMode === "upgrade" ?
    @@ -859,25 +1121,24 @@ function GearScreen() {

    {selectedInfusion.icon} {selectedInfusion.name}

    {selectedInfusion.description} Anchor purchase to a +{ACTIVE_INFUSION_MIN_GEAR_LEVEL} slot.

    - {infusionChoices.map((infusion) => selectInfusion(infusion.id)}>{infusion.icon}{infusion.name}{infusion.description}{hunter.gearProgress[selectedOwnerId].infusionAbilityId === infusion.id ? "✓" : ""})} + {infusionChoices.map((infusion) => selectInfusion(infusion.id)}>{infusion.icon}{infusion.name}{infusion.description}{hunter.gearProgress[selectedOwnerId].infusionAbilityId === infusion.id ? "✓" : ""})}
    {healerOwner &&
    Passive blessing · global +{PASSIVE_INFUSION_MIN_GEAR_LEVEL}
    - {ABILITY_ORDER.map((abilityId) => selectPassiveAbility(abilityId)}>{healerAbilities[abilityId].shortName})} + {ABILITY_ORDER.map((abilityId) => selectPassiveAbility(abilityId)}>{healerAbilities[abilityId].shortName})}
    - {passiveChoices.map((passive) => selectPassiveInfusion(passive.id)} onPointerEnter={() => selectPassiveInfusion(passive.id)} onClick={() => { selectPassiveInfusion(passive.id); installPassive(passive.id); }} - >{passive.icon}{healerAbilities[passive.abilityId].shortName}: {passive.name}{formatRunBuffEffect(passive.id, 1)}{hunter.gearProgress[selectedOwnerId].passiveInfusionId === passive.id ? "✓" : ""})} + >{passive.icon}{healerAbilities[passive.abilitySlotId].shortName}: {passive.name}{formatRunBuffEffect(passive.id, 1, healerAbilities[passive.abilitySlotId].shortName)}{hunter.gearProgress[selectedOwnerId].passiveInfusionId === passive.id ? "✓" : ""})}
    }
    } @@ -887,15 +1148,15 @@ function GearScreen() { } bottom={ -
    {workshopMode === "upgrade" ? `${GEAR_SLOT_LABELS[selectedSlotId]} recipe` : passiveContext ? `${healerAbilities[selectedPassive.abilityId].name}: ${selectedPassive.name}` : `${selectedInfusion.name} infusion`}{hunter.materials.reduce((sum, item) => sum + item.quantity, 0)} DROPS
    +
    {workshopMode === "upgrade" ? `${GEAR_SLOT_LABELS[selectedSlotId]} recipe` : passiveContext ? `${healerAbilities[selectedPassive.abilitySlotId].name}: ${selectedPassive.name}` : `${selectedInfusion.name} infusion`}{hunter.materials.reduce((sum, item) => sum + item.quantity, 0)} DROPS
    {workshopMode === "upgrade" ? "Upgrade requirements" : passiveContext ? "Passive blessing · Rank 1" : `Infusion requirements · ${GEAR_SLOT_LABELS[selectedSlotId]} +${slot.level} anchor`} - {passiveContext ?
    {passiveUnlocked ? "✓" : "×"}{formatRunBuffEffect(selectedPassive.id, 1)}{selectedPassive.detail}{hunter.gearProgress[selectedOwnerId].passiveInfusionId === selectedPassive.id ? "EQUIPPED" : "RANK 1"}
    : (workshopMode === "upgrade" ? costs : selectedInfusionCosts).length ? (workshopMode === "upgrade" ? costs : selectedInfusionCosts).map((cost) => { + {passiveContext ?
    {passiveUnlocked ? "✓" : "×"}{formatRunBuffEffect(selectedPassive.id, 1, healerAbilities[selectedPassive.abilitySlotId].shortName)}{selectedPassive.detail}{hunter.gearProgress[selectedOwnerId].passiveInfusionId === selectedPassive.id ? "EQUIPPED" : "RANK 1"}
    : (workshopMode === "upgrade" ? costs : selectedInfusionCosts).length ? (workshopMode === "upgrade" ? costs : selectedInfusionCosts).map((cost) => { const owned = hunter.materials.find((item) => item.id === cost.itemId)?.quantity ?? 0; return
    = cost.quantity ? "is-met" : "is-missing"} key={cost.itemId}>{owned >= cost.quantity ? "✓" : "×"}{cost.itemName}{owned} owned · {cost.quantity} needed{owned}/{cost.quantity}
    ; }) :
    Maximum rank reachedNo more materials required.+{MAX_GEAR_LEVEL}
    }
    - {workshopMode === "upgrade" ? {slot.level >= MAX_GEAR_LEVEL ? "Maximum rank" : `Upgrade to +${slot.level + 1}`}{canUpgrade ? "Spend group drops · autosave" : "Collect required group drops"} : passiveContext ?
    {hunter.gearProgress[selectedOwnerId].passiveInfusionId === selectedPassive.id ? "Passive equipped" : `${DEFAULT_CONTROLLER_GLYPHS.confirm} · Equip selected passive`}Applies at rank 1 next encounter.
    : {infusionEquipped ? "Infusion equipped" : `Install ${selectedInfusion.name}`}{!activeUnlocked ? `Raise any ${GEAR_OWNER_LABELS[selectedOwnerId]} slot to +${ACTIVE_INFUSION_MIN_GEAR_LEVEL}` : !anchorUnlocked ? `Select a +${ACTIVE_INFUSION_MIN_GEAR_LEVEL} anchor slot` : canInstallInfusion ? "Spend group drops · autosave" : infusionEquipped ? "Applies next encounter" : "Collect required group drops"}} + {workshopMode === "upgrade" ? {slot.level >= MAX_GEAR_LEVEL ? "Maximum rank" : `Upgrade to +${slot.level + 1}`}{canUpgrade ? "Spend group drops · autosave" : "Collect required group drops"} : passiveContext ?
    {hunter.gearProgress[selectedOwnerId].passiveInfusionId === selectedPassive.id ? "Passive equipped" : `${DEFAULT_CONTROLLER_GLYPHS.confirm} · Equip selected passive`}Applies at rank 1 next encounter.
    : {infusionEquipped ? "Infusion equipped" : `Install ${selectedInfusion.name}`}{!activeUnlocked ? `Raise any ${GEAR_OWNER_LABELS[selectedOwnerId]} slot to +${ACTIVE_INFUSION_MIN_GEAR_LEVEL}` : !anchorUnlocked ? `Select a +${ACTIVE_INFUSION_MIN_GEAR_LEVEL} anchor slot` : canInstallInfusion ? "Spend group drops · autosave" : infusionEquipped ? "Applies next encounter" : "Collect required group drops"}}
    {notice || "Gear changes save locally and apply when next encounter starts."}
    } @@ -903,8 +1164,186 @@ function GearScreen() { ); } -function SettingToggle({ id, label, copy, value, focusedId, focus, onClick }: { id: string; label: string; copy: string; value: boolean; focusedId: string; focus: (id: string) => void; onClick: () => void }) { - return {label}{copy}{value ? "ON" : "OFF"}; +const APPEARANCE_PREVIEW_ANIMATIONS = [ + { id: "idle", label: "Idle" }, + { id: "walk", label: "Walk" }, + { id: "cast", label: "Cast" }, +] as const; + +function appearanceControlId(slotId: AppearanceSlotId, direction: "previous" | "next") { + return `appearance-${slotId}-${direction}`; +} + +function AppearanceScreen() { + const hunter = useActiveHunter(); + const classId = useFrontendStore((state) => state.appearanceClassId); + const drafts = useFrontendStore((state) => state.appearanceDrafts); + const previewMode = useFrontendStore((state) => state.previewMode); + const previewAnimation = useFrontendStore((state) => state.previewAnimation); + const notice = useFrontendStore((state) => state.notice); + const selectAppearanceClass = useFrontendStore((state) => state.selectAppearanceClass); + const updateAppearanceDraft = useFrontendStore((state) => state.updateAppearanceDraft); + const resetAppearanceDraft = useFrontendStore((state) => state.resetAppearanceDraft); + const saveAppearanceDraft = useFrontendStore((state) => state.saveAppearanceDraft); + const closeAppearanceLab = useFrontendStore((state) => state.closeAppearanceLab); + const setAppearancePreviewMode = useFrontendStore((state) => state.setAppearancePreviewMode); + const setAppearancePreviewAnimation = useFrontendStore((state) => state.setAppearancePreviewAnimation); + const draft = drafts[classId] ?? createDefaultHealerAppearance(classId); + const saved = hunter?.healers[classId].appearance ?? createDefaultHealerAppearance(classId); + const dirtyClassIds = HEALER_CLASS_ORDER.filter((candidate) => !appearancesMatch( + drafts[candidate] ?? createDefaultHealerAppearance(candidate), + hunter?.healers[candidate].appearance ?? createDefaultHealerAppearance(candidate), + )); + const currentDirty = !appearancesMatch(draft, saved); + const dirtyCount = dirtyClassIds.length; + const [discardArmed, setDiscardArmed] = useState(false); + const effectivePreviewMode = CHARACTER_MODEL_MODE === "legacy" ? "legacy" : previewMode; + const changeSlot = useCallback((slotId: AppearanceSlotId, direction: -1 | 1) => { + updateAppearanceDraft(cycleAppearanceSlot(draft, slotId, direction)); + }, [draft, updateAppearanceDraft]); + const requestClose = useCallback(() => { + if (dirtyCount > 0 && !discardArmed) { + setDiscardArmed(true); + return; + } + closeAppearanceLab(); + }, [closeAppearanceLab, dirtyCount, discardArmed]); + useEffect(() => setDiscardArmed(false), [classId, drafts, previewAnimation, previewMode]); + const actions = useMemo(() => { + const classActions = HEALER_CLASS_ORDER.map((candidate, index) => ({ + id: `appearance-class-${candidate}`, + run: () => selectAppearanceClass(candidate), + neighbors: { + left: `appearance-class-${HEALER_CLASS_ORDER[(index - 1 + HEALER_CLASS_ORDER.length) % HEALER_CLASS_ORDER.length]}`, + right: `appearance-class-${HEALER_CLASS_ORDER[(index + 1) % HEALER_CLASS_ORDER.length]}`, + down: "appearance-animation-idle", + }, + })); + const animationActions = APPEARANCE_PREVIEW_ANIMATIONS.map((animation, index) => ({ + id: `appearance-animation-${animation.id}`, + run: () => setAppearancePreviewAnimation(animation.id), + neighbors: { + left: `appearance-animation-${APPEARANCE_PREVIEW_ANIMATIONS[(index - 1 + APPEARANCE_PREVIEW_ANIMATIONS.length) % APPEARANCE_PREVIEW_ANIMATIONS.length].id}`, + right: `appearance-animation-${APPEARANCE_PREVIEW_ANIMATIONS[(index + 1) % APPEARANCE_PREVIEW_ANIMATIONS.length].id}`, + up: `appearance-class-${classId}`, + down: appearanceControlId(APPEARANCE_SLOT_DEFINITIONS[0].id, index === 0 ? "previous" : "next"), + }, + })); + const slotActions = APPEARANCE_SLOT_DEFINITIONS.flatMap((slot, index) => { + const previousRow = index === 0 ? "appearance-animation-idle" : appearanceControlId(APPEARANCE_SLOT_DEFINITIONS[index - 1].id, "previous"); + const nextRow = index === APPEARANCE_SLOT_DEFINITIONS.length - 1 ? "appearance-compare" : appearanceControlId(APPEARANCE_SLOT_DEFINITIONS[index + 1].id, "previous"); + const previous = appearanceControlId(slot.id, "previous"); + const next = appearanceControlId(slot.id, "next"); + const enabled = appearanceSlotEnabled(draft, slot.id); + return [ + { id: previous, run: () => changeSlot(slot.id, -1), enabled, neighbors: { left: next, right: next, up: previousRow, down: nextRow } }, + { id: next, run: () => changeSlot(slot.id, 1), enabled, neighbors: { left: previous, right: previous, up: index === 0 ? "appearance-animation-cast" : appearanceControlId(APPEARANCE_SLOT_DEFINITIONS[index - 1].id, "next"), down: index === APPEARANCE_SLOT_DEFINITIONS.length - 1 ? "appearance-save" : appearanceControlId(APPEARANCE_SLOT_DEFINITIONS[index + 1].id, "next") } }, + ]; + }); + return [ + ...classActions, + ...animationActions, + ...slotActions, + { id: "appearance-compare", run: () => setAppearancePreviewMode(previewMode === "modular" ? "legacy" : "modular"), enabled: CHARACTER_MODEL_MODE !== "legacy", neighbors: { left: "appearance-close", right: "appearance-reset", up: appearanceControlId(APPEARANCE_SLOT_DEFINITIONS.at(-1)!.id, "previous") } }, + { id: "appearance-reset", run: resetAppearanceDraft, neighbors: { left: "appearance-compare", right: "appearance-save", up: appearanceControlId(APPEARANCE_SLOT_DEFINITIONS.at(-1)!.id, "previous") } }, + { id: "appearance-save", run: () => { saveAppearanceDraft(); }, neighbors: { left: "appearance-reset", right: "appearance-close", up: appearanceControlId(APPEARANCE_SLOT_DEFINITIONS.at(-1)!.id, "next") } }, + { id: "appearance-close", run: requestClose, neighbors: { left: "appearance-save", right: "appearance-compare", up: appearanceControlId(APPEARANCE_SLOT_DEFINITIONS.at(-1)!.id, "next") } }, + ]; + }, [changeSlot, classId, draft, previewMode, requestClose, resetAppearanceDraft, saveAppearanceDraft, selectAppearanceClass, setAppearancePreviewAnimation, setAppearancePreviewMode]); + const controller = useMenuController(actions, { initialId: `appearance-class-${classId}`, onBack: requestClose }); + if (!hunter) return null; + const healer = HEALER_CLASSES[classId]; + const legacyActive = effectivePreviewMode === "legacy"; + + return ( + +
    + +
    Character workshop

    Appearance Lab

    + {DEFAULT_CONTROLLER_GLYPHS.back} · {discardArmed ? "Confirm discard" : dirtyCount > 0 ? `Cancel (${dirtyCount} unsaved)` : "Close"} +
    +
    + Assembling shared rig
    }> + + +
    + {legacyActive ? "LEGACY WHOLE MODEL" : "MODULAR LIVE PREVIEW"} + {legacyActive ? "Saved parts preserved but intentionally ignored" : `${previewAnimation} animation · shared Rig_Medium`} +
    +
    + {healer.specialization} + {healer.name} + {currentDirty ? "UNSAVED CHANGES" : "SAVED LOOK"} +
    +
    +
    + {DEFAULT_CONTROLLER_GLYPHS.select} / TAB Open lower-screen controls + Every choice previews on same animation skeleton +
    +
    + } + bottom={ + +
    Appearance controls{dirtyCount > 0 ? `${dirtyCount} UNSAVED` : "SAVED LOCALLY"}
    +
    + {HEALER_CLASS_ORDER.map((candidate) => { + const candidateHealer = HEALER_CLASSES[candidate]; + return selectAppearanceClass(candidate)} + >{candidateHealer.icon}{candidateHealer.name}; + })} +
    +
    + {APPEARANCE_PREVIEW_ANIMATIONS.map((animation) => setAppearancePreviewAnimation(animation.id)} + >{animation.label})} +
    +
    + {APPEARANCE_SLOT_DEFINITIONS.map((slot) => { + const active = controller.selectedId.startsWith(`appearance-${slot.id}-`); + const enabled = appearanceSlotEnabled(draft, slot.id); + return
    + {slot.label}{slot.assetNote} + changeSlot(slot.id, -1)}>‹ + {appearanceSlotLabel(draft, slot.id)} + changeSlot(slot.id, 1)}>› +
    ; + })} +
    +
    + setAppearancePreviewMode(previewMode === "modular" ? "legacy" : "modular")}>{legacyActive ? "Show custom" : "Compare legacy"}{CHARACTER_MODEL_MODE === "legacy" ? "Rollout locked" : "No save change"} + ResetClass default + { saveAppearanceDraft(); }}>Save lookCurrent healer + {discardArmed ? "Confirm" : "Cancel"}{discardArmed ? "Press again" : dirtyCount > 0 ? `Discard ${dirtyCount}` : "Close lab"} +
    +
    0 ? "is-dirty" : ""}`}>{discardArmed ? `Discard ${dirtyCount} unsaved healer ${dirtyCount === 1 ? "look" : "looks"}? Press Cancel or Back again.` : CHARACTER_MODEL_MODE === "legacy" ? "Legacy rollout active. Modular choices remain saved for later." : currentDirty ? "Preview changed. Save look to use it in gameplay." : dirtyCount > 0 ? `${dirtyCount} other healer ${dirtyCount === 1 ? "look is" : "looks are"} still unsaved. Switch classes to save them.` : notice || "Saved look will load in the next encounter."}
    +
    + } + /> + ); +} + +function SettingToggle({ id, label, copy, value, selectedId, select, onClick }: { id: string; label: string; copy: string; value: boolean; selectedId: string; select: (id: string) => void; onClick: () => void }) { + return {label}{copy}{value ? "ON" : "OFF"}; } function SettingsScreen() { @@ -926,10 +1365,10 @@ function SettingsScreen() { -
    Field configuration

    Settings

    navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back
    +
    Field configuration

    Settings

    navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back
    -
    Audio
    Master volumeAll music, effects, and voice
    updateSetting("masterVolume", Math.max(0, settings.masterVolume - 10))}>−{settings.masterVolume}% updateSetting("masterVolume", Math.min(100, settings.masterVolume + 10))}>+
    -
    Display & accessibility updateSetting("reducedMotion", !settings.reducedMotion)} /> updateSetting("damageNumbers", !settings.damageNumbers)} /> updateSetting("largeText", !settings.largeText)} />
    +
    Audio
    Master volumeAll music, effects, and voice
    updateSetting("masterVolume", Math.max(0, settings.masterVolume - 10))}>−{settings.masterVolume}% updateSetting("masterVolume", Math.min(100, settings.masterVolume + 10))}>+
    +
    Display & accessibility updateSetting("reducedMotion", !settings.reducedMotion)} /> updateSetting("damageNumbers", !settings.damageNumbers)} /> updateSetting("largeText", !settings.largeText)} />
    {notice || "Settings write to offline storage immediately."}
    @@ -942,15 +1381,16 @@ function SettingsScreen() {
    {DEFAULT_CONTROLLER_GLYPHS.faceTop}{DEFAULT_CONTROLLER_GLYPHS.faceLeft}{DEFAULT_CONTROLLER_GLYPHS.faceRight}{DEFAULT_CONTROLLER_GLYPHS.faceBottom}
    {DEFAULT_CONTROLLER_GLYPHS.confirm} Confirm / cast Purify{DEFAULT_CONTROLLER_GLYPHS.back} Back / cast ShieldD-Pad Navigate / target partyRight stick Rotate camera{DEFAULT_CONTROLLER_GLYPHS.start} Pause / menu
    -
    No click-to-focus requiredController input routes through app-level actions.
    +
    No app focus requiredNative controller input routes through app-level actions.
    } /> ); } -function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], difficultySlug?: (typeof DIFFICULTIES)[number]["slug"]) => void }) { +function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], difficultySlug?: (typeof DIFFICULTIES)[number]["slug"], hockeyPvpMatch?: HockeyPvpMatchConfig) => void }) { const hunter = useActiveHunter(); + const accountId = useFrontendStore((state) => state.accountId); const modeId = useFrontendStore((state) => state.selectedMode); const selectedBossId = useFrontendStore((state) => state.selectedBossId); const selectedDifficultySlug = useFrontendStore((state) => state.selectedDifficultySlug); @@ -958,6 +1398,13 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi const selectDifficulty = useFrontendStore((state) => state.selectDifficulty); const navigate = useFrontendStore((state) => state.navigate); const [message, setMessage] = useState(""); + const [queueing, setQueueing] = useState(false); + const [queueElapsed, setQueueElapsed] = useState(0); + const queueActive = useRef(false); + const queueTicket = useRef(null); + const queuePollTimer = useRef(null); + const queueCpuTimer = useRef(null); + const queueClockTimer = useRef(null); const mode = MODE_COPY[modeId]; const healer = hunter ? HEALER_CLASSES[hunter.activeClassId] : HEALER_CLASSES.priest; const progress = hunter?.healers[hunter.activeClassId]; @@ -967,14 +1414,118 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi const isRogueTrials = modeId === "rogue-trials"; const isPveRun = isPve || isRogueTrials; const isDungeon = modeId === "dungeons"; + const isHockey = modeId === "hockey-healing"; + const isHockeyPvp = modeId === "hockey-healing-pvp"; + const isBlockbreaker = modeId === "blockbreaker"; + const isAetherAssault = modeId === "aether-assault"; const selectedBossGroup = BOSS_GROUP_BY_ID[selectedBoss.groupId]; const visibleBossIds = selectedBossGroup.bossIds; const bossGridColumns = Math.min(2, visibleBossIds.length); const selectBossGroup = (groupId: (typeof BOSS_GROUPS)[number]["id"]) => { selectBoss(BOSS_GROUP_BY_ID[groupId].bossIds[0]); }; + const clearQueueTimers = () => { + if (queuePollTimer.current !== null) window.clearTimeout(queuePollTimer.current); + if (queueCpuTimer.current !== null) window.clearTimeout(queueCpuTimer.current); + if (queueClockTimer.current !== null) window.clearInterval(queueClockTimer.current); + queuePollTimer.current = null; + queueCpuTimer.current = null; + queueClockTimer.current = null; + }; + const completePvpQueue = (match: HockeyPvpMatchConfig) => { + if (!queueActive.current) return; + queueActive.current = false; + clearQueueTimers(); + setQueueing(false); + setMessage(match.role === "cpu" ? `CPU rival found: ${match.opponentName}.` : `Matched with ${match.opponentName}.`); + onLaunch([hockeyPvpBossAt(match.seed, 0)], "initiate", match); + }; + const fallbackToCpu = () => { + if (!queueActive.current) return; + const ticketId = queueTicket.current; + queueTicket.current = null; + if (ticketId) void onlineRepository.cancelHockeyPvpQueue(ticketId).catch(() => undefined); + completePvpQueue({ + matchId: null, + seed: Math.max(1, Math.floor(Math.random() * 0xffffffff)), + opponentName: randomHockeyPvpCpuName(), + role: "cpu", + }); + }; + const cancelPvpQueue = () => { + if (!queueActive.current) return; + queueActive.current = false; + clearQueueTimers(); + const ticketId = queueTicket.current; + queueTicket.current = null; + if (ticketId) void onlineRepository.cancelHockeyPvpQueue(ticketId).catch(() => undefined); + setQueueing(false); + setQueueElapsed(0); + setMessage("Matchmaking cancelled."); + }; + const startPvpQueue = async () => { + if (!hunter || queueActive.current) return; + queueActive.current = true; + setQueueing(true); + setQueueElapsed(0); + setMessage(accountId ? "Searching online queue…" : "Offline queue: searching before CPU fallback…"); + const startedAt = Date.now(); + queueClockTimer.current = window.setInterval(() => setQueueElapsed(Date.now() - startedAt), 100); + queueCpuTimer.current = window.setTimeout(fallbackToCpu, HOCKEY_PVP_QUEUE_TIMEOUT_MS); + if (!accountId || !networkAppearsOnline()) return; + try { + const joined = await onlineRepository.joinHockeyPvpQueue(hunter.slotId, hunter.hunterName); + if (!queueActive.current) return; + queueTicket.current = joined.ticketId; + if (joined.match) { + completePvpQueue({ + matchId: joined.match.id, + seed: joined.match.seed, + opponentName: joined.match.opponentName, + role: joined.match.role, + }); + return; + } + const poll = async () => { + if (!queueActive.current || !queueTicket.current) return; + try { + const result = await onlineRepository.pollHockeyPvpQueue(queueTicket.current); + if (!queueActive.current) return; + if (result.match) { + completePvpQueue({ + matchId: result.match.id, + seed: result.match.seed, + opponentName: result.match.opponentName, + role: result.match.role, + }); + return; + } + } catch { + // Five-second CPU fallback remains authoritative during transient outages. + } + if (queueActive.current) queuePollTimer.current = window.setTimeout(poll, 350); + }; + queuePollTimer.current = window.setTimeout(poll, 350); + } catch { + setMessage("Online queue unavailable. CPU fallback still searching…"); + } + }; + useEffect(() => () => { + queueActive.current = false; + clearQueueTimers(); + const ticketId = queueTicket.current; + if (ticketId) void onlineRepository.cancelHockeyPvpQueue(ticketId).catch(() => undefined); + }, []); + const leaveMode = () => { + cancelPvpQueue(); + navigate("home"); + }; const launch = () => { if (isPveRun) return onLaunch(selectRandomBossPair(), "initiate"); + if (isHockey) return onLaunch(selectRandomBossPair(), "initiate"); + if (isBlockbreaker) return onLaunch(selectRandomBossPair(), "initiate"); + if (isAetherAssault) return onLaunch(selectRandomBossPair(), "initiate"); + if (isHockeyPvp) return queueing ? cancelPvpQueue() : void startPvpQueue(); if (isDungeon) return onLaunch([selectedBossId], selectedDifficultySlug); setMessage("Online matchmaking is not available for this mode yet."); }; @@ -1025,16 +1576,54 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi }, })) : []), { id: "launch", run: launch, neighbors: isDungeon ? { up: `difficulty-${DIFFICULTIES[DIFFICULTIES.length - 1].slug}` } : { up: "back" } }, - { id: "back", run: () => navigate("home"), neighbors: isDungeon ? { down: `boss-group-${selectedBossGroup.id}` } : { down: "launch" } }, - ], [bossGridColumns, isDungeon, isPveRun, modeId, navigate, onLaunch, selectBoss, selectDifficulty, selectedBossGroup, selectedBossId, selectedDifficultySlug, visibleBossIds]); - const controller = useMenuController(actions, { onBack: () => navigate("home") }); - const launchLabel = isRogueTrials ? "Begin Rogue Trials" : isPve ? "Begin randomized run" : isDungeon ? `Challenge ${selectedBoss.name}` : "Enter matchmaking"; + { id: "back", run: leaveMode, neighbors: isDungeon ? { down: `boss-group-${selectedBossGroup.id}` } : { down: "launch" } }, + ], [bossGridColumns, isAetherAssault, isBlockbreaker, isDungeon, isHockey, isHockeyPvp, isPveRun, modeId, navigate, onLaunch, queueing, selectBoss, selectDifficulty, selectedBossGroup, selectedBossId, selectedDifficultySlug, visibleBossIds]); + const controller = useMenuController(actions, { onBack: leaveMode }); + const launchLabel = isRogueTrials + ? "Begin Rogue Trials" + : isPve + ? "Begin RPG Roguelike" + : isHockey + ? "Begin Hockey Healing" + : isBlockbreaker + ? "Begin Blockbreaker" + : isAetherAssault + ? "Begin Aether Assault" + : isHockeyPvp + ? queueing ? "Cancel matchmaking" : "Enter online queue" + : isDungeon + ? `Challenge ${selectedBoss.name}` + : "Enter matchmaking"; const contextRules = isDungeon ? [ [selectedBoss.name, selectedBoss.summary], [bossMechanicName(selectedBoss.mechanicIds[0]), selectedBoss.briefing], [bossMechanicName(selectedBoss.mechanicIds[1]), "Controller-ready party behavior and full lower-display support."], ] + : isAetherAssault + ? [ + ["Movement is the only arcade input", "Spellfire launches automatically down the five runway lanes while every healing and targeting control stays unchanged."], + ["Formation pressure", "Eight arcane ships enter the first wave. Later formations grow to twenty, add armor, fire faster, and peel into diving attacks."], + ["Heal through every hit", "Ship bolts and dive collisions damage only the healer. Endless bosses keep attacking the full party until the formation falls."], + ] + : isBlockbreaker + ? [ + ["Break linked colors", "Aim the puck into five-column rows. A hit removes its full orthogonally connected color cluster."], + ["Accelerating wall", "Rows begin every 10 seconds, accelerate 10% each minute, and push survivors toward the danger line."], + ["Heal under pressure", `Two bosses attack without pause. Each brick-wall breach deals ${BLOCKBREAKER_BREACH_DAMAGE} damage to every party member. The run ends when all four allies fall.`], + ] + : isHockeyPvp + ? [ + ["Normalized rivals", "Both parties use default base gear with all upgrade and infusion bonuses disabled. Boss order remains identical."], + ["Escalating pressure", `Every boss kill by either party adds 5% global healing Dampening. Every net breach still deals ${HOCKEY_PVP_GOAL_DAMAGE} partywide damage.`], + ["Online or CPU", "Queue searches online for five seconds. If no rival answers, a randomly named CPU healer takes far goal."], + ] + : isHockey + ? [ + ["Wide goal defense", "Healer owns near half. Intercept every incoming puck before it reaches the wide blue goal."], + ["Pong rally", "Held left-stick direction controls return angle. Moving enemy paddle tracks the puck and strikes it back."], + ["Unbroken boss fight", "Party fights two bosses on enemy half. Every kill awards loot and pet chance before replacement arrives."], + ] : isRogueTrials ? [ ["Four dual rounds", "Clear four randomized pairs while drafting one stacking buff after each win."], @@ -1043,9 +1632,9 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi ] : isPve ? [ - ["Randomized pair", "Two distinct bosses are selected only when the run begins."], - ["Dual-boss pressure", "Both guardians fight simultaneously and must be defeated."], - ["Buff intermission", "Choose one of three stacking buffs after every cleared round."], + ["Draft every run", "Choose four companions from three five-card waves, then build a six-slot spellbook from every enabled healer class."], + ["Challenge hallways", "Brickbreaker, Hockey, and Aether Assault objectives connect boss rooms. Repeats raise their targets; failure still advances."], + ["Run-only growth", "Boss chests upgrade owned spells, companions, or +0–+5 gear. Shop after each three-boss act, then face a finale."], ] : [ ["Draft a healing path", "Choose rites after every completed room."], @@ -1056,24 +1645,24 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi -
    Game mode

    {mode.title}

    navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back
    +
    Game mode

    {mode.title}

    {DEFAULT_CONTROLLER_GLYPHS.back} · Back
    {!isDungeon &&
    {mode.eyebrow}

    {mode.title}

    {mode.description}

    {mode.detail}
    } {isDungeon && (
    Choose a mechanic group
    {BOSS_GROUPS.map((group) => ( - selectBossGroup(group.id)} > {group.letter}Group {group.letter}{group.name} - + ))}
    Group {selectedBossGroup.letter} · {selectedBossGroup.name}{selectedBossGroup.coreMechanic} mechanics · {visibleBossIds.length} guardians
    @@ -1081,18 +1670,18 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi {visibleBossIds.map((bossId) => { const boss = BOSS_DEFINITIONS[bossId]; return ( - selectBoss(bossId)} > {boss.icon}{boss.name}{boss.mechanicIds.filter((id) => !bossMechanicIsPassive(id)).map(bossMechanicName).join(" · ")}{selectedBossId === bossId ? "✓" : ""} - + ); })}
    @@ -1101,10 +1690,10 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi {isDungeon && (
    Difficulty - {DIFFICULTIES.map((difficulty) => selectDifficulty(difficulty.slug)}>{difficulty.name}iLvl {difficulty.itemLevel})} + {DIFFICULTIES.map((difficulty) => selectDifficulty(difficulty.slug)}>{difficulty.name}iLvl {difficulty.itemLevel})}
    )} - {launchLabel}{mode.status} · {DEFAULT_CONTROLLER_GLYPHS.confirm} + {launchLabel}{queueing ? `CPU fallback in ${Math.max(0, ((HOCKEY_PVP_QUEUE_TIMEOUT_MS - queueElapsed) / 1000)).toFixed(1)}s` : `${mode.status} · ${DEFAULT_CONTROLLER_GLYPHS.confirm}`} {message &&
    {message}
    } } @@ -1112,21 +1701,26 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi
    Run preparation{isDungeon ? selectedDifficulty.name.toUpperCase() : mode.status.toUpperCase()}
    {contextRules.map(([title, copy], index) =>
    0{index + 1}{title}{copy}
    )} -
    Equipped role{healer.specialization} · Level {progress?.level ?? 1}6 abilities · {progress?.inventory.length ?? 0} class items · Controller ready
    +
    Equipped role{healer.specialization} · Level {progress?.level ?? 1}{isHockeyPvp ? "6 abilities · Base gear normalized · Controller ready" : `6 abilities · ${progress?.inventory.length ?? 0} class items · Controller ready`}
    {isDungeon &&
    Guaranteed reward{bossGroupDrop(selectedBossId, selectedDifficultySlug).name}1–3 group drops · {selectedDifficulty.rarity} · Pet chance 1 in 500
    } + {isHockey &&
    Every boss killNormal boss loot awardedGuaranteed 1–3 group drops · Independent pet chance 1 in 500
    } + {isBlockbreaker &&
    Ranked recordsOverall score · Bricks · Survival10 points per brick ladder · +0.1× every 30 seconds
    } + {isAetherAssault &&
    Ranked recordOverall score · Wave at bestKill streak raises multiplier · Ship damage resets it
    } + {isHockeyPvp &&
    Ranked recordsWins / losses · Lifetime PVP boss killsOnline leaderboards publish through active hunter save
    }
    } /> ); } -export function FrontEnd({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], difficultySlug?: (typeof DIFFICULTIES)[number]["slug"]) => void }) { +export function FrontEnd({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], difficultySlug?: (typeof DIFFICULTIES)[number]["slug"], hockeyPvpMatch?: HockeyPvpMatchConfig) => void }) { const screen = useFrontendStore((state) => state.screen); if (screen === "login") return ; if (screen === "saves") return ; if (screen === "home") return ; if (screen === "profile") return ; if (screen === "gear") return ; + if (screen === "appearance") return ; if (screen === "settings") return ; if (screen === "mode") return ; return null; diff --git a/src/components/GameScene.tsx b/src/components/GameScene.tsx index 822c42f..1b93a7d 100644 --- a/src/components/GameScene.tsx +++ b/src/components/GameScene.tsx @@ -4,10 +4,25 @@ import { Suspense, useCallback, useEffect, useMemo, useRef, useState, type Mutab import * as THREE from "three"; import { getControllerMovement } from "../input/controller"; import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js"; -import { ARENA_CENTER, clampToArena } from "../game/arena"; -import { BOSS_ARCHETYPE_BY_ID } from "../game/bossCatalog"; -import { ALTERNATE_BOSS_CONFIG, BULL_URL, type AlternateBossKind } from "../game/bossVisuals"; +import { ARENA_CENTER, clampToArena, clampToHockeyArena, clampToHockeyHealerHalf } from "../game/arena"; +import { BOSS_ARCHETYPE_BY_ID, type BossArchetype } from "../game/bossCatalog"; +import { ALTERNATE_BOSS_CONFIG, BULL_BOSS_ANIMATION_CONFIG, BULL_URL, type AlternateBossKind } from "../game/bossVisuals"; import { bossAnimationCue } from "../game/bosses/mechanicPool"; +import { selectMeleeTargetIndex } from "../game/bosses/shared"; +import { + BOSS_HIT_ANIMATION_SECONDS, + BOSS_HIT_REACTION_COOLDOWN_SECONDS, + BOSS_MELEE_ANIMATION_SECONDS, + bossAnimationClipName, + bossAnimationTrigger, + isBossAnimationOneShot, + selectBossAnimationState, + shouldStartBossAnimation, + writeBossProceduralPose, + type BossAnimationClips, + type BossAnimationState, + type BossProceduralPose, +} from "../game/bossAnimation"; import { CAMERA_FOCUS_HEIGHT, CAMERA_LOOK_AHEAD, @@ -25,15 +40,76 @@ import { type ActorAnimationState, } from "../game/actorAnimation"; import { PERFORMANCE_PROBE_ENABLED, recordSimulationTick, simulationTickSnapshot } from "../game/performance"; -import type { PartyAbilityId } from "../game/partyCombat"; +import { + HOCKEY_ARENA_MAX_Z, + HOCKEY_NPC_PADDLE_WIDTH, + HOCKEY_NPC_PADDLE_Z, + HOCKEY_PUCK_RADIUS, + hockeyAimPreviewVisible, + hockeyReturnDirection, +} from "../game/hockeyHealing"; +import type { AiCombatantId, PartyAbilityId } from "../game/partyCombat"; import { partyAttackVfxProfile } from "../game/partyAttackVisuals"; +import { + STAFF_CAST_AFTERGLOW_SECONDS, + STAFF_CAST_GLOW_PROFILES, + isHealerPulseKind, + staffCastGlowStrength, +} from "../game/staffCastGlow"; +import { + HEALER_VISUAL_PROFILES, + type HealerVisualProfile, +} from "../game/healerVisuals"; +import { + CHARACTER_MODEL_MODE, + type CharacterAppearanceV1, + type CharacterModelMode, +} from "../game/characterAppearance"; +import { + weaponDefinition, + weaponUsesBothHands, + type CharacterWeaponGrip, + type CharacterWeaponModelId, +} from "../game/weaponCatalog"; +import { resolveCharacterEquipment } from "../game/characterEquipment"; +import { HEALER_CLASS_ORDER } from "../game/healers"; import { useGameStore } from "../game/store"; -import type { BossId, MemberId, PulseKind } from "../game/types"; +import type { BossId, HealerClassId, MemberId, PulseKind } from "../game/types"; import { BossRoom } from "./BossRoom"; +import { HealerClassAccessory } from "./HealerClassAccessory"; +import { ModularCharacterBody } from "./ModularCharacterBody"; import { BossMechanicIndicators } from "./boss/BossMechanicIndicators"; import { bossBurrowPositionY, bossIsBurrowing } from "./boss/bossBurrowVisuals"; import { bossCanTrackTarget, bossDeathOpacity } from "./boss/bossDeathVisuals"; import { GameAssetProvider, LEGACY_GAME_ASSETS_FORCED, selectedGameAssetUrl, useGameGLTF } from "./GameAssetProvider"; +import { characterEquipmentAssetUrl } from "./CharacterEquipmentAssets"; +import { + HOCKEY_PVP_PUCK_RADIUS, + HOCKEY_PVP_SIDE_OFFSET_Z, + hockeyPvpLocalToWorld, +} from "../game/hockeyHealingPvp"; +import { + BLOCKBREAKER_BRICK_DEPTH, + BLOCKBREAKER_BRICK_WIDTH, + BLOCKBREAKER_DANGER_Z, + BLOCKBREAKER_MAX_BRICKS, + BLOCKBREAKER_PUCK_RADIUS, + blockbreakerAimPreviewVisible, + blockbreakerColumnX, + blockbreakerRowZ, + type BlockbreakerBrickColor, +} from "../game/blockbreaker"; +import { blockbreakerBiomeForSeed } from "../game/blockbreakerBiomes"; +import { + AETHER_MAX_ENEMY_SHOTS, + AETHER_MAX_PLAYER_SHOTS, + AETHER_MAX_SHIPS, +} from "../game/aetherAssault"; +import { + AETHER_STANDARD_SHIP_COLORS, + aetherShipColorIndex, +} from "./aetherAssaultVisuals"; +import { clampToBossArenaWithPortals } from "../game/rpgRoguelike/playSpace"; const PARTY_MODEL_LEGACY_URLS: Record = { aelia: new URL("../assets/game/models/claudecraft/chars/players/druid.glb", import.meta.url).href, @@ -166,6 +242,7 @@ function useBossDeathFade( materials: readonly BossFadeMaterial[], defeated: boolean, baseLightIntensity: number, + bossId: BossId, ) { const elapsed = useRef(0); const lastOpacity = useRef(1); @@ -181,7 +258,7 @@ function useBossDeathFade( return; } elapsed.current += delta; - const opacity = bossDeathOpacity(elapsed.current); + const opacity = bossDeathOpacity(elapsed.current, bossId); if (opacity === lastOpacity.current) return; lastOpacity.current = opacity; if (group.current) group.current.visible = opacity > 0; @@ -190,12 +267,149 @@ function useBossDeathFade( }); } -function encounterBossAt(state: GameStoreState, bossIndex: number) { +function encounterBossAt(state: GameStoreState, bossIndex: number, opponent = false) { + if (opponent) return { boss: state.hockeyPvpOpponent.boss, motion: state.hockeyPvpOpponent.bossMotion }; return bossIndex === 0 ? { boss: state.boss, motion: state.bossMotion } : state.additionalBosses[bossIndex - 1]; } +function useBossAnimationPlayback({ + actions, + clips, + archetype, + bossIndex, + opponent, + modelRoot, +}: { + actions: Record; + clips: BossAnimationClips; + archetype: BossArchetype; + bossIndex: number; + opponent: boolean; + modelRoot: RefObject; +}) { + const activeClip = useRef(undefined); + const activeState = useRef(undefined); + const activeTrigger = useRef(Number.NaN); + const previousHp = useRef(undefined); + const observedMeleeAt = useRef(-1); + const meleeElapsed = useRef(Number.POSITIVE_INFINITY); + const hitElapsed = useRef(Number.POSITIVE_INFINITY); + const hitReadyAt = useRef(0); + const hitTrigger = useRef(0); + const observedPhaseStartedAt = useRef(Number.NaN); + const phaseElapsed = useRef(0); + const pose = useRef({ + x: 0, + y: 0, + z: 0, + pitch: 0, + yaw: 0, + roll: 0, + scaleX: 1, + scaleY: 1, + scaleZ: 1, + }); + + useFrame((_, delta) => { + const state = useGameStore.getState(); + const current = encounterBossAt(state, bossIndex, opponent); + if (!current) return; + const { boss, motion } = current; + + if (previousHp.current !== undefined + && boss.hp > 0 + && boss.hp < previousHp.current + && state.time >= hitReadyAt.current) { + hitElapsed.current = 0; + hitReadyAt.current = state.time + BOSS_HIT_REACTION_COOLDOWN_SECONDS; + hitTrigger.current += 1; + } + previousHp.current = boss.hp; + + if (motion.lastMeleeAt >= 0 && motion.lastMeleeAt !== observedMeleeAt.current) { + observedMeleeAt.current = motion.lastMeleeAt; + meleeElapsed.current = 0; + } + if (motion.phaseStartedAt !== observedPhaseStartedAt.current) { + observedPhaseStartedAt.current = motion.phaseStartedAt; + phaseElapsed.current = 0; + } + + const mechanicCue = bossAnimationCue(motion); + const animationState = selectBossAnimationState({ + defeated: boss.hp <= 0 || state.phase === "victory", + activeMechanic: motion.activeMechanicId !== null, + mechanicCue, + meleeElapsed: meleeElapsed.current, + hitElapsed: hitElapsed.current, + }); + const trigger = bossAnimationTrigger( + animationState, + motion.phaseStartedAt, + motion.lastMeleeAt, + hitTrigger.current, + ); + const clipName = bossAnimationClipName(clips, animationState); + + if (shouldStartBossAnimation(activeState.current, activeTrigger.current, animationState, trigger)) { + const next = actions[clipName]; + if (next) { + const clipChanged = activeClip.current !== clipName; + if (clipChanged && activeClip.current) actions[activeClip.current]?.fadeOut(0.16); + const timeScale = archetype === "duelist" && motion.mode === "mantis_line_telegraph" + ? 0.55 + : archetype === "duelist" && motion.mode === "mantis_cross_telegraph" + ? 0.6 + : motion.mode === "charging" + ? 1.3 + : 1; + next.reset().setEffectiveWeight(1).setEffectiveTimeScale(timeScale); + if (clipChanged) next.fadeIn(0.16); + if (isBossAnimationOneShot(animationState)) { + next.setLoop(THREE.LoopOnce, 1); + next.clampWhenFinished = true; + } else { + next.setLoop(THREE.LoopRepeat, Number.POSITIVE_INFINITY); + next.clampWhenFinished = false; + } + next.play(); + activeClip.current = clipName; + activeState.current = animationState; + activeTrigger.current = trigger; + } + } + + const animationElapsed = animationState === "melee" + ? meleeElapsed.current + : animationState === "hit" + ? hitElapsed.current + : phaseElapsed.current; + const phaseDuration = Number.isFinite(motion.phaseEndsAt) && motion.phaseEndsAt > motion.phaseStartedAt + ? motion.phaseEndsAt - motion.phaseStartedAt + : 1; + writeBossProceduralPose( + pose.current, + archetype, + motion.mode, + animationState, + animationElapsed, + phaseDuration, + ); + if (modelRoot.current) { + const nextPose = pose.current; + modelRoot.current.position.set(nextPose.x, nextPose.y, nextPose.z); + modelRoot.current.rotation.set(nextPose.pitch, nextPose.yaw, nextPose.roll); + modelRoot.current.scale.set(nextPose.scaleX, nextPose.scaleY, nextPose.scaleZ); + } + + meleeElapsed.current = Math.min(BOSS_MELEE_ANIMATION_SECONDS, meleeElapsed.current + delta); + hitElapsed.current = Math.min(BOSS_HIT_ANIMATION_SECONDS, hitElapsed.current + delta); + phaseElapsed.current += delta; + }); +} + function targetBossMotion(state: GameStoreState) { if (state.boss.hp > 0) return state.bossMotion; return state.additionalBosses.find((entry) => entry.boss.hp > 0)?.motion ?? state.bossMotion; @@ -206,21 +420,27 @@ function targetBossMotionByInstance(state: GameStoreState, instanceId?: string) return state.additionalBosses.find((entry) => entry.instanceId === instanceId)?.motion ?? targetBossMotion(state); } -type WeaponGrip = "staff" | "sword" | "crossbow" | "wand" | "dagger" | "prop"; - -const PARTY_WEAPON_GRIPS: Record = { - aelia: { right: "staff" }, - brann: { right: "sword", left: "prop" }, - nia: { right: "crossbow" }, - orin: { right: "wand", left: "prop" }, - vale: { right: "dagger", left: "dagger" }, +const PARTY_WEAPON_MODEL_IDS: Record = { + aelia: { right: "cc/adv_druid_staff" }, + brann: { right: "cc/adv_sword_1handed", left: "cc/shield_badge" }, + nia: { right: "cc/crossbow_2handed" }, + orin: { right: "cc/adv_wand", left: "cc/spellbook_open" }, + vale: { right: "cc/adv_dagger", left: "cc/adv_dagger" }, }; +const CROSSBOW_QUIVER_URL = characterEquipmentAssetUrl("cc/quiver"); -const VARIANT_GRIPS: Record, { lift: number; maxHeight: number }> = { - sword: { lift: 0.04, maxHeight: 2 }, +const VARIANT_GRIPS: Record, { lift: number; maxHeight: number }> = { + upright: { lift: 0.04, maxHeight: 2 }, dagger: { lift: 0.04, maxHeight: 1.4 }, staff: { lift: 0.18, maxHeight: 2.4 }, wand: { lift: 0.04, maxHeight: 1.2 }, + polearm: { lift: 0.12, maxHeight: 2.75 }, +}; + +const CROSSBOW_MOUNTS: Record, { x: number; y: number; scale: number }> = { + "cc/crossbow_1handed": { x: 0.255, y: 0.04, scale: 0.6109 }, + "cc/crossbow_2handed": { x: 0.3381, y: 0.058, scale: 0.7204 }, + "cc/skeleton_crossbow": { x: 0.33, y: 0.064, scale: 0.7094 }, }; function resolveRigNode(root: THREE.Object3D, authoredName: string) { @@ -228,7 +448,7 @@ function resolveRigNode(root: THREE.Object3D, authoredName: string) { ?? root.getObjectByName(authoredName.replace(/[[\].:/]/g, "")); } -function flattenWeaponScene(scene: THREE.Object3D) { +function flattenCrossbowScene(scene: THREE.Object3D) { if (scene.children.length !== 1) return scene; const holder = new THREE.Group(); const child = scene.children[0]; @@ -241,58 +461,402 @@ function flattenWeaponScene(scene: THREE.Object3D) { return holder; } -function prepareHeldWeapon(scene: THREE.Object3D, grip: WeaponGrip, side: "r" | "l") { +function prepareHeldWeapon( + scene: THREE.Object3D, + modelId: CharacterWeaponModelId, + grip: CharacterWeaponGrip, + side: "r" | "l", +) { // Shields and spellbooks carry useful authored offsets, so keep their scene transform. if (grip === "prop") return scene; - const weapon = flattenWeaponScene(scene); if (grip === "crossbow") { - weapon.position.set(0.3381, 0.058, 0); + const weapon = flattenCrossbowScene(scene); + const mount = CROSSBOW_MOUNTS[modelId as keyof typeof CROSSBOW_MOUNTS] + ?? CROSSBOW_MOUNTS["cc/crossbow_2handed"]; + weapon.position.set(mount.x, mount.y, 0); weapon.quaternion.set(0, 0.7071068, 0, 0.7071067); - weapon.scale.setScalar(0.7204); + weapon.scale.setScalar(mount.scale); return weapon; } - const { lift, maxHeight } = VARIANT_GRIPS[grip]; + const weapon = new THREE.Group(); + weapon.add(scene); + const profile = VARIANT_GRIPS[grip]; + const maxHeight = grip === "upright" && weaponUsesBothHands(modelId) + ? 2.75 + : profile.maxHeight; const bounds = new THREE.Box3().setFromObject(weapon); const height = bounds.max.y - bounds.min.y; const scale = height > 0.001 ? Math.min(1, maxHeight / height) : 1; - weapon.position.set(0, lift, 0); + weapon.position.set(0, profile.lift, 0); weapon.quaternion.set(0, side === "l" ? 0 : 1, 0, side === "l" ? 1 : 0); - weapon.scale.setScalar(scale); + weapon.scale.multiplyScalar(scale); return weapon; } +function prepareBackQuiver(scene: THREE.Object3D) { + const quiver = new THREE.Group(); + quiver.add(scene); + const bounds = new THREE.Box3().setFromObject(quiver); + const height = bounds.max.y - bounds.min.y; + quiver.scale.multiplyScalar(height > 0.001 ? 0.92 / height : 1); + quiver.position.set(0.16, 0.08, -0.24); + quiver.rotation.set(0.08, Math.PI, -0.16); + return quiver; +} + +interface WeaponAssetReference { + count: number; + scene: THREE.Object3D; + releaseTimer: ReturnType | null; +} + +const WEAPON_ASSET_REFERENCES = new Map(); +const PENDING_WEAPON_ASSET_RELEASE_MS = 5_000; +const MAX_PENDING_WEAPON_ASSETS = 16; +const PENDING_WEAPON_ASSETS = new Map>(); + +function releasePendingWeaponAsset(url: string) { + const releaseTimer = PENDING_WEAPON_ASSETS.get(url); + if (releaseTimer === undefined) return; + clearTimeout(releaseTimer); + PENDING_WEAPON_ASSETS.delete(url); + // An abandoned Suspense render never reaches useEffect, but useGLTF still caches + // its request/result. Clear that cache entry once no committed consumer owns it. + if (!WEAPON_ASSET_REFERENCES.has(url)) useGLTF.clear(url); +} + +function registerPendingWeaponAsset(url: string) { + if (WEAPON_ASSET_REFERENCES.has(url) || PENDING_WEAPON_ASSETS.has(url)) return; + PENDING_WEAPON_ASSETS.set(url, setTimeout( + () => releasePendingWeaponAsset(url), + PENDING_WEAPON_ASSET_RELEASE_MS, + )); + while (PENDING_WEAPON_ASSETS.size > MAX_PENDING_WEAPON_ASSETS) { + const oldestUrl = PENDING_WEAPON_ASSETS.keys().next().value as string | undefined; + if (!oldestUrl) break; + releasePendingWeaponAsset(oldestUrl); + } +} + +function commitPendingWeaponAsset(url: string) { + const releaseTimer = PENDING_WEAPON_ASSETS.get(url); + if (releaseTimer === undefined) return; + clearTimeout(releaseTimer); + PENDING_WEAPON_ASSETS.delete(url); +} + +function disposeWeaponAssetScene(scene: THREE.Object3D) { + const geometries = new Set(); + const materials = new Set(); + const textures = new Set(); + scene.traverse((object) => { + if (!(object instanceof THREE.Mesh)) return; + geometries.add(object.geometry); + for (const material of Array.isArray(object.material) ? object.material : [object.material]) { + materials.add(material); + for (const value of Object.values(material)) { + if (value instanceof THREE.Texture) textures.add(value); + } + } + }); + for (const geometry of geometries) geometry.dispose(); + for (const material of materials) material.dispose(); + for (const texture of textures) texture.dispose(); +} + +/** + * Drei caches parsed GLBs forever by default. Weapon browsing can touch eleven large + * embedded atlases, so release an asset after its last mounted user disappears. + */ +function useWeaponGLTF(url: string) { + registerPendingWeaponAsset(url); + const gltf = useGameGLTF(url); + useEffect(() => { + commitPendingWeaponAsset(url); + const existing = WEAPON_ASSET_REFERENCES.get(url); + if (existing) { + existing.count += 1; + if (existing.releaseTimer !== null) { + clearTimeout(existing.releaseTimer); + existing.releaseTimer = null; + } + } else { + WEAPON_ASSET_REFERENCES.set(url, { count: 1, scene: gltf.scene, releaseTimer: null }); + } + return () => { + const reference = WEAPON_ASSET_REFERENCES.get(url); + if (!reference) return; + reference.count = Math.max(0, reference.count - 1); + if (reference.count > 0 || reference.releaseTimer !== null) return; + reference.releaseTimer = setTimeout(() => { + const current = WEAPON_ASSET_REFERENCES.get(url); + if (!current || current.count > 0) return; + disposeWeaponAssetScene(current.scene); + useGLTF.clear(url); + WEAPON_ASSET_REFERENCES.delete(url); + }, 0); + }; + }, [gltf.scene, url]); + return gltf; +} + +interface StaffGlowMaterialBinding { + material: THREE.Material & { emissive: THREE.Color; emissiveIntensity: number }; + baseEmissive: THREE.Color; + baseEmissiveIntensity: number; +} + +function supportsEmissiveGlow(material: THREE.Material): material is StaffGlowMaterialBinding["material"] { + return "emissive" in material + && material.emissive instanceof THREE.Color + && "emissiveIntensity" in material + && typeof material.emissiveIntensity === "number"; +} + +function prepareStaffGlow(scene: THREE.Object3D) { + const materialClones = new Map(); + const bindings: StaffGlowMaterialBinding[] = []; + scene.traverse((object) => { + if (!(object instanceof THREE.Mesh)) return; + const cloneMaterial = (source: THREE.Material) => { + const existing = materialClones.get(source); + if (existing) return existing; + const clone = source.clone(); + materialClones.set(source, clone); + if (supportsEmissiveGlow(clone)) { + bindings.push({ + material: clone, + baseEmissive: clone.emissive.clone(), + baseEmissiveIntensity: clone.emissiveIntensity, + }); + } + return clone; + }; + object.material = Array.isArray(object.material) + ? object.material.map(cloneMaterial) + : cloneMaterial(object.material); + }); + + scene.updateMatrixWorld(true); + const bounds = new THREE.Box3().setFromObject(scene); + const height = bounds.max.y - bounds.min.y; + const tipPosition: [number, number, number] = [ + (bounds.min.x + bounds.max.x) * 0.5, + bounds.max.y - height * 0.06, + (bounds.min.z + bounds.max.z) * 0.5, + ]; + return { bindings, materials: [...materialClones.values()], tipPosition }; +} + +function createActorScene(source: THREE.Object3D, profile: HealerVisualProfile | null) { + const scene = cloneSkeleton(source); + if (!profile) return scene; + + // Preserve each GLB's authored material and texture palette. Class identity belongs + // in geometry, equipment, and small accents; whole-body tinting erases surface detail. + for (const nodeName of profile.hiddenNodes) { + const node = resolveRigNode(scene, nodeName); + if (node) node.visible = false; + } + return scene; +} + +function createRigActorScene(source: THREE.Object3D) { + const scene = cloneSkeleton(source); + const renderNodes: THREE.Object3D[] = []; + scene.traverse((object) => { + if (object instanceof THREE.Mesh) renderNodes.push(object); + }); + for (const renderNode of renderNodes) renderNode.parent?.remove(renderNode); + return scene; +} + +function disposeActorSkeletons(scene: THREE.Object3D) { + const skeletons = new Set(); + scene.traverse((object) => { + if (object instanceof THREE.SkinnedMesh) skeletons.add(object.skeleton); + }); + for (const skeleton of skeletons) skeleton.dispose(); +} + +function StaffCastGlow({ + bindings, + position, +}: { + bindings: readonly StaffGlowMaterialBinding[]; + position: readonly [number, number, number]; +}) { + const aura = useRef(null); + const auraMaterial = useRef(null); + const light = useRef(null); + const glowColor = useMemo(() => new THREE.Color(), []); + const lastClassId = useRef(null); + const lastPulseId = useRef(useGameStore.getState().scenePulse.id); + const afterglowAge = useRef(null); + const lastStrength = useRef(Number.NaN); + + useFrame(({ clock }, delta) => { + const state = useGameStore.getState(); + if (state.scenePulse.id !== lastPulseId.current) { + lastPulseId.current = state.scenePulse.id; + if (isHealerPulseKind(state.scenePulse.kind)) afterglowAge.current = 0; + } else if (afterglowAge.current !== null) { + afterglowAge.current += delta; + if (afterglowAge.current >= STAFF_CAST_AFTERGLOW_SECONDS) afterglowAge.current = null; + } + + const activeCast = state.activeCast; + const castingProgress = activeCast + ? (state.time - activeCast.startedAt) / Math.max(0.001, activeCast.completesAt - activeCast.startedAt) + : null; + const baseStrength = staffCastGlowStrength({ castingProgress, afterglowAge: afterglowAge.current }); + const strength = Math.min(1, baseStrength * (0.94 + Math.sin(clock.elapsedTime * 11) * 0.06)); + const classChanged = lastClassId.current !== state.healerClassId; + if (classChanged) { + lastClassId.current = state.healerClassId; + glowColor.set(STAFF_CAST_GLOW_PROFILES[state.healerClassId].color); + if (auraMaterial.current) auraMaterial.current.color.copy(glowColor); + if (light.current) light.current.color.copy(glowColor); + } + if (!classChanged && Math.abs(strength - lastStrength.current) < 0.001) return; + lastStrength.current = strength; + + for (const binding of bindings) { + binding.material.emissive.copy(binding.baseEmissive).lerp(glowColor, strength); + binding.material.emissiveIntensity = binding.baseEmissiveIntensity + strength * 3.2; + } + if (aura.current) { + aura.current.visible = strength > 0.01; + aura.current.scale.setScalar(0.82 + strength * 0.3); + } + if (auraMaterial.current) auraMaterial.current.opacity = strength * 0.62; + if (light.current) light.current.intensity = strength * 2.8; + }); + + return ( + + + + + + + + ); +} + function PartyCharacterModel({ memberId, + visualMemberId = memberId, + healerClassId, + appearanceOverride, + modelMode = CHARACTER_MODEL_MODE, animationState, animationTrigger, }: { memberId: MemberId; + visualMemberId?: MemberId; + healerClassId?: HealerClassId; + appearanceOverride?: CharacterAppearanceV1; + modelMode?: CharacterModelMode; animationState: MutableRefObject; animationTrigger: MutableRefObject; }) { - const gltf = useGameGLTF(PARTY_MODEL_URLS[memberId]); - const actorScene = useMemo(() => cloneSkeleton(gltf.scene), [gltf.scene]); - const loadout = PARTY_WEAPON_URLS[memberId]; - const grips = PARTY_WEAPON_GRIPS[memberId]; - const rightWeapon = useGameGLTF(loadout.right); - const leftWeapon = useGameGLTF(loadout.left ?? loadout.right); + const healerVisual = memberId === "aelia" && healerClassId + ? HEALER_VISUAL_PROFILES[healerClassId] + : null; + const healerAppearance = healerVisual + ? modelMode === "modular" && appearanceOverride + ? appearanceOverride + : healerVisual.appearance + : null; + const usesModularRenderer = Boolean(healerAppearance && modelMode === "modular"); + const modularAppearance = usesModularRenderer + ? healerAppearance + : null; + const bodyMemberId = healerVisual?.bodyMemberId ?? visualMemberId; + const scaleMemberId = modularAppearance?.scaleSourceMemberId ?? bodyMemberId; + const animationMemberId = healerVisual?.animationMemberId ?? visualMemberId; + const bodyGltf = useGameGLTF(PARTY_MODEL_URLS[bodyMemberId]); + const animationGltf = useGameGLTF(PARTY_MODEL_URLS[animationMemberId]); + const actorScene = useMemo( + () => usesModularRenderer + ? createRigActorScene(animationGltf.scene) + : createActorScene(bodyGltf.scene, healerVisual), + [animationGltf.scene, bodyGltf.scene, healerVisual, usesModularRenderer], + ); + const partyLoadout = PARTY_WEAPON_URLS[visualMemberId]; + const partyModelIds = PARTY_WEAPON_MODEL_IDS[visualMemberId]; + const resolvedHealerEquipment = healerAppearance + ? resolveCharacterEquipment(healerAppearance) + : null; + const rightModelId = resolvedHealerEquipment?.mainHand.modelId ?? partyModelIds.right; + const effectiveLeftModelId = resolvedHealerEquipment?.offHand?.modelId ?? (healerAppearance ? undefined : partyModelIds.left); + const wearsCrossbowQuiver = Boolean(usesModularRenderer && resolvedHealerEquipment?.backPropModelId); + const loadout = healerAppearance + ? { + right: characterEquipmentAssetUrl(rightModelId), + left: effectiveLeftModelId ? characterEquipmentAssetUrl(effectiveLeftModelId) : undefined, + } + : partyLoadout; + const grips = { + right: weaponDefinition(rightModelId).grip as CharacterWeaponGrip, + left: effectiveLeftModelId + ? weaponDefinition(effectiveLeftModelId).grip as CharacterWeaponGrip + : undefined, + }; + const rightWeapon = useWeaponGLTF(loadout.right); + const leftWeapon = useWeaponGLTF(loadout.left ?? loadout.right); + const quiverWeapon = useWeaponGLTF(wearsCrossbowQuiver ? CROSSBOW_QUIVER_URL : loadout.right); const rightHandSlot = resolveRigNode(actorScene, "handslot.r"); const leftHandSlot = resolveRigNode(actorScene, "handslot.l"); + const backSlot = resolveRigNode(actorScene, "chest") ?? resolveRigNode(actorScene, "spine"); + const accessorySlot = healerVisual ? resolveRigNode(actorScene, "head") : null; + const renderedAppearance = useMemo( + () => modularAppearance && resolvedHealerEquipment?.suppressSkinnedBack + ? { ...modularAppearance, backPartId: null } + : modularAppearance, + [modularAppearance, resolvedHealerEquipment?.suppressSkinnedBack], + ); const rightWeaponScene = useMemo( - () => prepareHeldWeapon(rightWeapon.scene.clone(true), grips.right, "r"), - [grips.right, rightWeapon.scene], + () => prepareHeldWeapon(rightWeapon.scene.clone(true), rightModelId, grips.right, "r"), + [grips.right, rightModelId, rightWeapon.scene], ); const leftWeaponScene = useMemo( - () => loadout.left ? prepareHeldWeapon(leftWeapon.scene.clone(true), grips.left ?? grips.right, "l") : null, - [grips.left, grips.right, leftWeapon.scene, loadout.left], + () => loadout.left && effectiveLeftModelId + ? prepareHeldWeapon(leftWeapon.scene.clone(true), effectiveLeftModelId, grips.left ?? grips.right, "l") + : null, + [effectiveLeftModelId, grips.left, grips.right, leftWeapon.scene, loadout.left], ); - const { actions } = useAnimations(gltf.animations, actorScene); + const quiverScene = useMemo( + () => wearsCrossbowQuiver ? prepareBackQuiver(quiverWeapon.scene.clone(true)) : null, + [quiverWeapon.scene, wearsCrossbowQuiver], + ); + const staffGlow = useMemo( + () => memberId === "aelia" && grips.right !== "crossbow" ? prepareStaffGlow(rightWeaponScene) : null, + [grips.right, memberId, rightWeaponScene], + ); + const { actions } = useAnimations(animationGltf.animations, actorScene); const activeClip = useRef(undefined); const activeState = useRef(undefined); const activeTrigger = useRef(Number.NaN); + useEffect(() => { + // A newly selected weapon can suspend this subtree while its GLB loads. Force + // the actor action to restart after it resumes instead of leaving the shared + // healer rig in its bind pose. + activeState.current = undefined; + activeTrigger.current = Number.NaN; + }, [effectiveLeftModelId, rightModelId]); + useEffect(() => { actorScene.traverse((object) => { if (object instanceof THREE.Mesh) { @@ -302,8 +866,10 @@ function PartyCharacterModel({ }); }, [actorScene]); + useEffect(() => () => disposeActorSkeletons(actorScene), [actorScene]); + useEffect(() => { - for (const weaponScene of [rightWeaponScene, leftWeaponScene]) { + for (const weaponScene of [rightWeaponScene, leftWeaponScene, quiverScene]) { if (!weaponScene) continue; weaponScene.traverse((object) => { if (object instanceof THREE.Mesh) { @@ -314,7 +880,11 @@ function PartyCharacterModel({ } }); } - }, [leftWeaponScene, rightWeaponScene]); + }, [leftWeaponScene, quiverScene, rightWeaponScene]); + + useEffect(() => () => { + for (const material of staffGlow?.materials ?? []) material.dispose(); + }, [staffGlow]); useFrame(() => { const state = animationState.current; @@ -330,7 +900,7 @@ function PartyCharacterModel({ : state === "cast" ? "Spellcasting" : state === "attack" - ? PARTY_ATTACK_CLIPS[memberId] + ? PARTY_ATTACK_CLIPS[visualMemberId] : "Idle"; if (!shouldStartActorAnimation(activeState.current, activeTrigger.current, state, trigger)) return; const next = actions[clipName]; @@ -354,9 +924,25 @@ function PartyCharacterModel({ return ( <> - + {renderedAppearance && ( + + )} + {rightHandSlot && createPortal(, rightHandSlot)} + {rightHandSlot && staffGlow && createPortal( + , + rightHandSlot, + )} {leftWeaponScene && leftHandSlot && createPortal(, leftHandSlot)} + {quiverScene && backSlot && createPortal(, backSlot)} + {accessorySlot && healerVisual && createPortal( + , + accessorySlot, + )} ); } @@ -450,6 +1036,16 @@ function Character({ memberId, selected = false }: { memberId: Exclude(null); const animationState = useRef("idle"); const animationTrigger = useRef(0); + const visualArchetype = useGameStore((state) => state.party.find((member) => member.id === memberId)?.runProfile?.visualArchetype); + const visualMemberId: MemberId = visualArchetype === "knight" + ? "brann" + : visualArchetype === "ranger" + ? "nia" + : visualArchetype === "mage" + ? "orin" + : visualArchetype === "rogue" + ? "vale" + : memberId; useEffect(() => { const start = useGameStore.getState().partyPositions[memberId]; group.current?.position.set(start[0], 0.025, start[1]); @@ -487,7 +1083,7 @@ function Character({ memberId, selected = false }: { memberId: Exclude 0 && (state.phase === "combat" || moving)) { const faceBoss = state.phase === "combat"; @@ -505,7 +1101,7 @@ function Character({ memberId, selected = false }: { memberId: Exclude - + {selected && ( @@ -516,14 +1112,24 @@ function Character({ memberId, selected = false }: { memberId: Exclude(null); const animationState = useRef("idle"); const animationTrigger = useRef(0); const keys = useRef(new Set()); const scenePulse = useGameStore((state) => state.scenePulse); + const healerClassId = useGameStore((state) => state.healerClassId); const selected = useGameStore((state) => state.selectedMemberId === "aelia"); + const rpgEncounterKey = useGameStore((state) => { + const phase = state.rpgRun?.phase; + if (state.runMode !== "rpg-roguelike") return null; + const bossIndex = state.rpgRun?.bossIndex ?? 0; + if (phase === "challenge-active") return bossIndex * 2 + 1; + if (phase === "boss-combat") return bossIndex * 2 + 2; + return null; + }); const setPlayerPosition = useGameStore((state) => state.setPlayerPosition); + const setHockeyAimDirection = useGameStore((state) => state.setHockeyAimDirection); const { camera } = useThree(); const broadcastTimer = useRef(0); const castingUntil = useRef(0); @@ -531,14 +1137,17 @@ function PlayerCharacter() { const desiredCameraPosition = useMemo(() => new THREE.Vector3(), []); const cameraOrbit = useRef({ yaw: DEFAULT_CAMERA_YAW, pitch: DEFAULT_CAMERA_PITCH }); const cameraRelativeMovement = useRef({ x: 0, z: 0 }); + const hockeyAimMovement = useRef({ x: 0, z: -1 }); useEffect(() => { const start = useGameStore.getState().partyPositions.aelia; group.current?.position.set(start[0], 0.025, start[1]); - }, []); + keys.current.clear(); + broadcastTimer.current = 0; + }, [rpgEncounterKey]); useEffect(() => { - if (["renew", "shield", "purify", "radiance", "barrier"].includes(scenePulse.kind)) { + if (["periodic-heal", "protective", "cleanse", "group-heal", "field"].includes(scenePulse.kind)) { castingUntil.current = performance.now() + 700; instantCastTrigger.current = scenePulse.id; } @@ -555,10 +1164,19 @@ function PlayerCharacter() { const nudgeZ = Number(key === "s") - Number(key === "w"); if (!nudgeX && !nudgeZ) return; setCameraRelativeMovement(cameraRelativeMovement.current, nudgeX, nudgeZ, cameraOrbit.current.yaw); - const next = clampToArena([ + const aetherAssaultMode = state.activityMode === "aether-assault"; + const hockeyMode = state.activityMode === "hockey-healing" || state.activityMode === "hockey-healing-pvp" || state.activityMode === "blockbreaker"; + const requestedPosition: [number, number] = [ group.current.position.x + cameraRelativeMovement.current.x * 0.18, group.current.position.z + cameraRelativeMovement.current.z * 0.18, - ]); + ]; + const next = state.runMode === "rpg-roguelike" && state.activityMode === "boss" + ? clampToBossArenaWithPortals(requestedPosition, { north: state.rpgRun?.phase === "boss-cleared", south: false }) + : aetherAssaultMode + ? clampToHockeyArena(requestedPosition, 0.65) + : hockeyMode + ? clampToHockeyHealerHalf(requestedPosition, 0.65) + : clampToArena(requestedPosition); group.current.position.x = next[0]; group.current.position.z = next[1]; setPlayerPosition([group.current.position.x, group.current.position.z]); @@ -579,15 +1197,21 @@ function PlayerCharacter() { const state = useGameStore.getState(); const knocked = state.party[0].knockedUntil > state.time; const player = state.party[0]; - if (state.phase === "combat" && !state.paused && !state.activeCast && player.hp > 0 && !knocked) { - inputX = Number(keys.current.has("d")) - Number(keys.current.has("a")); - inputZ = Number(keys.current.has("s")) - Number(keys.current.has("w")); - const controller = getControllerMovement(); - inputX += controller.moveX; - inputZ += controller.moveY; - } const controller = getControllerMovement(); - if (state.phase === "combat" && !state.paused) { + const rawInputX = Number(keys.current.has("d")) - Number(keys.current.has("a")) + controller.moveX; + const rawInputZ = Number(keys.current.has("s")) - Number(keys.current.has("w")) + controller.moveY; + if ((state.activityMode === "hockey-healing" || state.activityMode === "hockey-healing-pvp" || state.activityMode === "blockbreaker") && state.phase === "combat" && !state.paused) { + setCameraRelativeMovement(hockeyAimMovement.current, rawInputX, rawInputZ, cameraOrbit.current.yaw); + setHockeyAimDirection([hockeyAimMovement.current.x, hockeyAimMovement.current.z]); + } + if (state.phase === "combat" && !state.paused && !state.activeCast && player.hp > 0 && !knocked) { + inputX = rawInputX; + inputZ = rawInputZ; + } + if (state.activityMode === "aether-assault") { + cameraOrbit.current.yaw = DEFAULT_CAMERA_YAW; + cameraOrbit.current.pitch = DEFAULT_CAMERA_PITCH; + } else if (state.phase === "combat" && !state.paused) { updateCameraOrbit(cameraOrbit.current, controller.lookX, controller.lookY, delta); } setCameraRelativeMovement(cameraRelativeMovement.current, inputX, inputZ, cameraOrbit.current.yaw); @@ -596,7 +1220,14 @@ function PlayerCharacter() { const length = Math.hypot(inputX, inputZ); if (length > 0.05) { const speed = 4.6 * state.gearModifiers.aelia.moveSpeed * delta / Math.max(1, length); - const next = clampToArena([group.current.position.x + inputX * speed, group.current.position.z + inputZ * speed]); + const requestedPosition: [number, number] = [group.current.position.x + inputX * speed, group.current.position.z + inputZ * speed]; + const next = state.runMode === "rpg-roguelike" && state.activityMode === "boss" + ? clampToBossArenaWithPortals(requestedPosition, { north: state.rpgRun?.phase === "boss-cleared", south: false }) + : state.activityMode === "aether-assault" + ? clampToHockeyArena(requestedPosition, 0.65) + : state.activityMode === "hockey-healing" || state.activityMode === "hockey-healing-pvp" || state.activityMode === "blockbreaker" + ? clampToHockeyHealerHalf(requestedPosition, 0.65) + : clampToArena(requestedPosition); group.current.position.x = next[0]; group.current.position.z = next[1]; group.current.rotation.y = Math.atan2(inputX, inputZ); @@ -635,20 +1266,28 @@ function PlayerCharacter() { const horizontalDistance = Math.cos(cameraOrbit.current.pitch) * CAMERA_ORBIT_DISTANCE; const sinYaw = Math.sin(cameraOrbit.current.yaw); const cosYaw = Math.cos(cameraOrbit.current.yaw); + const pvpOffsetZ = state.activityMode === "hockey-healing-pvp" ? HOCKEY_PVP_SIDE_OFFSET_Z : 0; desiredCameraPosition.set( group.current.position.x + sinYaw * horizontalDistance, CAMERA_FOCUS_HEIGHT + Math.sin(cameraOrbit.current.pitch) * CAMERA_ORBIT_DISTANCE, - group.current.position.z + cosYaw * horizontalDistance, + group.current.position.z + pvpOffsetZ + cosYaw * horizontalDistance, ); camera.position.lerp(desiredCameraPosition, 1 - Math.pow(0.002, delta)); camera.lookAt( group.current.position.x - sinYaw * CAMERA_LOOK_AHEAD, CAMERA_FOCUS_HEIGHT, - group.current.position.z - cosYaw * CAMERA_LOOK_AHEAD, + group.current.position.z + pvpOffsetZ - cosYaw * CAMERA_LOOK_AHEAD, ); broadcastTimer.current += delta; - if (broadcastTimer.current > 0.15) { + const activeRunMode = useGameStore.getState().runMode; + const positionSyncInterval = activeRunMode === "hockey-healing" + || activeRunMode === "hockey-healing-pvp" + || activeRunMode === "blockbreaker" + || activeRunMode === "aether-assault" + ? 0.08 + : 0.15; + if (broadcastTimer.current > positionSyncInterval) { setPlayerPosition([group.current.position.x, group.current.position.z]); broadcastTimer.current = 0; } @@ -656,19 +1295,448 @@ function PlayerCharacter() { return ( - + {selected && ( )} - ); } -function Party() { +function HockeyHealingPlayfield() { + const puck = useRef(null); + const puckGlow = useRef(null); + const npcPaddle = useRef(null); + const npcPaddleMaterial = useRef(null); + const arrow = useRef(null); + const arrowMaterial = useRef(null); + + useFrame(({ clock }, delta) => { + const state = useGameStore.getState(); + const hockeyMode = state.activityMode === "hockey-healing"; + if (puck.current) { + puck.current.visible = hockeyMode; + if (hockeyMode) { + puck.current.position.set(state.hockey.puckPosition[0], 0.48, state.hockey.puckPosition[1]); + puck.current.rotation.y += 0.07; + const pulse = 1 + Math.sin(clock.elapsedTime * 7) * 0.08; + puck.current.scale.setScalar(pulse); + if (puckGlow.current) puckGlow.current.opacity = 0.6 + Math.sin(clock.elapsedTime * 7) * 0.18; + } + } + if (npcPaddle.current) { + npcPaddle.current.visible = hockeyMode; + if (hockeyMode) { + npcPaddle.current.position.x = THREE.MathUtils.damp(npcPaddle.current.position.x, state.hockey.paddleX, 18, delta); + const hitPulse = Math.max(0, 1 - (state.time - state.hockey.paddleHitAt) / 0.22); + npcPaddle.current.scale.set(1 + hitPulse * 0.04, 1 + hitPulse * 0.16, 1); + if (npcPaddleMaterial.current) npcPaddleMaterial.current.emissiveIntensity = 1.8 + hitPulse * 3.4; + } + } + if (!arrow.current) return; + const arrowVisible = hockeyMode + && state.phase === "combat" + && hockeyAimPreviewVisible(state.hockey, state.partyPositions.aelia); + arrow.current.visible = arrowVisible; + if (!arrowVisible) return; + const direction = hockeyReturnDirection(state.hockey.aimDirection); + arrow.current.position.set(state.hockey.puckPosition[0], 0.09, state.hockey.puckPosition[1]); + arrow.current.rotation.y = Math.atan2(-direction[0], -direction[1]); + if (arrowMaterial.current) arrowMaterial.current.opacity = 0.62 + Math.sin(clock.elapsedTime * 8) * 0.2; + }); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +const BLOCKBREAKER_COLOR_KEYS: readonly BlockbreakerBrickColor[] = ["cyan", "amber", "magenta", "lime"]; + +function BlockbreakerPlayfield() { + const biome = useGameStore((state) => blockbreakerBiomeForSeed(state.blockbreaker.seed)); + const root = useRef(null); + const bricks = useRef>({ + cyan: null, + amber: null, + magenta: null, + lime: null, + }); + const marksA = useRef(null); + const marksB = useRef(null); + const puck = useRef(null); + const puckGlow = useRef(null); + const arrow = useRef(null); + const arrowMaterial = useRef(null); + const breakFx = useRef(null); + const breakFxMaterial = useRef(null); + const transform = useMemo(() => new THREE.Object3D(), []); + const layout = useRef({ active: false, seed: 0, rows: -1, broken: -1 }); + + useFrame(({ clock }, delta) => { + const state = useGameStore.getState(); + const active = state.activityMode === "blockbreaker"; + if (root.current) root.current.visible = active; + if (!active) { + layout.current.active = false; + return; + } + + const blockbreaker = state.blockbreaker; + const reducedMotion = document.documentElement.classList.contains("force-reduced-motion"); + if (puck.current) { + puck.current.visible = blockbreaker.status === "live"; + puck.current.position.set(blockbreaker.puckPosition[0], 0.48, blockbreaker.puckPosition[1]); + if (!reducedMotion) puck.current.rotation.y += delta * 5.5; + puck.current.scale.setScalar(reducedMotion ? 1 : 1 + Math.sin(clock.elapsedTime * 7) * 0.08); + if (puckGlow.current) puckGlow.current.opacity = reducedMotion ? 0.58 : 0.58 + Math.sin(clock.elapsedTime * 7) * 0.18; + } + + const needsLayout = !layout.current.active + || layout.current.seed !== blockbreaker.seed + || layout.current.rows !== blockbreaker.rowsSpawned + || layout.current.broken !== blockbreaker.bricksBroken; + const brickMeshesReady = bricks.current.cyan + && bricks.current.amber + && bricks.current.magenta + && bricks.current.lime; + if (needsLayout && brickMeshesReady && marksA.current && marksB.current) { + const count = Math.min(BLOCKBREAKER_MAX_BRICKS, blockbreaker.bricks.length); + const colorCounts: Record = { cyan: 0, amber: 0, magenta: 0, lime: 0 }; + marksA.current.count = count; + marksB.current.count = count; + for (let index = 0; index < count; index += 1) { + const brick = blockbreaker.bricks[index]; + const x = blockbreakerColumnX(brick.column); + const z = blockbreakerRowZ(brick.row); + const colorIndex = colorCounts[brick.color]; + transform.position.set(x, 0.72, z); + transform.rotation.set(0, 0, 0); + transform.scale.set(BLOCKBREAKER_BRICK_WIDTH, 1.24, BLOCKBREAKER_BRICK_DEPTH); + transform.updateMatrix(); + bricks.current[brick.color]?.setMatrixAt(colorIndex, transform.matrix); + colorCounts[brick.color] = colorIndex + 1; + + transform.position.set(x, 0.72, z + BLOCKBREAKER_BRICK_DEPTH * 0.535); + transform.rotation.set(0, 0, brick.color === "amber" ? Math.PI / 4 : 0); + if (brick.color === "cyan") transform.scale.set(0.14, 0.38, 0.035); + else if (brick.color === "amber") transform.scale.set(0.27, 0.27, 0.035); + else if (brick.color === "magenta") transform.scale.set(0.35, 0.09, 0.035); + else transform.scale.set(0.34, 0.075, 0.035); + if (brick.color === "lime") transform.position.y += 0.16; + transform.updateMatrix(); + marksA.current.setMatrixAt(index, transform.matrix); + + transform.position.set(x, 0.72, z + BLOCKBREAKER_BRICK_DEPTH * 0.54); + transform.rotation.set(0, 0, 0); + if (brick.color === "magenta") transform.scale.set(0.09, 0.35, 0.035); + else if (brick.color === "lime") { + transform.position.y -= 0.16; + transform.scale.set(0.34, 0.075, 0.035); + } else transform.scale.setScalar(0.0001); + transform.updateMatrix(); + marksB.current.setMatrixAt(index, transform.matrix); + } + for (const color of BLOCKBREAKER_COLOR_KEYS) { + const mesh = bricks.current[color]; + if (!mesh) continue; + mesh.count = colorCounts[color]; + mesh.instanceMatrix.needsUpdate = true; + mesh.computeBoundingSphere(); + } + marksA.current.instanceMatrix.needsUpdate = true; + marksB.current.instanceMatrix.needsUpdate = true; + marksA.current.computeBoundingSphere(); + marksB.current.computeBoundingSphere(); + layout.current = { + active: true, + seed: blockbreaker.seed, + rows: blockbreaker.rowsSpawned, + broken: blockbreaker.bricksBroken, + }; + } + + if (arrow.current) { + const visible = state.phase === "combat" && blockbreakerAimPreviewVisible(blockbreaker, state.partyPositions.aelia); + arrow.current.visible = visible; + if (visible) { + const direction = hockeyReturnDirection(blockbreaker.aimDirection); + arrow.current.position.set(blockbreaker.puckPosition[0], 0.09, blockbreaker.puckPosition[1]); + arrow.current.rotation.y = Math.atan2(-direction[0], -direction[1]); + if (arrowMaterial.current) arrowMaterial.current.opacity = reducedMotion ? 0.72 : 0.62 + Math.sin(clock.elapsedTime * 8) * 0.2; + } + } + + if (breakFx.current) { + const age = state.time - blockbreaker.lastBreakAt; + const visible = age >= 0 && age < 0.42; + breakFx.current.visible = visible; + if (visible) { + const progress = age / 0.42; + breakFx.current.position.set(blockbreaker.puckPosition[0], 0.2, blockbreaker.puckPosition[1]); + breakFx.current.scale.setScalar(reducedMotion ? 1.4 : 0.6 + progress * 4.2); + if (breakFxMaterial.current) breakFxMaterial.current.opacity = reducedMotion ? 0.5 : (1 - progress) * 0.9; + } + } + }); + + return ( + + {BLOCKBREAKER_COLOR_KEYS.map((color) => ( + { bricks.current[color] = mesh; }} + args={[undefined, undefined, BLOCKBREAKER_MAX_BRICKS]} + castShadow + receiveShadow + frustumCulled={false} + > + + + + ))} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function AetherAssaultPlayfield() { + const root = useRef(null); + const hulls = useRef(null); + const wings = useRef(null); + const trails = useRef(null); + const warnings = useRef(null); + const playerShots = useRef(null); + const enemyShots = useRef(null); + const hullMaterial = useRef(null); + const wingMaterial = useRef(null); + const trailMaterial = useRef(null); + const hitRing = useRef(null); + const hitMaterial = useRef(null); + const transform = useMemo(() => new THREE.Object3D(), []); + const standardColors = useMemo(() => AETHER_STANDARD_SHIP_COLORS.map((color) => new THREE.Color(color)), []); + + useFrame(({ clock }) => { + const state = useGameStore.getState(); + const active = state.activityMode === "aether-assault"; + if (root.current) root.current.visible = active; + if (!active || !hulls.current || !wings.current || !trails.current || !warnings.current || !playerShots.current || !enemyShots.current) return; + + const reducedMotion = document.documentElement.classList.contains("force-reduced-motion"); + const aether = state.aetherAssault; + const shipCount = Math.min(AETHER_MAX_SHIPS, aether.ships.length); + const formationColor = standardColors[aetherShipColorIndex(aether.seed, aether.wave, 0)]; + hullMaterial.current?.color.copy(formationColor); + wingMaterial.current?.color.copy(formationColor); + trailMaterial.current?.color.copy(formationColor); + let trailCount = 0; + let warningCount = 0; + hulls.current.count = shipCount; + wings.current.count = shipCount; + for (let index = 0; index < shipCount; index += 1) { + const ship = aether.ships[index]; + const armored = ship.kind === "armored"; + const hover = reducedMotion ? 0 : Math.sin(clock.elapsedTime * 3.2 + index * 0.7) * 0.1; + const diveTilt = ship.phase === "diving" ? Math.sin(clock.elapsedTime * 5 + index) * 0.38 : 0; + transform.position.set(ship.position[0], 2.25 + hover, ship.position[1]); + transform.rotation.set(diveTilt, Math.PI, diveTilt * 0.45); + transform.scale.setScalar(armored ? 0.82 : 0.66); + transform.updateMatrix(); + hulls.current.setMatrixAt(index, transform.matrix); + + transform.position.set(ship.position[0], 2.12 + hover, ship.position[1] + 0.08); + transform.rotation.set(diveTilt, Math.PI, diveTilt * 0.45); + transform.scale.set(armored ? 1.38 : 1.12, armored ? 0.12 : 0.09, armored ? 0.76 : 0.62); + transform.updateMatrix(); + wings.current.setMatrixAt(index, transform.matrix); + + if (ship.phase === "entering" || ship.phase === "diving" || ship.phase === "returning") { + transform.position.set(ship.position[0], 2.18 + hover, ship.position[1] + 0.85); + transform.rotation.set(Math.PI / 2, 0, 0); + transform.scale.set(0.11, 0.11, ship.phase === "diving" ? 1.5 : 0.92); + transform.updateMatrix(); + trails.current.setMatrixAt(trailCount, transform.matrix); + trailCount += 1; + } + if (ship.phase === "diving") { + transform.position.set(ship.targetPosition[0], 0.045, HOCKEY_ARENA_MAX_Z - 0.9); + transform.rotation.set(-Math.PI / 2, 0, 0); + transform.scale.setScalar(reducedMotion ? 1 : 0.82 + Math.sin(clock.elapsedTime * 8) * 0.14); + transform.updateMatrix(); + warnings.current.setMatrixAt(warningCount, transform.matrix); + warningCount += 1; + } + } + hulls.current.instanceMatrix.needsUpdate = true; + wings.current.instanceMatrix.needsUpdate = true; + trails.current.count = trailCount; + warnings.current.count = warningCount; + trails.current.instanceMatrix.needsUpdate = true; + warnings.current.instanceMatrix.needsUpdate = true; + + const playerShotCount = Math.min(AETHER_MAX_PLAYER_SHOTS, aether.playerShots.length); + playerShots.current.count = playerShotCount; + for (let index = 0; index < playerShotCount; index += 1) { + const shot = aether.playerShots[index]; + transform.position.set(shot.position[0], 1.15, shot.position[1]); + transform.rotation.set(Math.PI / 2, 0, 0); + transform.scale.set(0.14, 0.14, 0.58); + transform.updateMatrix(); + playerShots.current.setMatrixAt(index, transform.matrix); + } + playerShots.current.instanceMatrix.needsUpdate = true; + + const enemyShotCount = Math.min(AETHER_MAX_ENEMY_SHOTS, aether.enemyShots.length); + enemyShots.current.count = enemyShotCount; + for (let index = 0; index < enemyShotCount; index += 1) { + const shot = aether.enemyShots[index]; + transform.position.set(shot.position[0], 0.82, shot.position[1]); + transform.rotation.set(0, 0, 0); + transform.scale.setScalar(0.24); + transform.updateMatrix(); + enemyShots.current.setMatrixAt(index, transform.matrix); + } + enemyShots.current.instanceMatrix.needsUpdate = true; + + if (hitRing.current) { + const age = state.time - aether.lastPlayerHitAt; + const visible = age >= 0 && age < 0.6; + hitRing.current.visible = visible; + if (visible) { + const progress = age / 0.6; + const player = state.partyPositions.aelia; + hitRing.current.position.set(player[0], 0.08, player[1]); + hitRing.current.scale.setScalar(0.7 + progress * 2.8); + if (hitMaterial.current) hitMaterial.current.opacity = reducedMotion ? 0.68 : (1 - progress) * 0.9; + } + } + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function Party({ playerAppearance }: { playerAppearance?: CharacterAppearanceV1 }) { const selected = useGameStore((state) => state.selectedMemberId); const [loadSupportModels, setLoadSupportModels] = useState(false); @@ -680,7 +1748,7 @@ function Party() { return ( <> }> - + {loadSupportModels ? ( @@ -708,16 +1776,107 @@ function PartyFallback({ memberIds }: { memberIds: readonly MemberId[] }) { ); } -function BossFallback({ bossIndex }: { bossIndex: number }) { - const boss = useGameStore((state) => bossIndex === 0 ? state.boss : state.additionalBosses[bossIndex - 1]?.boss); - const motion = useGameStore((state) => bossIndex === 0 ? state.bossMotion : state.additionalBosses[bossIndex - 1]?.motion); +function OpponentCharacter({ memberId }: { memberId: MemberId }) { + const group = useRef(null); + const animationState = useRef("idle"); + const animationTrigger = useRef(0); + + useEffect(() => { + const start = useGameStore.getState().hockeyPvpOpponent.partyPositions[memberId]; + group.current?.position.set(start[0], 0.025, start[1]); + }, [memberId]); + + useFrame((_, delta) => { + if (!group.current) return; + const state = useGameStore.getState(); + const opponent = state.hockeyPvpOpponent; + const target = opponent.partyPositions[memberId]; + const dx = target[0] - group.current.position.x; + const dz = target[1] - group.current.position.z; + const moving = Math.hypot(dx, dz) > 0.015; + const blend = 1 - Math.pow(0.002, delta); + group.current.position.x = THREE.MathUtils.lerp(group.current.position.x, target[0], blend); + group.current.position.z = THREE.MathUtils.lerp(group.current.position.z, target[1], blend); + + const member = opponent.party.find((entry) => entry.id === memberId); + const actor = memberId === "aelia" ? null : opponent.partyCombat.combatants[memberId]; + const knocked = Boolean(member && member.knockedUntil > state.time); + const attacking = Boolean(actor?.visualAction && actor.visualAction.endsAt > state.time); + animationTrigger.current = !member || member.hp <= 0 + ? 0 + : knocked + ? member.knockedUntil + : attacking + ? actor?.visualAction?.startedAt ?? 0 + : 0; + animationState.current = !member || member.hp <= 0 + ? "death" + : knocked + ? "hit" + : attacking + ? "attack" + : moving + ? "walk" + : "idle"; + + if (member && member.hp > 0 && !knocked) { + const boss = opponent.bossMotion.position; + const facingX = boss[0] - group.current.position.x; + const facingZ = boss[1] - group.current.position.z; + const targetAngle = Math.atan2(facingX, facingZ); + const angleDelta = Math.atan2( + Math.sin(targetAngle - group.current.rotation.y), + Math.cos(targetAngle - group.current.rotation.y), + ); + group.current.rotation.y += angleDelta * (1 - Math.pow(0.0008, delta)); + } + }); + + return ( + + + + + + + + ); +} + +function OpponentPartyFallback() { + const positions = useGameStore((state) => state.hockeyPvpOpponent.partyPositions); + return ( + <> + {(Object.keys(positions) as MemberId[]).map((memberId) => ( + + + + + ))} + + ); +} + +function OpponentParty() { + return ( + }> + {(["aelia", "brann", "nia", "orin", "vale"] as MemberId[]).map((memberId) => ( + + ))} + + ); +} + +function BossFallback({ bossIndex, opponent = false }: { bossIndex: number; opponent?: boolean }) { + const boss = useGameStore((state) => opponent ? state.hockeyPvpOpponent.boss : bossIndex === 0 ? state.boss : state.additionalBosses[bossIndex - 1]?.boss); + const motion = useGameStore((state) => opponent ? state.hockeyPvpOpponent.bossMotion : bossIndex === 0 ? state.bossMotion : state.additionalBosses[bossIndex - 1]?.motion); const group = useRef(null); const material = useRef(null); const deathElapsed = useRef(0); useFrame((_, delta) => { const defeated = (boss?.hp ?? 1) <= 0; deathElapsed.current = defeated ? deathElapsed.current + delta : 0; - const opacity = bossDeathOpacity(deathElapsed.current); + const opacity = bossDeathOpacity(deathElapsed.current, boss?.id); if (group.current) group.current.visible = opacity > 0; if (material.current) { const transparent = opacity < 0.999; @@ -742,54 +1901,37 @@ function BossFallback({ bossIndex }: { bossIndex: number }) { ); } -function BullBoss({ bossIndex }: { bossIndex: number }) { +function BullBoss({ bossIndex, opponent = false }: { bossIndex: number; opponent?: boolean }) { const phase = useGameStore((state) => state.phase); - const motion = useGameStore((state) => bossIndex === 0 ? state.bossMotion : state.additionalBosses[bossIndex - 1]?.motion); - const motionMode = motion?.mode ?? "holding"; - const animationCue = motion ? bossAnimationCue(motion) : "idle"; - const bossHp = useGameStore((state) => (bossIndex === 0 ? state.boss : state.additionalBosses[bossIndex - 1]?.boss)?.hp ?? 0); + const bossHp = useGameStore((state) => (opponent ? state.hockeyPvpOpponent.boss : bossIndex === 0 ? state.boss : state.additionalBosses[bossIndex - 1]?.boss)?.hp ?? 0); const defeated = bossHp <= 0; const group = useRef(null); + const modelRoot = useRef(null); const light = useRef(null); const gltf = useGLTF(BULL_URL, false, true); const { model: bullScene, fadeMaterials } = useMemo(() => createBossRenderModel(gltf.scene), [gltf.scene]); const { actions } = useAnimations(gltf.animations, bullScene); const targetPosition = useMemo(() => new THREE.Vector3(), []); + useBossAnimationPlayback({ + actions, + clips: BULL_BOSS_ANIMATION_CONFIG, + archetype: "bull", + bossIndex, + opponent, + modelRoot, + }); + useEffect(() => { return () => { for (const entry of fadeMaterials) entry.material.dispose(); }; }, [fadeMaterials]); - useBossDeathFade(group, light, fadeMaterials, defeated, 2.8); - - const clipName = phase === "victory" || defeated - ? "Death" - : animationCue === "attack" - ? "Idle_Headlow" - : animationCue === "special" - ? "Gallop_Jump" - : animationCue === "move" - ? "Gallop" - : "Idle"; - - useEffect(() => { - const next = actions[clipName]; - if (!next) return; - for (const action of Object.values(actions)) action?.fadeOut(0.18); - next.reset().setEffectiveWeight(1).setEffectiveTimeScale(motionMode === "charging" ? 1.3 : 1).fadeIn(0.18).play(); - if (clipName === "Death") { - next.setLoop(THREE.LoopOnce, 1); - next.clampWhenFinished = true; - } else { - next.setLoop(THREE.LoopRepeat, Number.POSITIVE_INFINITY); - } - return () => { next.fadeOut(0.18); }; - }, [actions, clipName, motionMode]); + useBossDeathFade(group, light, fadeMaterials, defeated, 2.8, "bulldrome"); useFrame((_, delta) => { if (!group.current) return; const state = useGameStore.getState(); - const current = encounterBossAt(state, bossIndex); + const current = encounterBossAt(state, bossIndex, opponent); if (!current) return; const motion = current.motion; targetPosition.set(motion.position[0], 0.03, motion.position[1]); @@ -801,8 +1943,12 @@ function BullBoss({ bossIndex }: { bossIndex: number }) { return; } - let facingX = state.partyPositions.brann[0] - motion.position[0]; - let facingZ = state.partyPositions.brann[1] - motion.position[1]; + const partyPositions = opponent ? state.hockeyPvpOpponent.partyPositions : state.partyPositions; + const party = opponent ? state.hockeyPvpOpponent.party : state.party; + const targetIndex = selectMeleeTargetIndex(party); + const targetId = targetIndex >= 0 ? party[targetIndex].id : "brann"; + let facingX = partyPositions[targetId][0] - motion.position[0]; + let facingZ = partyPositions[targetId][1] - motion.position[1]; if (motion.mode === "telegraph" || motion.mode === "charging" || motion.mode === "pouncing") { facingX = motion.chargeEnd[0] - motion.chargeStart[0]; facingZ = motion.chargeEnd[1] - motion.chargeStart[1]; @@ -816,27 +1962,22 @@ function BullBoss({ bossIndex }: { bossIndex: number }) { if (phase === "briefing") return null; return ( - + + + ); } - -function alternateBossClip(kind: AlternateBossKind, motion: ReturnType["bossMotion"]) { - const config = ALTERNATE_BOSS_CONFIG[kind]; - return config[bossAnimationCue(motion)]; -} - -function AlternateBoss({ kind, bossIndex }: { kind: AlternateBossKind; bossIndex: number }) { +function AlternateBoss({ kind, bossIndex, opponent = false }: { kind: AlternateBossKind; bossIndex: number; opponent?: boolean }) { const config = ALTERNATE_BOSS_CONFIG[kind]; const archetype = BOSS_ARCHETYPE_BY_ID[kind]; const phase = useGameStore((state) => state.phase); - const motion = useGameStore((state) => bossIndex === 0 ? state.bossMotion : state.additionalBosses[bossIndex - 1]?.motion); - const motionMode = motion?.mode ?? "holding"; - const bossHp = useGameStore((state) => (bossIndex === 0 ? state.boss : state.additionalBosses[bossIndex - 1]?.boss)?.hp ?? 0); + const bossHp = useGameStore((state) => (opponent ? state.hockeyPvpOpponent.boss : bossIndex === 0 ? state.boss : state.additionalBosses[bossIndex - 1]?.boss)?.hp ?? 0); const defeated = bossHp <= 0; const group = useRef(null); + const modelRoot = useRef(null); const light = useRef(null); const assetUrl = selectedGameAssetUrl(config.url, config.optimizedUrl ?? config.url); const gltf = useGameGLTF(assetUrl); @@ -845,38 +1986,25 @@ function AlternateBoss({ kind, bossIndex }: { kind: AlternateBossKind; bossIndex const targetPosition = useMemo(() => new THREE.Vector3(), []); const burrowPositionY = bossBurrowPositionY(modelTopY, config.scale); + useBossAnimationPlayback({ + actions, + clips: config, + archetype, + bossIndex, + opponent, + modelRoot, + }); + useEffect(() => { return () => { for (const entry of fadeMaterials) entry.material.dispose(); }; }, [fadeMaterials]); - useBossDeathFade(group, light, fadeMaterials, defeated, 2.5); - - const clipName = phase === "victory" || defeated ? config.death : alternateBossClip(kind, motion ?? useGameStore.getState().bossMotion); - - useEffect(() => { - const next = actions[clipName]; - if (!next) return; - for (const action of Object.values(actions)) action?.fadeOut(0.16); - const timeScale = archetype === "duelist" && motionMode === "mantis_line_telegraph" - ? 0.55 - : archetype === "duelist" && motionMode === "mantis_cross_telegraph" - ? 0.6 - : 1; - next.reset().setEffectiveWeight(1).setEffectiveTimeScale(timeScale).fadeIn(0.16).play(); - const authoredOneShot = ![config.idle, config.move].includes(clipName); - if (phase === "victory" || defeated || authoredOneShot) { - next.setLoop(THREE.LoopOnce, 1); - next.clampWhenFinished = true; - } else { - next.setLoop(THREE.LoopRepeat, Number.POSITIVE_INFINITY); - } - return () => { next.fadeOut(0.16); }; - }, [actions, archetype, clipName, config.idle, config.move, defeated, motionMode, phase]); + useBossDeathFade(group, light, fadeMaterials, defeated, 2.5, kind); useFrame((_, delta) => { if (!group.current) return; const state = useGameStore.getState(); - const current = encounterBossAt(state, bossIndex); + const current = encounterBossAt(state, bossIndex, opponent); if (!current) return; const motion = current.motion; const airborne = archetype === "sky-sweeper" && motion.mode === "skyfall"; @@ -886,9 +2014,13 @@ function AlternateBoss({ kind, bossIndex }: { kind: AlternateBossKind; bossIndex group.current.position.lerp(targetPosition, 1 - Math.pow(0.00001, delta)); if (!bossCanTrackTarget(current.boss.hp)) return; + const partyPositions = opponent ? state.hockeyPvpOpponent.partyPositions : state.partyPositions; + const party = opponent ? state.hockeyPvpOpponent.party : state.party; + const meleeTargetIndex = selectMeleeTargetIndex(party); + const meleeTargetId = meleeTargetIndex >= 0 ? party[meleeTargetIndex].id : "brann"; let targetAngle = Math.atan2( - state.partyPositions.brann[0] - motion.position[0], - state.partyPositions.brann[1] - motion.position[1], + partyPositions[meleeTargetId][0] - motion.position[0], + partyPositions[meleeTargetId][1] - motion.position[1], ); if (archetype === "sky-sweeper" && (motion.mode === "breath_telegraph" || motion.mode === "breath_sweeping")) { targetAngle = motion.breathAngle; @@ -896,7 +2028,7 @@ function AlternateBoss({ kind, bossIndex }: { kind: AlternateBossKind; bossIndex motion.mode === "mantis_line_telegraph" || motion.mode === "mantis_cross_telegraph" ) { - const target = state.partyPositions[motion.chargeTargetId]; + const target = partyPositions[motion.chargeTargetId]; targetAngle = Math.atan2(target[0] - motion.position[0], target[1] - motion.position[1]); } else if (motion.mode === "telegraph" || motion.mode === "charging") { targetAngle = Math.atan2(motion.chargeEnd[0] - motion.position[0], motion.chargeEnd[1] - motion.position[1]); @@ -911,7 +2043,9 @@ function AlternateBoss({ kind, bossIndex }: { kind: AlternateBossKind; bossIndex if (phase === "briefing") return null; return ( - + + + ); @@ -930,6 +2064,9 @@ function BarrierField() { if (!active) return; group.current.position.set(state.barrier.center[0], 0.058, state.barrier.center[1]); group.current.rotation.y = clock.elapsedTime * 0.08; + const fieldColor = state.barrier.kind === "spirit-link" ? "#9d8cf2" : "#e7bf46"; + if (fill.current) fill.current.color.set(fieldColor); + if (innerRing.current) innerRing.current.color.set(fieldColor); if (fill.current) fill.current.opacity = 0.16 + (Math.sin(clock.elapsedTime * 2.6) + 1) * 0.035; if (innerRing.current) innerRing.current.opacity = 0.38 + (Math.sin(clock.elapsedTime * 3.2) + 1) * 0.12; }); @@ -971,11 +2108,13 @@ function TankAuraField() { useFrame(({ clock }) => { if (!group.current) return; const state = useGameStore.getState(); - const active = state.phase === "combat" && state.partyCombat.tankAura.expiresAt > state.time && state.party[1].hp > 0; + const sourceId = state.partyCombat.tankAura.sourceId; + const source = state.party.find((member) => member.id === sourceId); + const active = state.phase === "combat" && state.partyCombat.tankAura.expiresAt > state.time && Boolean(source && source.hp > 0); group.current.visible = active; if (!active) return; - const brann = state.partyPositions.brann; - group.current.position.set(brann[0], 0.06, brann[1]); + const tankPosition = state.partyPositions[sourceId]; + group.current.position.set(tankPosition[0], 0.06, tankPosition[1]); group.current.rotation.y = clock.elapsedTime * -0.22; if (material.current) material.current.opacity = 0.13 + (Math.sin(clock.elapsedTime * 5) + 1) * 0.05; }); @@ -1000,7 +2139,7 @@ function TankAuraField() { ); } -function RangedProjectile({ memberId }: { memberId: "nia" | "orin" }) { +function RangedProjectile({ memberId }: { memberId: AiCombatantId }) { const projectile = useRef(null); const impact = useRef(null); const coreMaterial = useRef(null); @@ -1015,14 +2154,18 @@ function RangedProjectile({ memberId }: { memberId: "nia" | "orin" }) { const current = useMemo(() => new THREE.Vector3(), []); const direction = useMemo(() => new THREE.Vector3(), []); const up = useMemo(() => new THREE.Vector3(0, 1, 0), []); + const visualArchetype = useGameStore((state) => state.party.find((member) => member.id === memberId)?.runProfile?.visualArchetype); + const arrowStyle = visualArchetype === "ranger" || !visualArchetype && memberId === "nia"; useFrame(({ clock }) => { if (!projectile.current || !impact.current) return; const state = useGameStore.getState(); const action = state.partyCombat.combatants[memberId].visualAction; const member = state.party.find((entry) => entry.id === memberId)!; + const profile = action ? partyAttackVfxProfile(action.abilityId) : null; const rapid = action?.abilityId === "rapid_fire"; const active = action !== null + && profile?.style === "projectile" && state.phase === "combat" && member.hp > 0 && action.abilityId !== "overcharge" @@ -1030,9 +2173,8 @@ function RangedProjectile({ memberId }: { memberId: "nia" | "orin" }) { && (rapid ? state.time <= action.endsAt : state.time <= action.impactAt); projectile.current.visible = active; impact.current.visible = false; - if (!action || state.phase !== "combat" || member.hp <= 0 || action.abilityId === "overcharge") return; + if (!action || !profile || profile.style !== "projectile" || state.phase !== "combat" || member.hp <= 0 || action.abilityId === "overcharge") return; - const profile = partyAttackVfxProfile(action.abilityId); if (lastAbilityId.current !== action.abilityId) { lastAbilityId.current = action.abilityId; coreMaterial.current?.color.set(profile.primary); @@ -1053,11 +2195,11 @@ function RangedProjectile({ memberId }: { memberId: "nia" | "orin" }) { end.set(target[0], 1.12, target[1]); if (active) { current.copy(start).lerp(end, progress); - current.y += Math.sin(progress * Math.PI) * (memberId === "orin" ? 0.95 : 0.34); + current.y += Math.sin(progress * Math.PI) * (arrowStyle ? 0.34 : 0.95); projectile.current.position.copy(current); direction.subVectors(end, start).normalize(); projectile.current.quaternion.setFromUnitVectors(up, direction); - const pulseScale = memberId === "orin" ? 1 + Math.sin(clock.elapsedTime * 14) * 0.12 : 1; + const pulseScale = arrowStyle ? 1 : 1 + Math.sin(clock.elapsedTime * 14) * 0.12; projectile.current.scale.setScalar(profile.scale * pulseScale); trail.current?.scale.set(1, profile.trail, 1); if (trailMaterial.current) trailMaterial.current.opacity = 0.34 + Math.sin(clock.elapsedTime * 10) * 0.08; @@ -1079,7 +2221,7 @@ function RangedProjectile({ memberId }: { memberId: "nia" | "orin" }) { return ( <> - {memberId === "nia" ? ( + {arrowStyle ? ( <> @@ -1132,13 +2274,15 @@ function RangedProjectile({ memberId }: { memberId: "nia" | "orin" }) { function RangedProjectiles() { return ( <> + + ); } -function CloseAttackVfx({ memberId }: { memberId: "brann" | "orin" | "vale" }) { +function CloseAttackVfx({ memberId }: { memberId: AiCombatantId }) { const group = useRef(null); const firstArc = useRef(null); const secondArc = useRef(null); @@ -1230,7 +2374,7 @@ function CloseAttackVfx({ memberId }: { memberId: "brann" | "orin" | "vale" }) { ); } -function PartyPowerAuraVfx({ memberId }: { memberId: "orin" | "vale" }) { +function PartyPowerAuraVfx({ memberId }: { memberId: AiCombatantId }) { const group = useRef(null); const material = useRef(null); useFrame(({ clock }) => { @@ -1238,12 +2382,13 @@ function PartyPowerAuraVfx({ memberId }: { memberId: "orin" | "vale" }) { const state = useGameStore.getState(); const actor = state.partyCombat.combatants[memberId]; const member = state.party.find((entry) => entry.id === memberId)!; - const active = state.phase === "combat" && member.hp > 0 && (memberId === "orin" ? actor.overchargeStacks > 0 : actor.bladeFlurryUntil > state.time); + const overcharged = actor.overchargeStacks > 0; + const active = state.phase === "combat" && member.hp > 0 && (overcharged || actor.bladeFlurryUntil > state.time); group.current.visible = active; if (!active) return; const position = state.partyPositions[memberId]; group.current.position.set(position[0], 0.16, position[1]); - group.current.rotation.y = clock.elapsedTime * (memberId === "orin" ? 1.4 : -1.8); + group.current.rotation.y = clock.elapsedTime * (overcharged ? 1.4 : -1.8); const pulse = 0.92 + Math.sin(clock.elapsedTime * 5.5) * 0.12; group.current.scale.setScalar(pulse); if (material.current) material.current.opacity = 0.38 + Math.sin(clock.elapsedTime * 4.2) * 0.1; @@ -1268,8 +2413,11 @@ function PartyCombatVfx() { <> + + + @@ -1291,6 +2439,106 @@ function BossActor() { ); } +function OpponentBossActor() { + const phase = useGameStore((state) => state.phase); + const bossId = useGameStore((state) => state.hockeyPvpOpponent.boss.id); + if (phase === "briefing") return null; + return ( + }> + {bossId === "bulldrome" + ? + : } + + ); +} + +function HockeyHealingPvpPlayfield() { + const puck = useRef(null); + const puckGlow = useRef(null); + const arrow = useRef(null); + const arrowMaterial = useRef(null); + + useFrame(({ clock }) => { + const state = useGameStore.getState(); + const active = state.activityMode === "hockey-healing-pvp"; + if (puck.current) { + puck.current.visible = active; + if (active) { + puck.current.position.set(state.hockeyPvp.puckPosition[0], 0.48, state.hockeyPvp.puckPosition[1]); + puck.current.rotation.y += 0.07; + puck.current.scale.setScalar(1 + Math.sin(clock.elapsedTime * 7) * 0.08); + if (puckGlow.current) puckGlow.current.opacity = 0.6 + Math.sin(clock.elapsedTime * 7) * 0.18; + } + } + if (!arrow.current) return; + const localPlayer = hockeyPvpLocalToWorld(state.partyPositions.aelia); + const distance = Math.hypot( + state.hockeyPvp.puckPosition[0] - localPlayer[0], + state.hockeyPvp.puckPosition[1] - localPlayer[1], + ); + const visible = active + && state.phase === "combat" + && state.hockeyPvp.puckVelocity[1] > 0 + && distance < 8; + arrow.current.visible = visible; + if (!visible) return; + const direction = hockeyReturnDirection(state.hockeyPvp.aimDirection); + arrow.current.position.set(state.hockeyPvp.puckPosition[0], 0.09, state.hockeyPvp.puckPosition[1]); + arrow.current.rotation.y = Math.atan2(-direction[0], -direction[1]); + if (arrowMaterial.current) arrowMaterial.current.opacity = 0.62 + Math.sin(clock.elapsedTime * 8) * 0.2; + }); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function EncounterActors({ playerAppearance }: { playerAppearance?: CharacterAppearanceV1 }) { + const pvp = useGameStore((state) => state.activityMode === "hockey-healing-pvp"); + const localOffset = pvp ? HOCKEY_PVP_SIDE_OFFSET_Z : 0; + return ( + <> + + + + + + + + + + {pvp && ( + + + + + )} + + ); +} + type PerformanceMemory = Performance & { memory?: { usedJSHeapSize: number; totalJSHeapSize: number; jsHeapSizeLimit: number }; }; @@ -1384,9 +2632,10 @@ function PerformanceProbe() { const FX_BURST_PARTICLE_COUNT = 8; function scenePulseColor(kind: PulseKind) { - if (kind === "shield") return "#62bdff"; - if (kind === "purify") return "#c39bff"; - if (kind === "renew") return "#72e0a1"; + if (kind === "protective") return "#62bdff"; + if (kind === "cleanse") return "#c39bff"; + if (kind === "periodic-heal") return "#72e0a1"; + if (kind === "field") return "#d8c16e"; if (kind === "breath") return "#66dcff"; if (kind === "venom") return "#8fdb4f"; if (kind === "tether") return "#d482ff"; @@ -1456,7 +2705,7 @@ function CombatFx() { return ; } -export function GameScene() { +export function GameScene({ playerAppearance }: { playerAppearance?: CharacterAppearanceV1 }) { const [dpr, setDpr] = useState(() => Math.min(MAX_RENDER_DPR, Math.max(MIN_RENDER_DPR, window.devicePixelRatio || 1))); const setRenderDpr = useCallback((next: number) => { setDpr((current) => Math.abs(current - next) < 0.001 ? current : next); @@ -1473,19 +2722,212 @@ export function GameScene() { - - - - - - - + + + + + {PERFORMANCE_PROBE_ENABLED && } ); } +const MIXED_GALLERY_APPEARANCE: CharacterAppearanceV1 = { + version: 1, + rigId: "medium", + scaleSourceMemberId: "brann", + headPartId: "rogue-head", + upperBodyPartId: "knight-upper", + lowerBodyPartId: "ranger-lower", + headwearPartId: "mage-hat", + backPartId: "druid-backpack", + mainHand: { modelId: "cc/adv_wand", grip: "wand" }, + offHand: { modelId: "cc/spellbook_open", grip: "prop" }, +}; + +function galleryAppearanceOverride(classId: HealerClassId, mix: string | null) { + if (!mix || CHARACTER_MODEL_MODE !== "modular") return undefined; + const base = HEALER_VISUAL_PROFILES[classId].appearance; + if (mix === "head") return { ...base, headPartId: "rogue-head" } satisfies CharacterAppearanceV1; + if (mix === "upper") return { ...base, upperBodyPartId: "knight-upper" } satisfies CharacterAppearanceV1; + if (mix === "lower") return { ...base, lowerBodyPartId: "ranger-lower" } satisfies CharacterAppearanceV1; + if (mix === "headwear") return { ...base, headwearPartId: "knight-helmet" } satisfies CharacterAppearanceV1; + if (mix === "back") return { ...base, backPartId: "druid-backpack" } satisfies CharacterAppearanceV1; + if (mix === "weapons") return { + ...base, + mainHand: { modelId: "cc/adv_wand", grip: "wand" }, + offHand: { modelId: "cc/spellbook_open", grip: "prop" }, + } satisfies CharacterAppearanceV1; + return mix === "all" || mix === "1" ? MIXED_GALLERY_APPEARANCE : undefined; +} + +export type HealerPreviewAnimation = Extract; + +function PreviewHealerActor({ + appearanceOverride, + animation = "idle", + classId, + modelMode = CHARACTER_MODEL_MODE, + positionX, +}: { + appearanceOverride?: CharacterAppearanceV1; + animation?: HealerPreviewAnimation; + classId: HealerClassId; + modelMode?: CharacterModelMode; + positionX: number; +}) { + const animationState = useRef(animation); + const animationTrigger = useRef(0); + const profile = HEALER_VISUAL_PROFILES[classId]; + + useEffect(() => { + if (animationState.current === animation) return; + animationState.current = animation; + animationTrigger.current += 1; + }, [animation]); + + return ( + + + + + + + + ); +} + +function GalleryCamera() { + const { camera } = useThree(); + useEffect(() => { + camera.lookAt(0, 1.15, 0); + camera.updateProjectionMatrix(); + }, [camera]); + return null; +} + +function PreviewFrameScheduler() { + const { advance } = useThree(); + const frameId = useRef(null); + const lastRenderedAt = useRef(null); + + useEffect(() => { + const schedule = (now: number) => { + const previous = lastRenderedAt.current; + if (previous === null || now - previous + FRAME_INTERVAL_JITTER_MS >= GAMEPLAY_FRAME_INTERVAL_MS) { + lastRenderedAt.current = now; + advance(now / 1000, true); + } + frameId.current = window.requestAnimationFrame(schedule); + }; + frameId.current = window.requestAnimationFrame(schedule); + return () => { + if (frameId.current !== null) window.cancelAnimationFrame(frameId.current); + frameId.current = null; + lastRenderedAt.current = null; + }; + }, [advance]); + + return null; +} + +export function HealerAppearancePreview({ + animation = "idle", + appearance, + classId, + modelMode = CHARACTER_MODEL_MODE, +}: { + animation?: HealerPreviewAnimation; + appearance: CharacterAppearanceV1; + classId: HealerClassId; + modelMode?: CharacterModelMode; +}) { + return ( + + + + + + + + + + + + + + + + + + ); +} + +/** Development-only deterministic view used for rig, socket, and silhouette QA. */ +export function HealerModelGallery() { + const query = new URLSearchParams(window.location.search); + const requestedClass = query.get("class"); + const requestedMix = query.get("mix"); + const galleryClasses = HEALER_CLASS_ORDER.filter((classId) => !requestedClass || classId === requestedClass); + const visibleClasses = galleryClasses.length > 0 ? galleryClasses : HEALER_CLASS_ORDER; + const solo = visibleClasses.length === 1; + return ( +
    + + + + + + + + + + + + + {visibleClasses.map((classId, index) => ( + + ))} + + + +
    + {visibleClasses.map((classId) => {classId})} +
    +
    + ); +} + if (LEGACY_GAME_ASSETS_FORCED) { for (const memberId of CRITICAL_PARTY_MEMBER_IDS) { useGLTF.preload(PARTY_MODEL_URLS[memberId], false, true); diff --git a/src/components/HealerClassAccessory.tsx b/src/components/HealerClassAccessory.tsx new file mode 100644 index 0000000..8540694 --- /dev/null +++ b/src/components/HealerClassAccessory.tsx @@ -0,0 +1,115 @@ +import { useFrame } from "@react-three/fiber"; +import { useRef } from "react"; +import type * as THREE from "three"; +import type { HealerVisualProfile } from "../game/healerVisuals"; + +function RelicMaterial({ color }: { color: string }) { + return ( + + ); +} + +export function HealerClassAccessory({ profile }: { profile: HealerVisualProfile }) { + const animated = useRef(null); + + useFrame((_, delta) => { + if (!animated.current || profile.accessory !== "clockwork-rings") return; + animated.current.rotation.z += delta * 0.42; + animated.current.rotation.y -= delta * 0.18; + }); + + if (profile.accessory === "sun-halo") { + return ( + + + + + + + + + + + ); + } + + if (profile.accessory === "grove-antlers") { + return ( + + + + + + + + + + + + + + + + + + + ); + } + + if (profile.accessory === "storm-totem") { + return ( + + + + + + + + + + + + + + + ); + } + + if (profile.accessory === "sun-crest") { + return ( + + + + + + + + + + + ); + } + + return ( + + + + + + + + + + + + + + + ); +} diff --git a/src/components/ModularCharacterBody.tsx b/src/components/ModularCharacterBody.tsx new file mode 100644 index 0000000..69fe86d --- /dev/null +++ b/src/components/ModularCharacterBody.tsx @@ -0,0 +1,114 @@ +import { createPortal } from "@react-three/fiber"; +import { useEffect, useMemo } from "react"; +import * as THREE from "three"; +import { + CHARACTER_PART_CATALOG, + characterAppearancePartIds, + type CharacterAppearanceV1, + type CharacterPartId, +} from "../game/characterAppearance"; +import type { MemberId } from "../game/types"; +import { useGameGLTF } from "./GameAssetProvider"; + +interface BoundCharacterPart { + group: THREE.Group; + skeletons: THREE.Skeleton[]; +} + +function rigBonesByName(rigScene: THREE.Object3D) { + const bones = new Map(); + rigScene.traverse((object) => { + if (object instanceof THREE.Bone) bones.set(object.name, object); + }); + return bones; +} + +function createBoundCharacterPart( + sourceScene: THREE.Object3D, + rigScene: THREE.Object3D, + partId: CharacterPartId, +): BoundCharacterPart { + const definition = CHARACTER_PART_CATALOG[partId]; + const rigBones = rigBonesByName(rigScene); + const group = new THREE.Group(); + group.name = `character-part:${partId}`; + const skeletons: THREE.Skeleton[] = []; + + sourceScene.updateMatrixWorld(true); + for (const nodeName of definition.nodeNames) { + const sourceNode = sourceScene.getObjectByName(nodeName); + if (!sourceNode) throw new Error(`Character part ${partId} is missing node ${nodeName}.`); + + const partNode = sourceNode.clone(true); + partNode.matrix.copy(sourceNode.matrixWorld); + partNode.matrix.decompose(partNode.position, partNode.quaternion, partNode.scale); + partNode.traverse((object) => { + if (!(object instanceof THREE.SkinnedMesh)) return; + const mappedBones = object.skeleton.bones.map((sourceBone) => { + const rigBone = rigBones.get(sourceBone.name); + if (!rigBone) throw new Error(`Character part ${partId} cannot resolve rig bone ${sourceBone.name}.`); + return rigBone; + }); + const skeleton = new THREE.Skeleton( + mappedBones, + object.skeleton.boneInverses.map((inverse) => inverse.clone()), + ); + const bindMatrix = object.bindMatrix.clone(); + object.bind(skeleton, bindMatrix); + object.castShadow = true; + object.receiveShadow = true; + object.frustumCulled = false; + skeletons.push(skeleton); + }); + group.add(partNode); + } + + return { group, skeletons }; +} + +function ModularCharacterPart({ + actorScene, + modelUrls, + partId, +}: { + actorScene: THREE.Object3D; + modelUrls: Record; + partId: CharacterPartId; +}) { + const definition = CHARACTER_PART_CATALOG[partId]; + const gltf = useGameGLTF(modelUrls[definition.sourceMemberId]); + const boundPart = useMemo( + () => createBoundCharacterPart(gltf.scene, actorScene, partId), + [actorScene, gltf.scene, partId], + ); + + useEffect(() => () => { + for (const skeleton of boundPart.skeletons) skeleton.dispose(); + }, [boundPart]); + + const rigRoot = actorScene.getObjectByName("Rig_Medium") ?? actorScene; + return createPortal(, rigRoot); +} + +export function ModularCharacterBody({ + actorScene, + appearance, + modelUrls, +}: { + actorScene: THREE.Object3D; + appearance: CharacterAppearanceV1; + modelUrls: Record; +}) { + return ( + <> + {characterAppearancePartIds(appearance).map((partId) => ( + + ))} + + ); +} diff --git a/src/components/TopScreen.tsx b/src/components/TopScreen.tsx index 1e43a7a..3c75c69 100644 --- a/src/components/TopScreen.tsx +++ b/src/components/TopScreen.tsx @@ -1,11 +1,15 @@ -import { lazy, Suspense } from "react"; -import { barrierProtects, upcomingEncounterMechanic, useGameStore } from "../game/store"; -import { HEALER_CLASSES } from "../game/healers"; +import { lazy, Suspense, useEffect, useRef, useState } from "react"; +import { barrierProtects, healerFieldContains, upcomingEncounterMechanic, useGameStore } from "../game/store"; +import { HEALER_ABILITIES } from "../game/healers"; import { BOSS_DEFINITIONS } from "../game/bossCatalog"; import { bossRoomFor } from "../game/bossRooms"; import { tankAuraProtects } from "../game/partyCombat"; import { BuffDraftPanel } from "./BuffDraftPanel"; import { DEFAULT_CONTROLLER_GLYPHS } from "../input/controllerGlyphs"; +import { hockeyPvpDampeningPercent } from "../game/hockeyHealingPvp"; +import { blockbreakerTimeMultiplier } from "../game/blockbreaker"; +import { RpgRunOverlay } from "./rpgRoguelike/RpgRunOverlay"; +import type { CharacterAppearanceV1 } from "../game/characterAppearance"; const GameScene = lazy(() => import("./GameScene").then((module) => ({ default: module.GameScene }))); @@ -43,10 +47,12 @@ function CompactParty() { )} - {member.renewExpiresAt > 0 && R} + {member.healingEffects.some((effect) => effect.expiresAt > time) && H} + {member.reactiveHeal && member.reactiveHeal.expiresAt > time && E{member.reactiveHeal.charges}} {member.debuffs.length > 0 && !} {barrierProtects(partyPositions[member.id], barrier, time) && B} - {tankAuraProtects(partyPositions[member.id], partyPositions.brann, tankAura, time) && T} + {barrier.kind === "spirit-link" && healerFieldContains(partyPositions[member.id], barrier, time) && S} + {tankAuraProtects(partyPositions[member.id], partyPositions[tankAura.sourceId], tankAura, time) && T} {member.knockedUntil > time && KD} @@ -57,12 +63,11 @@ function CompactParty() { } function CastingBar() { - const healerClassId = useGameStore((state) => state.healerClassId); - const abilityName = HEALER_CLASSES[healerClassId].abilities.mend.name; const activeCast = useGameStore((state) => state.activeCast); const time = useGameStore((state) => state.time); const party = useGameStore((state) => state.party); if (!activeCast) return null; + const abilityName = HEALER_ABILITIES[activeCast.abilityId].name; const duration = activeCast.completesAt - activeCast.startedAt; const progress = Math.min(1, Math.max(0, (time - activeCast.startedAt) / duration)); const target = party.find((member) => member.id === activeCast.targetId); @@ -78,13 +83,19 @@ function CastingBar() { function BossBar() { const boss = useGameStore((state) => state.boss); const additionalBosses = useGameStore((state) => state.additionalBosses); + const opponentBoss = useGameStore((state) => state.hockeyPvpOpponent.boss); const phase = useGameStore((state) => state.phase); + const activityMode = useGameStore((state) => state.activityMode); if (phase === "briefing") return null; - const bosses = [boss, ...additionalBosses.map((entry) => entry.boss)]; + const hockeyMode = activityMode === "hockey-healing"; + const blockbreakerMode = activityMode === "blockbreaker"; + const aetherAssaultMode = activityMode === "aether-assault"; + const pvpMode = activityMode === "hockey-healing-pvp"; + const bosses = pvpMode ? [boss, opponentBoss] : [boss, ...additionalBosses.map((entry) => entry.boss)]; return ( -
    1 ? "is-multi" : ""} ${bosses.length === 3 ? "is-trio" : ""}`}> - {bosses.map((entry) =>
    -
    Vault Beast{entry.name}{Math.ceil((entry.hp / entry.maxHp) * 100)}%
    +
    1 ? "is-multi" : ""} ${bosses.length === 3 ? "is-trio" : ""} ${pvpMode ? "is-pvp" : ""}`}> + {bosses.map((entry, index) =>
    +
    {pvpMode ? index === 0 ? "Your target" : "Rival target" : hockeyMode ? index === 0 ? "Striker · paddle + damage" : "Frontline · party damage" : blockbreakerMode ? index === 0 ? "Breaker flank" : "Wall pressure" : aetherAssaultMode ? index === 0 ? "Arcade flank" : "Party pressure" : "Vault Beast"}{entry.name}{Math.ceil((entry.hp / entry.maxHp) * 100)}%
    )}
    @@ -110,48 +121,123 @@ function EncounterCallout() { function PhaseOverlay() { const phase = useGameStore((state) => state.phase); const runMode = useGameStore((state) => state.runMode); + const activityMode = useGameStore((state) => state.activityMode); const round = useGameStore((state) => state.round); const endlessMode = useGameStore((state) => state.endlessMode); const endlessBossKills = useGameStore((state) => state.endlessBossKills); const primaryBoss = useGameStore((state) => state.boss); const additionalBosses = useGameStore((state) => state.additionalBosses); + const hockey = useGameStore((state) => state.hockey); + const blockbreaker = useGameStore((state) => state.blockbreaker); + const aetherAssault = useGameStore((state) => state.aetherAssault); + const hockeyPvp = useGameStore((state) => state.hockeyPvp); + if (runMode === "rpg-roguelike") return null; if (phase === "intermission") return ; const bosses = [primaryBoss, ...additionalBosses.map((entry) => entry.boss)]; const definitions = bosses.map((boss) => BOSS_DEFINITIONS[boss.id]); const room = bossRoomFor(primaryBoss.id); const bossNames = bosses.map((boss) => boss.name).join(" & "); + const hockeyMode = activityMode === "hockey-healing"; + const blockbreakerMode = activityMode === "blockbreaker"; + const aetherAssaultMode = activityMode === "aether-assault"; + const pvpMode = activityMode === "hockey-healing-pvp"; const showEndlessChoice = phase === "victory" && runMode === "rogue-trials" && round === 5 && !endlessMode; - const endlessDefeat = phase === "defeat" && endlessMode; - const briefingMode = runMode === "rogue-trials" + const endlessDefeat = phase === "defeat" && endlessMode && !hockeyMode && !blockbreakerMode && !aetherAssaultMode; + const briefingMode = hockeyMode + ? "Endless Goal Defense" + : blockbreakerMode + ? "Endless Color Break" + : aetherAssaultMode + ? "Endless Arcade Assault" + : pvpMode + ? `Versus ${hockeyPvp.opponentName}` + : runMode === "rogue-trials" ? bosses.length === 3 ? "Rogue Trials · Trio Finale" : "Rogue Trials · Dual Round" : bosses.length > 1 ? "Roguelike PVE · Dual Encounter" : definitions[0].trial; if (phase === "combat") return null; const title = phase === "briefing" - ? room.name + ? hockeyMode ? "Hockey Healing" : blockbreakerMode ? "Blockbreaker" : aetherAssaultMode ? "Aether Assault" : pvpMode ? "Healing Hockey PVP" : room.name : phase === "victory" - ? showEndlessChoice ? "Rogue Trials Cleared" : `${bossNames} Broken` - : endlessDefeat ? "Endless Run Ended" : "Party Broken"; + ? showEndlessChoice ? "Rogue Trials Cleared" : pvpMode ? "Match Won" : `${bossNames} Broken` + : hockeyMode ? "Goal Breached" : blockbreakerMode && blockbreaker.status === "lost" ? "Wall Breached" : aetherAssaultMode ? "Formation Lost" : pvpMode ? "Match Lost" : endlessDefeat ? "Endless Run Ended" : "Party Broken"; const eyebrow = phase === "briefing" - ? `${briefingMode} · ${room.biome}` + ? hockeyMode ? `${briefingMode} · Rectangular Boss Rink` : blockbreakerMode ? `${briefingMode} · Advancing Brick Rink` : aetherAssaultMode ? `${briefingMode} · Bright Five-Lane Rink` : pvpMode ? `${briefingMode} · Extended Versus Rink` : `${briefingMode} · ${room.biome}` : phase === "victory" - ? showEndlessChoice ? "Endless Path Unlocked" : "Encounter Complete" - : endlessDefeat ? `${endlessBossKills} Endless Bosses Defeated` : "Encounter Failed"; + ? showEndlessChoice ? "Endless Path Unlocked" : pvpMode ? `${hockeyPvp.opponentGoalsConceded} Rival Goals · ${endlessBossKills} Boss Kills` : "Encounter Complete" + : hockeyMode ? `${hockey.returns} Pucks Returned · ${endlessBossKills} Bosses Defeated` : blockbreakerMode ? `${blockbreaker.bricksBroken} Bricks · ${blockbreaker.score.toLocaleString()} Points` : aetherAssaultMode ? `${aetherAssault.score.toLocaleString()} Points · Wave ${aetherAssault.wave}` : pvpMode ? `${hockeyPvp.localGoalsConceded} Goals Conceded · ${hockeyPvp.opponentBossKills} Rival Boss Kills` : endlessDefeat ? `${endlessBossKills} Endless Bosses Defeated` : "Encounter Failed"; const copy = phase === "briefing" - ? definitions.map((boss) => boss.briefing).join(" ") + ? hockeyMode + ? "Defend the wide blue goal. Aim each return; the moving Pong paddle strikes it back. Party fights both bosses on enemy half. Fallen bosses are replaced." + : blockbreakerMode + ? "Aim the puck into advancing five-brick rows. Matching orthogonal colors break as one combo while two bosses pressure the party. Missed pucks safely re-serve." + : aetherAssaultMode + ? "Move across the full bright rink while spellfire launches automatically. Line up ship formations, dodge red bolts and dives, and keep healing through two endless bosses." + : pvpMode + ? "Two parties fight matching boss sequences. Defend your goal and aim each return. Every goal deals 45 damage to all five players. Fallen bosses respawn instantly." + : definitions.map((boss) => boss.briefing).join(" ") : phase === "victory" - ? showEndlessChoice ? "Leave with the clear, or continue against an unbroken chain of replacement bosses." : "Five entered. Five endured." - : endlessDefeat ? `Run record: ${endlessBossKills} bosses defeated after the trio finale.` : definitions.map((boss) => boss.failure).join(" "); + ? showEndlessChoice ? "Leave with the clear, or continue against an unbroken chain of replacement bosses." : pvpMode ? `${hockeyPvp.opponentName}'s party fell first.` : "Five entered. Five endured." + : hockeyMode + ? `Run ended after ${hockey.returns} returns and ${endlessBossKills} boss kills.` + : blockbreakerMode + ? `Run record: ${blockbreaker.bricksBroken} bricks, ${blockbreaker.score.toLocaleString()} points, and ${endlessBossKills} boss kills.` + : aetherAssaultMode + ? `Run record: ${aetherAssault.score.toLocaleString()} points, wave ${aetherAssault.wave}, ${aetherAssault.kills} ships, and ${endlessBossKills} boss kills.` + : pvpMode ? `${hockeyPvp.opponentName} kept their party standing.` + : endlessDefeat ? `Run record: ${endlessBossKills} bosses defeated after the trio finale.` : definitions.map((boss) => boss.failure).join(" "); return (
    {eyebrow}

    {title}

    {copy}

    - {phase === "briefing" ? "Begin from lower display" : showEndlessChoice ? "Choose Endless Mode or Quit on lower display" : "Restart from lower display"} + {phase === "briefing" ? "Begin from lower display" : showEndlessChoice ? "Choose Endless Mode or Quit on lower display" : pvpMode ? "Choose next match from lower display" : "Restart from lower display"}
    ); } +function BlockbreakerScorePopup() { + const activityMode = useGameStore((state) => state.activityMode); + const lastBreakAt = useGameStore((state) => state.blockbreaker.lastBreakAt); + const count = useGameStore((state) => state.blockbreaker.lastBreakCount); + const award = useGameStore((state) => state.blockbreaker.lastScoreAward); + const [visibleAt, setVisibleAt] = useState(null); + + useEffect(() => { + if (activityMode !== "blockbreaker" || !Number.isFinite(lastBreakAt)) { + setVisibleAt(null); + return; + } + setVisibleAt(lastBreakAt); + const timeout = window.setTimeout(() => setVisibleAt((current) => current === lastBreakAt ? null : current), 850); + return () => window.clearTimeout(timeout); + }, [activityMode, lastBreakAt]); + + if (visibleAt === null) return null; + return
    +{award.toLocaleString()}{count} {count === 1 ? "brick" : "brick combo"}
    ; +} + +function AetherScorePopup() { + const activityMode = useGameStore((state) => state.activityMode); + const lastKillAt = useGameStore((state) => state.aetherAssault.lastKillAt); + const award = useGameStore((state) => state.aetherAssault.lastKillScore); + const multiplier = useGameStore((state) => state.aetherAssault.multiplier); + const [visibleAt, setVisibleAt] = useState(null); + + useEffect(() => { + if (activityMode !== "aether-assault" || !Number.isFinite(lastKillAt)) { + setVisibleAt(null); + return; + } + setVisibleAt(lastKillAt); + const timeout = window.setTimeout(() => setVisibleAt((current) => current === lastKillAt ? null : current), 700); + return () => window.clearTimeout(timeout); + }, [activityMode, lastKillAt]); + + if (visibleAt === null) return null; + return
    +{award.toLocaleString()}{multiplier.toFixed(2)}× streak
    ; +} + function PauseOverlay({ onExit }: { onExit?: () => void }) { const paused = useGameStore((state) => state.paused); const selection = useGameStore((state) => state.pauseSelection); @@ -170,14 +256,12 @@ function PauseOverlay({ onExit }: { onExit?: () => void }) {

    Simulation, damage, and movement are stopped.

    @@ -188,30 +272,116 @@ function PauseOverlay({ onExit }: { onExit?: () => void }) { ); } -export function TopScreen({ onExit }: { onExit?: () => void }) { +function GoalPopup() { + const activityMode = useGameStore((state) => state.activityMode); + const goalSequence = useGameStore((state) => state.hockeyPvp.goalSequence); + const lastGoalSide = useGameStore((state) => state.hockeyPvp.lastGoalSide); + const lastSeenSequence = useRef(goalSequence); + const [visibleSequence, setVisibleSequence] = useState(null); + + useEffect(() => { + if (activityMode !== "hockey-healing-pvp" || goalSequence < lastSeenSequence.current) { + lastSeenSequence.current = goalSequence; + setVisibleSequence(null); + return; + } + if (goalSequence === lastSeenSequence.current) return; + lastSeenSequence.current = goalSequence; + setVisibleSequence(goalSequence); + const timeout = window.setTimeout(() => { + setVisibleSequence((current) => current === goalSequence ? null : current); + }, 1_250); + return () => window.clearTimeout(timeout); + }, [activityMode, goalSequence]); + + if (visibleSequence === null) return null; + return ( +
    + GOAL +
    + ); +} + +function DampeningIndicator() { + const activityMode = useGameStore((state) => state.activityMode); + const localBossKills = useGameStore((state) => state.endlessBossKills); + const opponentBossKills = useGameStore((state) => state.hockeyPvp.opponentBossKills); + if (activityMode !== "hockey-healing-pvp") return null; + const percent = hockeyPvpDampeningPercent(localBossKills, opponentBossKills); + return ( +
    + Dampening{percent}% + +
    + ); +} + +export function TopScreen({ + onExit, + playerAppearance, +}: { + onExit?: () => void; + playerAppearance?: CharacterAppearanceV1; +}) { const phase = useGameStore((state) => state.phase); const bossCount = useGameStore((state) => state.additionalBosses.length + 1); const round = useGameStore((state) => state.round); const runMode = useGameStore((state) => state.runMode); + const activityMode = useGameStore((state) => state.activityMode); const endlessMode = useGameStore((state) => state.endlessMode); const endlessBossKills = useGameStore((state) => state.endlessBossKills); + const hockeyReturns = useGameStore((state) => state.hockey.returns); + const blockbreaker = useGameStore((state) => state.blockbreaker); + const aetherAssault = useGameStore((state) => state.aetherAssault); + const hockeyPvp = useGameStore((state) => state.hockeyPvp); + const time = useGameStore((state) => state.time); const setPaused = useGameStore((state) => state.setPaused); + const rpgRun = useGameStore((state) => state.rpgRun); + const rpgFocusId = useGameStore((state) => state.rpgFocusId); + const dispatchRpgAction = useGameStore((state) => state.dispatchRpgAction); + const setRpgFocusId = useGameStore((state) => state.setRpgFocusId); + const restart = useGameStore((state) => state.restart); + const hockeyMode = activityMode === "hockey-healing"; + const blockbreakerMode = activityMode === "blockbreaker"; + const aetherAssaultMode = activityMode === "aether-assault"; + const pvpMode = activityMode === "hockey-healing-pvp"; + const duration = `${Math.floor(time / 60)}:${String(Math.floor(time % 60)).padStart(2, "0")}`; return (
    }> - +
    -
    {endlessMode ? `Endless · ${endlessBossKills} kills` : runMode !== "encounter" ? `Round ${round}` : "Objective"}{endlessMode ? "Defeat bosses · replacements incoming" : bossCount === 3 ? "Defeat trio · keep five alive" : bossCount === 2 ? "Defeat both · keep five alive" : "Keep all five alive"}
    +
    {hockeyMode ? `Hockey Healing · ${hockeyReturns} returns · ${duration}` : blockbreakerMode ? `Blockbreaker · ${blockbreaker.score.toLocaleString()} pts · ${duration}` : aetherAssaultMode ? `Aether Assault · ${aetherAssault.score.toLocaleString()} pts · ${duration}` : pvpMode ? `VS ${hockeyPvp.opponentName} · ${duration}` : endlessMode ? `Endless · ${endlessBossKills} kills` : runMode !== "encounter" ? `Round ${round}` : "Objective"}{hockeyMode ? `Defend wide goal · ${endlessBossKills} boss kills` : blockbreakerMode ? `${blockbreaker.bricksBroken} bricks · ${blockbreakerTimeMultiplier(time).toFixed(1)}× · row in ${Math.max(0, blockbreaker.nextRowAt - time).toFixed(1)}s` : aetherAssaultMode ? `Wave ${aetherAssault.wave} · ${aetherAssault.ships.length} ships · ${aetherAssault.multiplier.toFixed(2)}×` : pvpMode ? `Goals ${hockeyPvp.opponentGoalsConceded}–${hockeyPvp.localGoalsConceded} · Bosses ${endlessBossKills}–${hockeyPvp.opponentBossKills}` : endlessMode ? "Defeat bosses · replacements incoming" : bossCount === 3 ? "Defeat trio · keep five alive" : bossCount === 2 ? "Defeat both · keep five alive" : "Keep all five alive"}
    + -
    WASD Move Q / E Target 1–6 Cast
    +
    WASD Move{aetherAssaultMode ? " + auto-fire" : ""} Q / E Target 1–6 Cast
    {onExit && }
    + + + + {runMode === "rpg-roguelike" && rpgRun && ( + + )}
    ); diff --git a/src/components/aetherAssaultVisuals.test.ts b/src/components/aetherAssaultVisuals.test.ts new file mode 100644 index 0000000..512e009 --- /dev/null +++ b/src/components/aetherAssaultVisuals.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { + AETHER_ARMORED_SHIP_COLORS, + AETHER_STANDARD_SHIP_COLORS, + aetherShipColor, + aetherShipColorIndex, +} from "./aetherAssaultVisuals"; + +describe("Aether Assault ship colors", () => { + it("assigns a deterministic mixed palette across a formation", () => { + const first = Array.from({ length: 6 }, (_, index) => aetherShipColor(1234, 1, index, "standard")); + const replay = Array.from({ length: 6 }, (_, index) => aetherShipColor(1234, 1, index, "standard")); + + expect(replay).toEqual(first); + expect(new Set(first).size).toBe(AETHER_STANDARD_SHIP_COLORS.length); + }); + + it("changes palette ordering between waves and keeps armored variants distinct", () => { + const waveOneIndex = aetherShipColorIndex(42, 1, 0); + const waveTwoIndex = aetherShipColorIndex(42, 2, 0); + + expect(waveTwoIndex).not.toBe(waveOneIndex); + expect(aetherShipColor(42, 1, 0, "standard")).toBe(AETHER_STANDARD_SHIP_COLORS[waveOneIndex]); + expect(aetherShipColor(42, 1, 0, "armored")).toBe(AETHER_ARMORED_SHIP_COLORS[waveOneIndex]); + }); +}); diff --git a/src/components/aetherAssaultVisuals.ts b/src/components/aetherAssaultVisuals.ts new file mode 100644 index 0000000..89c6f8f --- /dev/null +++ b/src/components/aetherAssaultVisuals.ts @@ -0,0 +1,32 @@ +import type { AetherShipKind } from "../game/aetherAssault"; + +export const AETHER_STANDARD_SHIP_COLORS = [ + "#20c8e8", + "#8a5cff", + "#f044b5", + "#31cf74", + "#f06b3c", + "#d6b91c", +] as const; + +export const AETHER_ARMORED_SHIP_COLORS = [ + "#9ef5ff", + "#d0b8ff", + "#ff9bdc", + "#9af0b8", + "#ffb088", + "#ffe976", +] as const; + +export function aetherShipColorIndex(seed: number, wave: number, shipIndex: number) { + const normalizedSeed = Math.floor(Number.isFinite(seed) ? seed : 0) >>> 0; + const mixedSeed = (normalizedSeed ^ (normalizedSeed >>> 16)) >>> 0; + const normalizedWave = Math.max(1, Math.floor(Number.isFinite(wave) ? wave : 1)); + const normalizedIndex = Math.max(0, Math.floor(Number.isFinite(shipIndex) ? shipIndex : 0)); + return (mixedSeed + (normalizedWave - 1) * 3 + normalizedIndex * 5) % AETHER_STANDARD_SHIP_COLORS.length; +} + +export function aetherShipColor(seed: number, wave: number, shipIndex: number, kind: AetherShipKind) { + const palette = kind === "armored" ? AETHER_ARMORED_SHIP_COLORS : AETHER_STANDARD_SHIP_COLORS; + return palette[aetherShipColorIndex(seed, wave, shipIndex)]; +} diff --git a/src/components/boss/bossDeathVisuals.test.ts b/src/components/boss/bossDeathVisuals.test.ts index 219b8e3..6fe4a20 100644 --- a/src/components/boss/bossDeathVisuals.test.ts +++ b/src/components/boss/bossDeathVisuals.test.ts @@ -6,6 +6,8 @@ import { BOSS_DEATH_HOLD_SECONDS, advanceBossIndicatorOpacity, bossCanTrackTarget, + bossDeathDespawnSeconds, + bossDeathHoldSeconds, bossDeathOpacity, } from "./bossDeathVisuals"; @@ -28,4 +30,11 @@ describe("boss death visuals", () => { expect(bossDeathOpacity(BOSS_DEATH_HOLD_SECONDS + BOSS_DEATH_FADE_SECONDS / 2)).toBeCloseTo(0.5); expect(bossDeathOpacity(BOSS_DEATH_DESPAWN_SECONDS)).toBe(0); }); + + it("keeps Gravehorn visible through its full authored fall", () => { + const hold = bossDeathHoldSeconds("gravehorn-triceratops"); + expect(hold).toBeGreaterThan(5.73); + expect(bossDeathOpacity(5.73, "gravehorn-triceratops")).toBe(1); + expect(bossDeathOpacity(bossDeathDespawnSeconds("gravehorn-triceratops"), "gravehorn-triceratops")).toBe(0); + }); }); diff --git a/src/components/boss/bossDeathVisuals.ts b/src/components/boss/bossDeathVisuals.ts index ccd8549..e1127c6 100644 --- a/src/components/boss/bossDeathVisuals.ts +++ b/src/components/boss/bossDeathVisuals.ts @@ -2,6 +2,8 @@ export { BOSS_DEATH_DESPAWN_SECONDS, BOSS_DEATH_FADE_SECONDS, BOSS_DEATH_HOLD_SECONDS, + bossDeathDespawnSeconds, + bossDeathHoldSeconds, bossDeathOpacity, } from "../../game/bossDeath"; diff --git a/src/components/rpgRoguelike/RpgRoomPortals.tsx b/src/components/rpgRoguelike/RpgRoomPortals.tsx new file mode 100644 index 0000000..402c1cf --- /dev/null +++ b/src/components/rpgRoguelike/RpgRoomPortals.tsx @@ -0,0 +1,171 @@ +import { useFrame } from "@react-three/fiber"; +import { useRef } from "react"; +import * as THREE from "three"; +import { ARENA_CENTER, ARENA_WALL_RADIUS } from "../../game/arena"; +import { BOSS_ARENA_PORTAL_HALF_WIDTH } from "../../game/rpgRoguelike/playSpace"; + +export interface RpgRoomPortalsProps { + entryOpen: boolean; + exitOpen: boolean; + accent: string; + wallColor: string; +} + +interface PortalDoorwayProps { + open: boolean; + accent: string; + wallColor: string; + position: readonly [number, number, number]; + rotationY: number; + hideNearCamera?: boolean; +} + +const DOOR_HEIGHT = 3.25; +const DOOR_DEPTH = 0.24; +const FRAME_DEPTH = 0.72; +const FRAME_POST_WIDTH = 0.48; +const OPEN_ANGLE = Math.PI * 0.56; +const OPEN_DAMPING = 8; +const CAMERA_HIDE_RADIUS = 3.6; + +function DoorLeaf({ side, accent, wallColor }: { + side: "left" | "right"; + accent: string; + wallColor: string; +}) { + const leafWidth = BOSS_ARENA_PORTAL_HALF_WIDTH; + const centerX = side === "left" ? leafWidth * 0.5 : -leafWidth * 0.5; + return ( + + + + + + {[-0.9, 0, 0.9].map((y) => ( + + + + + ))} + + ); +} + +function PortalDoorway({ + open, + accent, + wallColor, + position, + rotationY, + hideNearCamera = false, +}: PortalDoorwayProps) { + const root = useRef(null); + const leftHinge = useRef(null); + const rightHinge = useRef(null); + + useFrame(({ camera }, delta) => { + if (root.current) { + const distanceToCamera = Math.hypot( + camera.position.x - position[0], + camera.position.z - position[2], + ); + root.current.visible = !hideNearCamera || distanceToCamera >= CAMERA_HIDE_RADIUS; + } + if (!leftHinge.current || !rightHinge.current) return; + const reducedMotion = typeof document !== "undefined" + && document.documentElement.classList.contains("force-reduced-motion"); + const leftTarget = open ? OPEN_ANGLE : 0; + const rightTarget = -leftTarget; + if (reducedMotion) { + leftHinge.current.rotation.y = leftTarget; + rightHinge.current.rotation.y = rightTarget; + return; + } + leftHinge.current.rotation.y = THREE.MathUtils.damp( + leftHinge.current.rotation.y, + leftTarget, + OPEN_DAMPING, + delta, + ); + rightHinge.current.rotation.y = THREE.MathUtils.damp( + rightHinge.current.rotation.y, + rightTarget, + OPEN_DAMPING, + delta, + ); + }); + + const postX = BOSS_ARENA_PORTAL_HALF_WIDTH + FRAME_POST_WIDTH * 0.5; + return ( + + {([-1, 1] as const).map((side) => ( + + + + + ))} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +/** + * Projects the linear RPG route through the boss arena: entry is the south + * portal (+Z), while the next-room exit is the north portal (-Z). + */ +export function RpgRoomPortals({ entryOpen, exitOpen, accent, wallColor }: RpgRoomPortalsProps) { + return ( + + + + + ); +} diff --git a/src/components/rpgRoguelike/RpgRunOverlay.tsx b/src/components/rpgRoguelike/RpgRunOverlay.tsx new file mode 100644 index 0000000..2c506b0 --- /dev/null +++ b/src/components/rpgRoguelike/RpgRunOverlay.tsx @@ -0,0 +1,431 @@ +import { useEffect, useRef, type KeyboardEvent } from "react"; +import { HEALER_ABILITIES } from "../../game/healers"; +import { + ABILITY_LOADOUT_SLOTS, + MAX_ACTIVE_ROSTER, + MAX_EQUIPPED_SPELLS, + PARTY_DRAFT_WAVE_COUNT, + PARTY_RECRUITS_PER_WAVE, + SPELL_DRAFT_WAVE_COUNT, + SPELL_PICKS_PER_WAVE, + rpgFocusId, +} from "../../game/rpgRoguelike"; +import { DEFAULT_CONTROLLER_GLYPHS } from "../../input/controllerGlyphs"; +import { + ChallengeObjective, + createUiContext, + currentBoss, + FocusButton, + GearCard, + PartyCard, + rewardSummary, + RoutePips, + RunHeader, + runAccentStyle, + SpellCard, + type RpgRunUiContext, + type RpgRunUiProps, +} from "./RpgRunUiShared"; +import "./rpgRoguelike.css"; + +function DraftFooter({ context, focusId, action, disabled, label, hint }: { + readonly context: RpgRunUiContext; + readonly focusId: string; + readonly action: Parameters[0]; + readonly disabled: boolean; + readonly label: string; + readonly hint: string; +}) { + return ( +
    + {hint} + + {label}{DEFAULT_CONTROLLER_GLYPHS.confirm} + +
    + ); +} + +function PartyDraft({ context }: { context: RpgRunUiContext }) { + const { run } = context; + const draft = run.partyDraft!; + const canRemove = draft.waveIndex > 0; + const canRecruit = run.roster.length < MAX_ACTIVE_ROSTER + && draft.recruitedThisWaveIds.length < PARTY_RECRUITS_PER_WAVE; + const finalWave = draft.waveIndex === PARTY_DRAFT_WAVE_COUNT - 1; + const canContinue = finalWave + ? run.roster.length === MAX_ACTIVE_ROSTER && run.roster.some((member) => member.role === "Tank") + : run.roster.length + (PARTY_DRAFT_WAVE_COUNT - draft.waveIndex - 1) * PARTY_RECRUITS_PER_WAVE >= MAX_ACTIVE_ROSTER; + return ( +
    +
    +
    Draft {draft.waveIndex + 1} / {PARTY_DRAFT_WAVE_COUNT}

    Assemble your party

    +

    Recruit up to {PARTY_RECRUITS_PER_WAVE} this wave. Four companions enter each room.

    + {draft.recruitedThisWaveIds.length}/{PARTY_RECRUITS_PER_WAVE} picked +
    +
    + {draft.offers.map((candidate) => { + const recruited = run.roster.some((member) => member.instanceId === candidate.candidateId); + const disabled = !recruited && !canRecruit; + const action = recruited + ? { type: "party-remove", memberId: candidate.candidateId } as const + : { type: "party-recruit", candidateId: candidate.candidateId } as const; + return ( + Party full : ( + {recruited ? canRemove ? "Remove" : "Locked" : "Recruit"} + )} + /> + ); + })} +
    +
    + Party {run.roster.length}/{MAX_ACTIVE_ROSTER} + {run.roster.map((member) => ( + {member.name}{member.className}× + ))} + {Array.from({ length: Math.max(0, MAX_ACTIVE_ROSTER - run.roster.length) }, (_, index) => Open)} +
    + +
    + ); +} + +function SpellDraft({ context }: { context: RpgRunUiContext }) { + const { run } = context; + const draft = run.spellDraft!; + const canRemove = draft.waveIndex > 0; + const canPick = run.selectedSpellIds.length < MAX_EQUIPPED_SPELLS + && draft.pickedThisWaveIds.length < SPELL_PICKS_PER_WAVE; + const finalWave = draft.waveIndex === SPELL_DRAFT_WAVE_COUNT - 1; + const canContinue = !finalWave || run.selectedSpellIds.length > 0; + return ( +
    +
    +
    Draft {draft.waveIndex + 1} / {SPELL_DRAFT_WAVE_COUNT}

    Build your spellbook

    +

    Learn up to {SPELL_PICKS_PER_WAVE} spells. Abilities from every enabled healer class can mix.

    + {draft.pickedThisWaveIds.length}/{SPELL_PICKS_PER_WAVE} picked +
    +
    + {draft.offers.map((spellId) => { + const selected = run.selectedSpellIds.includes(spellId); + const disabled = !selected && !canPick; + const action = selected + ? { type: "spell-remove", spellId } as const + : { type: "spell-pick", spellId } as const; + return ( + Wave cap reached : ( + {selected ? canRemove ? "Remove" : "Locked" : "Learn"} + )} + /> + ); + })} +
    +
    + Spellbook {run.selectedSpellIds.length}/{MAX_EQUIPPED_SPELLS} + {ABILITY_LOADOUT_SLOTS.map((slotId, index) => ({ slotId, index, spellId: run.abilityLoadout[slotId] })) + .filter((entry): entry is { slotId: typeof ABILITY_LOADOUT_SLOTS[number]; index: number; spellId: NonNullable } => Boolean(entry.spellId)) + .map(({ spellId, index }) => { + const spell = HEALER_ABILITIES[spellId]; + return ( + {index + 1}{spell.icon}{spell.shortName}× + ); + })} +
    + 0 ? "Remove drafted spells to make room for new magic." : "Each learned spell fills the next ability slot."} + /> +
    + ); +} + +function Briefing({ context, kind }: { context: RpgRunUiContext; kind: "challenge" | "boss" }) { + const { run } = context; + const boss = currentBoss(run); + if (kind === "challenge") { + return ( +
    + + Hallway challenge +

    Prove the party before the next door

    + +

    Failure does not end the run. Success adds gold and improves the next chest.

    + + Start Challenge {DEFAULT_CONTROLLER_GLYPHS.confirm} + +
    + ); + } + return ( +
    + {boss?.icon ?? "♛"} + {run.bossIndex === run.bossRoute.length - 1 ? "Final encounter" : "Boss chamber"} +

    {boss?.name ?? "Unknown Guardian"}

    +

    {boss?.title}

    +

    {boss?.briefing}

    + {run.lastChallengeResult && ( +
    + {run.lastChallengeResult.succeeded ? `Challenge cleared · +${run.lastChallengeResult.objective.rewardCurrency} gold · upgraded chest` : "Challenge missed · standard chest remains"} +
    + )} + + Enter Chamber {DEFAULT_CONTROLLER_GLYPHS.confirm} + +
    + ); +} + +function BossCleared({ context }: { context: RpgRunUiContext }) { + const boss = currentBoss(context.run); + return ( +
    +
    North gate open
    + Boss defeated +

    {boss?.name} has fallen

    +

    Walk through the far door to claim the chest. Party health carries forward.

    + + Open Chest {DEFAULT_CONTROLLER_GLYPHS.confirm} + +
    + ); +} + +function Rewards({ context }: { context: RpgRunUiContext }) { + const chest = context.run.pendingReward; + if (!chest) return null; + return ( +
    +
    +
    Chest quality +{chest.quality}

    Choose one reward

    +

    Rewards improve this run only. Gear auto-equips when stronger.

    +
    +
    + {chest.choices.map((choice) => { + const summary = rewardSummary(choice); + return ( + + {summary.icon}{summary.eyebrow}

    {choice.label}

    {summary.detail}

    Claim +
    + ); + })} +
    +
    ← / → Choose {DEFAULT_CONTROLLER_GLYPHS.confirm} Claim
    +
    + ); +} + +function Shop({ context }: { context: RpgRunUiContext }) { + const { run } = context; + const shop = run.shop; + if (!shop) return null; + const needsRest = run.playerHp > 0 && run.playerHp < 100 + || run.roster.some((member) => member.hp > 0 && member.hp < member.stats.maxHp); + const dead = run.roster.filter((member) => member.hp <= 0); + return ( +
    +
    +
    Safe intermission

    Wayfarer's Exchange

    +

    Run gear auto-equips when stronger. Sell displaced gear from the bag.

    +
    +
    +

    Buy gear

    + {shop.offers.map((offer) => { + const disabled = offer.sold || run.currency < offer.price; + return ( + {offer.sold ? "Sold" : run.currency < offer.price ? "Need gold" : "Buy"} + } /> + ); + })} +
    +

    Bag & services

    + {run.bag.length ? run.bag.map((item) => ( + + {item.slotId} · +{item.enhancement}{item.name}Sell ◆ {item.sellPrice} + + )) :

    Bag empty. Replaced gear appears here.

    } + + Living membersRest party◆ {shop.restCost} + + {dead.map((member) => ( + + Return at 50% HPRevive {member.name}◆ {shop.reviveCost} + + ))} +
    +
    + +
    + ); +} + +function Terminal({ context }: { context: RpgRunUiContext }) { + const victory = context.run.phase === "victory"; + return ( +
    + {victory ? "♛" : "◇"} + {victory ? "Expedition complete" : "Run ended"} +

    {victory ? "The gauntlet is conquered" : "The party has fallen"}

    +

    {context.run.bossesDefeated} bosses defeated · {context.run.selectedSpellIds.length} spells drafted · ◆ {context.run.currency} remaining

    +
    + New Run + Mode Select +
    +
    + ); +} + +function CompactCombatHud({ context }: { context: RpgRunUiContext }) { + const { run } = context; + const boss = currentBoss(run); + const cleared = run.phase === "boss-cleared"; + return ( +
    +
    + {run.phase === "challenge-active" ? "Hallway challenge" : cleared ? "Room cleared" : "RPG Roguelike"} + {run.phase === "challenge-active" ? run.currentChallenge?.objective.name : cleared ? "North gate open · cross it or confirm" : boss?.name} +
    + {run.phase === "challenge-active" ? : } +
    + ); +} + +/** Main-display overlay. Draft/reward/shop phases gate play; live phases render compact HUD only. */ +export function RpgRunOverlay(props: RpgRunUiProps) { + const context = createUiContext(props); + const live = props.run.phase === "challenge-active" || props.run.phase === "boss-combat" || props.run.phase === "boss-cleared"; + const overlayRef = useRef(null); + const previousFocusRef = useRef(null); + + useEffect(() => { + if (live) return; + previousFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null; + return () => { + const previous = previousFocusRef.current; + if (previous?.isConnected) previous.focus({ preventScroll: true }); + previousFocusRef.current = null; + }; + }, [live]); + + useEffect(() => { + if (live) return; + const frame = requestAnimationFrame(() => { + const selected = overlayRef.current?.querySelector("[data-rpg-focus].is-controller-selected:not(:disabled)"); + selected?.focus({ preventScroll: true }); + }); + return () => cancelAnimationFrame(frame); + }, [context.activeFocusId, live, props.run.phase]); + + const trapModalTab = (event: KeyboardEvent) => { + if (live || event.key !== "Tab") return; + const buttons = [...(overlayRef.current?.querySelectorAll("button:not(:disabled)") ?? [])]; + if (!buttons.length) { + event.preventDefault(); + overlayRef.current?.focus({ preventScroll: true }); + return; + } + const currentIndex = buttons.indexOf(document.activeElement as HTMLButtonElement); + const nextIndex = event.shiftKey + ? currentIndex <= 0 ? buttons.length - 1 : currentIndex - 1 + : currentIndex < 0 || currentIndex === buttons.length - 1 ? 0 : currentIndex + 1; + event.preventDefault(); + buttons[nextIndex].focus({ preventScroll: true }); + }; + return ( +
    + {live ? : ( + <> + + + {props.run.phase === "party-draft" && } + {props.run.phase === "spell-draft" && } + {props.run.phase === "challenge-briefing" && } + {props.run.phase === "boss-briefing" && } + {props.run.phase === "reward" && } + {props.run.phase === "shop" && } + {(props.run.phase === "victory" || props.run.phase === "defeat") && } + + )} +
    + ); +} diff --git a/src/components/rpgRoguelike/RpgRunTacticalPanel.tsx b/src/components/rpgRoguelike/RpgRunTacticalPanel.tsx new file mode 100644 index 0000000..b5bbaec --- /dev/null +++ b/src/components/rpgRoguelike/RpgRunTacticalPanel.tsx @@ -0,0 +1,410 @@ +import type { CSSProperties } from "react"; +import { HEALER_ABILITIES } from "../../game/healers"; +import { ABILITY_CONTROLLER_BINDINGS } from "../../game/controllerBindings"; +import { + ABILITY_LOADOUT_SLOTS, + MAX_ACTIVE_ROSTER, + MAX_EQUIPPED_SPELLS, + PARTY_DRAFT_WAVE_COUNT, + PARTY_RECRUITS_PER_WAVE, + SPELL_DRAFT_WAVE_COUNT, + SPELL_PICKS_PER_WAVE, + rpgFocusId, +} from "../../game/rpgRoguelike"; +import { DEFAULT_CONTROLLER_GLYPHS } from "../../input/controllerGlyphs"; +import { + ChallengeObjective, + createUiContext, + currentBoss, + FocusButton, + GearCard, + PartyCard, + rewardSummary, + RoutePips, + RunHeader, + runAccentStyle, + SpellCard, + type RpgRunUiContext, + type RpgLiveCombatUi, + type RpgRunUiProps, +} from "./RpgRunUiShared"; +import "./rpgRoguelike.css"; + +function TacticalParty({ context, interactive = false }: { context: RpgRunUiContext; interactive?: boolean }) { + const { run } = context; + return ( +
    +

    Party

    {run.roster.filter((member) => member.hp > 0).length}/{run.roster.length} standing
    +
    + {run.roster.map((member) => ( + Remove + ) : undefined} /> + ))} + {!run.roster.length &&

    Drafted companions appear here.

    } +
    +
    + ); +} + +function TacticalSpellbook({ context, interactive = false }: { context: RpgRunUiContext; interactive?: boolean }) { + const { run } = context; + return ( +
    +

    Spellbook

    {run.selectedSpellIds.length}/{MAX_EQUIPPED_SPELLS} slots
    +
    + {ABILITY_LOADOUT_SLOTS.map((slotId, index) => ({ slotId, index, spellId: run.abilityLoadout[slotId] })) + .filter((entry): entry is { slotId: typeof ABILITY_LOADOUT_SLOTS[number]; index: number; spellId: NonNullable } => Boolean(entry.spellId)) + .map(({ spellId, index }) => { + const spell = HEALER_ABILITIES[spellId]; + return interactive ? ( + {index + 1}{spell.icon}{spell.shortName}Rank {run.spellRanks[spellId] ?? 0}× + ) : ( +
    + {index + 1}{spell.icon}{spell.shortName}Rank {run.spellRanks[spellId] ?? 0} +
    + ); + })} + {!run.selectedSpellIds.length &&

    Drafted spells appear here.

    } +
    +
    + ); +} + +function TacticalContinue({ context, focusId, action, label, disabled = false }: { + readonly context: RpgRunUiContext; + readonly focusId: string; + readonly action: Parameters[0]; + readonly label: string; + readonly disabled?: boolean; +}) { + return ( + + {label}{DEFAULT_CONTROLLER_GLYPHS.confirm} + + ); +} + +function TacticalPartyDraft({ context }: { context: RpgRunUiContext }) { + const { run } = context; + const draft = run.partyDraft!; + const canRemove = draft.waveIndex > 0; + const canRecruit = run.roster.length < MAX_ACTIVE_ROSTER && draft.recruitedThisWaveIds.length < PARTY_RECRUITS_PER_WAVE; + const finalWave = draft.waveIndex === PARTY_DRAFT_WAVE_COUNT - 1; + const canContinue = finalWave + ? run.roster.length === MAX_ACTIVE_ROSTER && run.roster.some((member) => member.role === "Tank") + : run.roster.length + (PARTY_DRAFT_WAVE_COUNT - draft.waveIndex - 1) * PARTY_RECRUITS_PER_WAVE >= MAX_ACTIVE_ROSTER; + return ( + <> +
    +

    Offers · wave {draft.waveIndex + 1}/{PARTY_DRAFT_WAVE_COUNT}

    {draft.recruitedThisWaveIds.length}/{PARTY_RECRUITS_PER_WAVE} picked
    +
    + {draft.offers.map((candidate) => { + const recruited = run.roster.some((member) => member.instanceId === candidate.candidateId); + const disabled = (recruited && !canRemove) || (!recruited && !canRecruit); + return ( + + {candidate.role === "Tank" ? "⬡" : "⚔"} + {candidate.rarity} · {candidate.role}{candidate.name}{candidate.className} + HP {candidate.stats.maxHp}ST {candidate.stats.singleTarget.toFixed(2)} · AOE {candidate.stats.areaDamage.toFixed(2)} + {recruited ? canRemove ? "Remove" : "Locked" : disabled ? "Full" : "Recruit"} + + ); + })} +
    +
    + + + + ); +} + +function TacticalSpellDraft({ context }: { context: RpgRunUiContext }) { + const { run } = context; + const draft = run.spellDraft!; + const canRemove = draft.waveIndex > 0; + const canPick = run.selectedSpellIds.length < MAX_EQUIPPED_SPELLS && draft.pickedThisWaveIds.length < SPELL_PICKS_PER_WAVE; + const finalWave = draft.waveIndex === SPELL_DRAFT_WAVE_COUNT - 1; + return ( + <> +
    +

    Spells · wave {draft.waveIndex + 1}/{SPELL_DRAFT_WAVE_COUNT}

    {draft.pickedThisWaveIds.length}/{SPELL_PICKS_PER_WAVE} picked
    +
    + {draft.offers.map((spellId) => { + const spell = HEALER_ABILITIES[spellId]; + const selected = run.selectedSpellIds.includes(spellId); + const disabled = (selected && !canRemove) || (!selected && !canPick); + return ( + + {spell.icon}{spell.targeting} · {spell.mana} mana{spell.name}{spell.description}{selected ? canRemove ? "Remove" : "Locked" : disabled ? "Full" : "Learn"} + + ); + })} +
    +
    + + + + ); +} + +function TacticalBriefing({ context }: { context: RpgRunUiContext }) { + const { run } = context; + const challenge = run.phase === "challenge-briefing"; + const boss = currentBoss(run); + return ( + <> +
    + {challenge ? "◇" : boss?.icon ?? "♛"} + {challenge ? "Hallway challenge" : "Boss chamber"} +

    {challenge ? run.currentChallenge?.objective.name : boss?.name}

    +

    {challenge ? "Success improves the next chest. Failure still opens the boss door." : boss?.briefing}

    +
    + {challenge && } + + + + ); +} + +function TacticalLiveParty({ live }: { live: RpgLiveCombatUi }) { + return ( +
    +

    Live party

    {live.party.filter((member) => member.hp > 0).length}/{live.party.length} standing
    +
    + {live.party.map((member) => { + const health = Math.max(0, Math.min(100, member.hp / Math.max(1, member.maxHp) * 100)); + const selected = member.id === live.selectedMemberId; + return ( + + ); + })} +
    +
    + ); +} + +function TacticalLiveSpellbook({ context, live }: { context: RpgRunUiContext; live: RpgLiveCombatUi }) { + const castProgress = live.activeCast + ? Math.max(0, Math.min(1, (live.time - live.activeCast.startedAt) / Math.max(0.001, live.activeCast.completesAt - live.activeCast.startedAt))) + : 0; + return ( +
    +

    Spell arsenal

    {Math.ceil(live.mana)} / {live.maxMana} mana
    +
    +
    + Verdancy{live.spellResources.verdancy}/5 + Tidal Surge{live.spellResources.tidalSurge}/2 + Conviction{live.spellResources.conviction}/3 + Chronoshards{live.spellResources.chronoshards}/3 +
    + {live.activeCast && ( +
    + Casting {HEALER_ABILITIES[live.activeCast.abilityId].name} +
    + )} +
    + {ABILITY_LOADOUT_SLOTS.map((slotId, index) => { + const spellId = context.run.abilityLoadout[slotId]; + if (!spellId) return
    {index + 1}Empty
    ; + const spell = HEALER_ABILITIES[spellId]; + const cooldown = Math.max(0, live.cooldowns[slotId] - live.time, live.globalCooldownUntil - live.time); + const selected = live.party.find((member) => member.id === live.selectedMemberId); + const disabled = !live.onCastAbility || live.activeCast !== null || cooldown > 0 || live.mana < spell.mana + || (spell.targeting === "ally" && (!selected || selected.hp <= 0)) + || (spell.pulseKind === "cleanse" && !selected?.debuffs.length); + return ( + + ); + })} +
    +
    + ); +} + +function TacticalLive({ context }: { context: RpgRunUiContext }) { + const { run } = context; + const boss = currentBoss(run); + const live = context.liveCombat; + return ( + <> + {run.phase === "challenge-active" ? : ( +
    + {boss?.icon ?? "♛"}Boss battle

    {boss?.name}

    {boss?.summary}

    +
    + )} + {live ? : } + {live ? : } +

    D-pad Target Face buttons Cast

    + + ); +} + +function TacticalCleared({ context }: { context: RpgRunUiContext }) { + return ( + <> +
    + North gate open

    Room cleared

    Cross the far doorway. Health and fallen companions carry forward.

    +
    + + + + ); +} + +function TacticalRewards({ context }: { context: RpgRunUiContext }) { + const chest = context.run.pendingReward; + if (!chest) return null; + return ( +
    +

    Choose one reward

    Chest +{chest.quality}
    +
    + {chest.choices.map((choice) => { + const summary = rewardSummary(choice); + return ( + + {summary.icon}{summary.eyebrow}{choice.label}

    {summary.detail}

    Claim +
    + ); + })} +
    +
    + ); +} + +function TacticalShop({ context }: { context: RpgRunUiContext }) { + const { run } = context; + const shop = run.shop; + if (!shop) return null; + const needsRest = run.playerHp > 0 && run.playerHp < 100 + || run.roster.some((member) => member.hp > 0 && member.hp < member.stats.maxHp); + return ( + <> +
    +

    Buy gear

    ◆ {run.currency}
    +
    + {shop.offers.map((offer) => ( + + {offer.sold ? "Sold" : "Buy"} + + } /> + ))} +
    +
    +
    +

    Bag & services

    {run.bag.length} stored
    +
    + {run.bag.map((item) => ( + + Sell gear{item.name}◆ {item.sellPrice} + + ))} + + Restore living partyRest◆ {shop.restCost} + + {run.roster.filter((member) => member.hp <= 0).map((member) => ( + + Return at 50% HPRevive {member.name}◆ {shop.reviveCost} + + ))} +
    +
    + + + ); +} + +function TacticalTerminal({ context }: { context: RpgRunUiContext }) { + const victory = context.run.phase === "victory"; + return ( +
    + {victory ? "♛" : "◇"}{victory ? "Run complete" : "Run ended"}

    {victory ? "Victory" : "Party fallen"}

    +

    {context.run.bossesDefeated} bosses · {context.run.selectedSpellIds.length} spells · ◆ {context.run.currency}

    + New run{DEFAULT_CONTROLLER_GLYPHS.confirm} + Mode select +
    + ); +} + +/** Secondary-display tactical projection. Same semantic focus IDs as top overlay. */ +export function RpgRunTacticalPanel(props: RpgRunUiProps) { + const context = createUiContext(props); + const run = props.run; + return ( + + ); +} diff --git a/src/components/rpgRoguelike/RpgRunUiShared.tsx b/src/components/rpgRoguelike/RpgRunUiShared.tsx new file mode 100644 index 0000000..11ed8d8 --- /dev/null +++ b/src/components/rpgRoguelike/RpgRunUiShared.tsx @@ -0,0 +1,325 @@ +import { useEffect, useRef, type CSSProperties, type ReactNode } from "react"; +import { BOSS_DEFINITIONS } from "../../game/bossCatalog"; +import { HEALER_ABILITIES } from "../../game/healers"; +import type { AbilitySlotId, ActiveCast, HealerAbilityId, MemberId, PartyMember } from "../../game/types"; +import type { + PartyDraftCandidate, + PartyRosterMember, + RewardChoice, + RpgRoguelikeAction, + RpgRoguelikeRunState, + RunGearItem, +} from "../../game/rpgRoguelike"; +import { BOSSES_PER_ACT, TOTAL_BOSS_COUNT } from "../../game/rpgRoguelike"; +import type { RpgUiCommand } from "../../game/rpgRoguelike"; +import { normalizeRpgFocusId } from "../../game/rpgRoguelike"; + +export interface RpgRunUiProps { + readonly run: RpgRoguelikeRunState; + readonly focusedId?: string | null; + readonly onFocusChange?: (focusId: string) => void; + readonly onAction: (action: RpgRoguelikeAction) => void; + readonly onRestartRun?: () => void; + readonly onExitRun?: () => void; + readonly className?: string; + readonly liveCombat?: RpgLiveCombatUi; +} + +export interface RpgLiveCombatUi { + readonly party: readonly PartyMember[]; + readonly selectedMemberId: MemberId; + readonly mana: number; + readonly maxMana: number; + readonly cooldowns: Readonly>; + readonly globalCooldownUntil: number; + readonly time: number; + readonly activeCast: ActiveCast | null; + readonly spellResources: { + readonly verdancy: number; + readonly tidalSurge: number; + readonly conviction: number; + readonly chronoshards: number; + }; + readonly onSelectMember?: (memberId: MemberId) => void; + readonly onCastAbility?: (abilitySlotId: AbilitySlotId) => void; +} + +export interface RpgRunUiContext extends RpgRunUiProps { + readonly activeFocusId: string | null; +} + +export function createUiContext(props: RpgRunUiProps): RpgRunUiContext { + return { ...props, activeFocusId: normalizeRpgFocusId(props.run, props.focusedId) }; +} + +export function runAccentStyle(accent: string): CSSProperties { + return { "--rpg-accent": accent } as CSSProperties; +} + +export function executeUiCommand(context: RpgRunUiContext, command: RpgUiCommand): void { + if (command.type === "run-action") context.onAction(command.action); + else if (command.type === "restart-run") context.onRestartRun?.(); + else context.onExitRun?.(); +} + +interface FocusButtonProps { + readonly context: RpgRunUiContext; + readonly focusId: string; + readonly command: RpgUiCommand; + readonly className?: string; + readonly disabled?: boolean; + readonly pressed?: boolean; + readonly style?: CSSProperties; + readonly children: ReactNode; + readonly label?: string; +} + +export function FocusButton({ + context, + focusId, + command, + className = "", + disabled = false, + pressed, + style, + children, + label, +}: FocusButtonProps) { + const selected = context.activeFocusId === focusId; + const buttonRef = useRef(null); + useEffect(() => { + const button = buttonRef.current; + if (!selected || !button) return; + let parent = button.parentElement; + while (parent && (parent.closest(".rpg-run-overlay") || parent.closest(".rpg-run-tactical"))) { + const childRect = button.getBoundingClientRect(); + const parentRect = parent.getBoundingClientRect(); + if (parent.scrollHeight > parent.clientHeight) { + if (childRect.top < parentRect.top) parent.scrollTop -= parentRect.top - childRect.top; + else if (childRect.bottom > parentRect.bottom) parent.scrollTop += childRect.bottom - parentRect.bottom; + } + if (parent.scrollWidth > parent.clientWidth) { + if (childRect.left < parentRect.left) parent.scrollLeft -= parentRect.left - childRect.left; + else if (childRect.right > parentRect.right) parent.scrollLeft += childRect.right - parentRect.right; + } + parent = parent.parentElement; + } + }, [selected]); + return ( + + ); +} + +export function phaseLabel(run: RpgRoguelikeRunState): string { + switch (run.phase) { + case "party-draft": return "Party Draft"; + case "spell-draft": return "Spell Draft"; + case "challenge-briefing": return "Hallway Challenge"; + case "challenge-active": return "Challenge Active"; + case "boss-briefing": return run.bossIndex === TOTAL_BOSS_COUNT - 1 ? "Final Boss" : "Boss Door"; + case "boss-combat": return run.bossIndex === TOTAL_BOSS_COUNT - 1 ? "Final Battle" : "Boss Battle"; + case "boss-cleared": return "Room Cleared"; + case "reward": return "Reward Chest"; + case "shop": return `Act ${run.shop?.act ?? 1} Intermission`; + case "victory": return "Run Complete"; + case "defeat": return "Party Fallen"; + } +} + +export function routeLabel(run: RpgRoguelikeRunState): string { + if (run.bossIndex >= TOTAL_BOSS_COUNT - 1) return "Finale"; + return `Act ${Math.floor(run.bossIndex / BOSSES_PER_ACT) + 1} · Room ${(run.bossIndex % BOSSES_PER_ACT) + 1}`; +} + +export function currentBoss(run: RpgRoguelikeRunState) { + const bossId = run.bossRoute[run.bossIndex]; + return bossId ? BOSS_DEFINITIONS[bossId] : null; +} + +export function RunHeader({ run, compact = false }: { run: RpgRoguelikeRunState; compact?: boolean }) { + return ( +
    +
    + {routeLabel(run)} + {phaseLabel(run)} +
    +
    + {run.currency} + {Math.ceil(run.playerHp)} + {run.bossesDefeated}/{TOTAL_BOSS_COUNT} +
    +
    + ); +} + +export function RoutePips({ run }: { run: RpgRoguelikeRunState }) { + return ( +
    + {run.bossRoute.map((bossId, index) => ( + + ))} +
    + ); +} + +function titleCase(value: string): string { + return value.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" "); +} + +export function PartyCard({ + member, + candidate, + selected = false, + compact = false, + action, +}: { + readonly member?: PartyRosterMember; + readonly candidate?: PartyDraftCandidate; + readonly selected?: boolean; + readonly compact?: boolean; + readonly action?: ReactNode; +}) { + const entry = member ?? candidate; + if (!entry) return null; + const hp = member?.hp ?? entry.stats.maxHp; + const hpPercent = Math.max(0, Math.min(100, (hp / entry.stats.maxHp) * 100)); + return ( +
    +
    {entry.rarity}{entry.role}
    +

    {entry.name}

    +

    {entry.className}

    + {!compact && ( +
    + HP{entry.stats.maxHp} + ST{entry.stats.singleTarget.toFixed(2)} + AOE{entry.stats.areaDamage.toFixed(2)} + DEF{entry.stats.defense.toFixed(2)} +
    + )} + {member && ( +
    + + {hp <= 0 ? "Fallen" : `${Math.ceil(hp)} / ${entry.stats.maxHp}`} +
    + )} + {!compact && entry.traitIds.length > 0 &&
    {entry.traitIds.map(titleCase).join(" · ")}
    } + {action} +
    + ); +} + +export function SpellCard({ + spellId, + rank = 0, + selected = false, + compact = false, + action, +}: { + readonly spellId: HealerAbilityId; + readonly rank?: number; + readonly selected?: boolean; + readonly compact?: boolean; + readonly action?: ReactNode; +}) { + const spell = HEALER_ABILITIES[spellId]; + return ( +
    + {spell.icon} +
    + {spell.targeting} · {spell.mana} mana{rank > 0 ? ` · Rank ${rank}` : ""} +

    {spell.name}

    + {!compact &&

    {spell.description}

    } +
    + {action} +
    + ); +} + +export function gearOwnerName(run: RpgRoguelikeRunState, item: RunGearItem): string { + if (item.ownerId === "player") return "Healer"; + return run.roster.find((member) => member.instanceId === item.ownerId)?.name ?? "Companion"; +} + +export function GearCard({ run, item, price, action }: { + readonly run: RpgRoguelikeRunState; + readonly item: RunGearItem; + readonly price?: number; + readonly action?: ReactNode; +}) { + return ( +
    + {item.slotId === "weapon" ? "⚔" : item.slotId === "armor" ? "⬡" : "◇"} +
    + {gearOwnerName(run, item)} · {item.slotId} +

    {item.name}

    +

    +{item.statValue} {titleCase(item.statId)}{price !== undefined ? ` · ◆ ${price}` : ""}

    +
    + {action} +
    + ); +} + +export function rewardSummary(choice: RewardChoice): { icon: string; eyebrow: string; detail: string; accent: string } { + if (choice.kind === "spell-rank") { + const spell = HEALER_ABILITIES[choice.spellId]; + return { icon: spell.icon, eyebrow: "Spell upgrade", detail: `${spell.name} reaches rank ${choice.nextRank}.`, accent: spell.color }; + } + if (choice.kind === "member-rarity") { + return { icon: "♟", eyebrow: "Party upgrade", detail: `Promote companion to ${choice.nextRarity}.`, accent: "#b47aec" }; + } + if (choice.kind === "run-gear") { + return { icon: "◇", eyebrow: `Gear +${choice.item.enhancement}`, detail: `Equip ${choice.item.name}; displaced gear moves to bag.`, accent: "#65aef2" }; + } + return { icon: "◆", eyebrow: "Run currency", detail: `Gain ${choice.amount} gold for this run.`, accent: "#efc858" }; +} + +export function ChallengeObjective({ run, compact = false }: { run: RpgRoguelikeRunState; compact?: boolean }) { + const challenge = run.currentChallenge; + const previous = run.lastChallengeResult; + if (!challenge && !previous) return null; + const objective = challenge?.objective ?? previous!.objective; + const metrics = challenge?.metrics ?? previous!.metrics; + const current = metrics[objective.metric]; + const progress = Math.max(0, Math.min(100, (current / objective.target) * 100)); + const copy = objective.challengeId === "blockbreaker" + ? `Break ${objective.target} bricks before the board falls.` + : objective.challengeId === "hockey" + ? `Defeat ${objective.target} ${objective.target === 1 ? "boss" : "bosses"} without conceding.` + : `Defeat ${objective.target} enemies before the assault ends.`; + return ( +
    +
    Repeat tier {objective.repeatIndex + 1}{objective.name}
    + {!compact &&

    {copy}

    } +
    + + {current} / {objective.target} +
    + {!compact &&
    Success: ◆ {objective.rewardCurrency} · chest quality +{objective.chestQualityBonus}
    } +
    + ); +} diff --git a/src/components/rpgRoguelike/index.ts b/src/components/rpgRoguelike/index.ts new file mode 100644 index 0000000..5aedb71 --- /dev/null +++ b/src/components/rpgRoguelike/index.ts @@ -0,0 +1,4 @@ +export { RpgRoomPortals, type RpgRoomPortalsProps } from "./RpgRoomPortals"; +export { RpgRunOverlay } from "./RpgRunOverlay"; +export { RpgRunTacticalPanel } from "./RpgRunTacticalPanel"; +export type { RpgRunUiProps } from "./RpgRunUiShared"; diff --git a/src/components/rpgRoguelike/rpgRoguelike.css b/src/components/rpgRoguelike/rpgRoguelike.css new file mode 100644 index 0000000..d48e3b5 --- /dev/null +++ b/src/components/rpgRoguelike/rpgRoguelike.css @@ -0,0 +1,1771 @@ +.rpg-run-overlay, +.rpg-run-tactical { + --rpg-gold: #efc858; + --rpg-ink: #f3f7f4; + --rpg-muted: #9eb0aa; + --rpg-line: rgba(179, 210, 199, 0.2); + --rpg-panel: rgba(8, 19, 17, 0.96); + color: var(--rpg-ink); + font-family: "Rajdhani", "Avenir Next Condensed", sans-serif; + text-align: left; +} + +.rpg-run-overlay { + position: absolute; + inset: 0; + z-index: 34; + container: rpg-top / inline-size; + pointer-events: none; +} + +.rpg-run-overlay.is-gated { + pointer-events: auto; + background: + linear-gradient(180deg, rgba(2, 7, 6, 0.9), rgba(3, 10, 8, 0.98)), + repeating-linear-gradient(120deg, transparent 0 15px, rgba(255, 255, 255, 0.018) 15px 16px); + box-shadow: inset 0 0 80px rgba(0, 0, 0, 0.75); +} + +.rpg-run-header { + height: 50px; + padding: 8px 16px 6px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + border-bottom: 1px solid var(--rpg-line); + background: linear-gradient(180deg, rgba(11, 28, 24, 0.98), rgba(6, 16, 14, 0.92)); +} + +.rpg-run-header > div:first-child { + min-width: 0; + display: grid; +} + +.rpg-run-header small, +.rpg-phase-title small, +.rpg-centered-panel > small, +.rpg-cleared-panel > small, +.rpg-tactical-hero > small, +.rpg-tactical-terminal > small { + color: var(--rpg-gold); + font-size: 9px; + font-weight: 700; + letter-spacing: 0.16em; + line-height: 1.1; + text-transform: uppercase; +} + +.rpg-run-header strong { + overflow: hidden; + color: var(--rpg-ink); + font-family: "Cinzel", Georgia, serif; + font-size: 17px; + letter-spacing: 0.04em; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-run-resources { + display: flex; + align-items: center; + gap: 8px; +} + +.rpg-run-resources span { + min-width: 70px; + padding: 5px 9px; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + color: #dce8e2; + border: 1px solid var(--rpg-line); + border-radius: 4px; + background: rgba(3, 10, 8, 0.68); + font-size: 12px; + font-weight: 700; +} + +.rpg-run-resources i { + color: var(--rpg-gold); + font-style: normal; +} + +.rpg-route-pips { + height: 18px; + padding: 5px 16px; + display: grid; + grid-template-columns: repeat(10, minmax(0, 1fr)); + gap: 5px; + background: rgba(2, 8, 7, 0.9); +} + +.rpg-route-pips i { + height: 3px; + border-radius: 2px; + background: rgba(179, 210, 199, 0.15); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03); +} + +.rpg-route-pips i.is-cleared { + background: #65ba8d; + box-shadow: 0 0 6px rgba(101, 186, 141, 0.45); +} + +.rpg-route-pips i.is-current { + height: 5px; + margin-top: -1px; + background: var(--rpg-gold); + box-shadow: 0 0 8px rgba(239, 200, 88, 0.58); +} + +.rpg-route-pips i.is-finale { + clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); +} + +.rpg-phase-panel { + position: absolute; + inset: 68px 16px 10px; + min-height: 0; +} + +.rpg-phase-title { + min-height: 50px; + display: grid; + grid-template-columns: minmax(220px, 1fr) minmax(250px, 1.6fr) auto; + align-items: center; + gap: 14px; +} + +.rpg-phase-title h2, +.rpg-centered-panel h2, +.rpg-cleared-panel h2, +.rpg-tactical-hero h2, +.rpg-tactical-terminal h2 { + margin: 1px 0 0; + color: var(--rpg-ink); + font-family: "Cinzel", Georgia, serif; + font-size: 21px; + letter-spacing: 0.035em; + line-height: 1.1; +} + +.rpg-phase-title p { + margin: 0; + color: var(--rpg-muted); + font-size: 12px; + line-height: 1.25; +} + +.rpg-phase-title > b { + padding: 5px 9px; + color: var(--rpg-gold); + border: 1px solid rgba(239, 200, 88, 0.28); + border-radius: 4px; + background: rgba(239, 200, 88, 0.07); + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.rpg-party-draft, +.rpg-spell-draft, +.rpg-reward-panel, +.rpg-shop-panel { + display: flex; + flex-direction: column; +} + +.rpg-party-offer-grid { + flex: 1; + min-height: 0; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 7px; +} + +.rpg-party-card, +.rpg-spell-card, +.rpg-gear-card { + position: relative; + overflow: hidden; + min-width: 0; + margin: 0; + border: 1px solid color-mix(in srgb, var(--rpg-accent, #86a39a) 42%, transparent); + border-radius: 6px; + background: + linear-gradient(145deg, color-mix(in srgb, var(--rpg-accent, #86a39a) 10%, transparent), transparent 42%), + rgba(9, 22, 19, 0.92); + box-shadow: inset 0 1px rgba(255, 255, 255, 0.035); +} + +.rpg-party-card { + padding: 9px 9px 7px; + display: flex; + flex-direction: column; +} + +.rpg-party-card::before { + content: ""; + position: absolute; + inset: 0 0 auto; + height: 2px; + background: var(--rpg-accent); + opacity: 0.8; +} + +.rpg-party-card.rarity-white { --rarity-color: #e7e9ec; } +.rpg-party-card.rarity-green, +.rpg-picked-chip.rarity-green { --rarity-color: #70d886; } +.rpg-party-card.rarity-blue, +.rpg-picked-chip.rarity-blue { --rarity-color: #65aef2; } +.rpg-party-card.rarity-purple, +.rpg-picked-chip.rarity-purple { --rarity-color: #b47aec; } +.rpg-party-card.rarity-gold, +.rpg-picked-chip.rarity-gold { --rarity-color: #efc858; } + +.rpg-party-card.is-picked { + border-color: var(--rarity-color, var(--rpg-gold)); + background: + linear-gradient(145deg, color-mix(in srgb, var(--rarity-color, var(--rpg-gold)) 18%, transparent), transparent 52%), + rgba(10, 25, 21, 0.96); +} + +.rpg-card-kicker { + display: flex; + justify-content: space-between; + gap: 5px; + color: var(--rarity-color, #e7e9ec); + font-size: 8px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.rpg-card-kicker b { + color: var(--rpg-muted); + font-weight: 600; +} + +.rpg-party-card h3, +.rpg-spell-card h3, +.rpg-gear-card h3 { + overflow: hidden; + margin: 6px 0 0; + color: #fff; + font-family: "Cinzel", Georgia, serif; + font-size: 15px; + line-height: 1.1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-party-card > p, +.rpg-spell-card p, +.rpg-gear-card p { + margin: 2px 0 0; + color: var(--rpg-muted); + font-size: 10px; + line-height: 1.2; +} + +.rpg-stat-row { + margin-top: 8px; + padding-top: 7px; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px; + border-top: 1px solid var(--rpg-line); +} + +.rpg-stat-row span { + display: flex; + align-items: center; + justify-content: space-between; + gap: 4px; + color: #dce8e2; + font-size: 10px; + font-weight: 700; +} + +.rpg-stat-row small { + color: #71857d; + font-size: 7px; + letter-spacing: 0.08em; +} + +.rpg-party-card > footer { + margin-top: auto; + padding: 7px 0 25px; + color: color-mix(in srgb, var(--rpg-accent) 72%, white); + font-size: 8px; + line-height: 1.25; +} + +.rpg-vital-bar { + position: relative; + height: 12px; + margin-top: 7px; + overflow: hidden; + border-radius: 2px; + background: rgba(0, 0, 0, 0.52); +} + +.rpg-vital-bar > i { + position: absolute; + inset: 0 auto 0 0; + background: linear-gradient(90deg, #3d9f6d, #75d79d); +} + +.rpg-vital-bar > small { + position: absolute; + inset: 0; + color: #f0fff5; + font-size: 8px; + font-weight: 700; + line-height: 12px; + text-align: center; + text-shadow: 0 1px 2px #000; +} + +.rpg-card-action { + position: absolute; + inset: 0; + z-index: 2; + padding: 0; + cursor: pointer; + border: 2px solid transparent; + border-radius: inherit; + background: transparent; +} + +.rpg-card-action > span, +.rpg-card-status { + position: absolute; + right: 5px; + bottom: 5px; + padding: 3px 7px; + color: #06100d; + border-radius: 3px; + background: var(--rpg-gold); + font-size: 8px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.rpg-card-status { + color: #91a29c; + background: rgba(5, 12, 10, 0.88); +} + +.rpg-card-action.is-controller-selected, +.rpg-run-overlay button.is-controller-selected, +.rpg-run-tactical button.is-controller-selected { + outline: none; + border-color: var(--rpg-gold); + box-shadow: 0 0 0 1px rgba(239, 200, 88, 0.34), 0 0 14px rgba(239, 200, 88, 0.2); +} + +.rpg-picked-strip, +.rpg-spellbook-strip { + min-height: 48px; + padding: 5px 0; + display: flex; + align-items: stretch; + gap: 5px; +} + +.rpg-picked-strip > strong, +.rpg-spellbook-strip > strong { + width: 68px; + display: grid; + align-content: center; + color: var(--rpg-muted); + font-size: 9px; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.rpg-picked-chip, +.rpg-empty-chip, +.rpg-spellbook-chip { + min-width: 0; + flex: 1; + border: 1px solid var(--rpg-line); + border-radius: 4px; + background: rgba(10, 24, 20, 0.9); +} + +.rpg-picked-chip { + position: relative; + padding: 4px 18px 4px 10px; + display: grid; + align-content: center; + cursor: pointer; + text-align: left; +} + +.rpg-picked-chip > i { + position: absolute; + inset: 0 auto 0 0; + width: 3px; +} + +.rpg-picked-chip > small { + overflow: hidden; + color: var(--rpg-muted); + font-size: 8px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-picked-chip > b { + position: absolute; + right: 6px; + color: #82958e; +} + +.rpg-empty-chip { + display: grid; + place-items: center; + color: #52645d; + border-style: dashed; + font-size: 9px; + font-style: normal; +} + +.rpg-draft-footer { + min-height: 43px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + border-top: 1px solid var(--rpg-line); +} + +.rpg-draft-footer > span { + overflow: hidden; + color: var(--rpg-muted); + font-size: 10px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-primary-action, +.rpg-secondary-action { + min-width: 160px; + min-height: 34px; + padding: 7px 13px; + display: flex; + align-items: center; + justify-content: center; + gap: 14px; + cursor: pointer; + border: 1px solid rgba(239, 200, 88, 0.55); + border-radius: 4px; + background: linear-gradient(180deg, #d7b650, #9d7925); + color: #07100d; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.rpg-secondary-action { + border-color: var(--rpg-line); + background: rgba(10, 25, 21, 0.88); + color: #d6e4de; +} + +.rpg-primary-action:disabled, +.rpg-secondary-action:disabled, +.rpg-run-overlay button:disabled, +.rpg-run-tactical button:disabled { + cursor: default; + filter: saturate(0.25); + opacity: 0.43; +} + +.rpg-spell-offer-grid { + flex: 1; + min-height: 0; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 9px; +} + +.rpg-spell-card { + padding: 13px 13px 32px 54px; +} + +.rpg-spell-card::before { + content: ""; + position: absolute; + inset: 0 auto 0 0; + width: 3px; + background: var(--rpg-accent); +} + +.rpg-spell-card.is-picked { + border-color: var(--rpg-accent); + background: linear-gradient(145deg, color-mix(in srgb, var(--rpg-accent) 17%, transparent), transparent 60%), rgba(9, 22, 19, 0.96); +} + +.rpg-spell-icon { + position: absolute; + top: 12px; + left: 12px; + width: 32px; + height: 32px; + display: grid; + place-items: center; + color: var(--rpg-accent); + border: 1px solid color-mix(in srgb, var(--rpg-accent) 45%, transparent); + border-radius: 50%; + background: color-mix(in srgb, var(--rpg-accent) 10%, #07110f); + font-size: 18px; + font-style: normal; +} + +.rpg-spell-card h3 { + margin-top: 3px; +} + +.rpg-spell-card > div > small { + color: var(--rpg-accent); + font-size: 8px; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +.rpg-spell-card p { + margin-top: 8px; + font-size: 11px; + line-height: 1.32; +} + +.rpg-spellbook-strip { + min-height: 52px; +} + +.rpg-spellbook-chip { + padding: 4px 6px; + display: grid; + grid-template-columns: auto auto minmax(0, 1fr) auto; + align-items: center; + gap: 4px; + cursor: pointer; + color: #dce8e2; +} + +.rpg-spellbook-chip > b { + color: #71857d; + font-size: 8px; +} + +.rpg-spellbook-chip > i { + color: var(--rpg-accent); + font-style: normal; +} + +.rpg-spellbook-chip > span { + overflow: hidden; + font-size: 9px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-spellbook-chip > small { + color: #71857d; +} + +.rpg-centered-panel, +.rpg-cleared-panel { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +.rpg-centered-panel::before, +.rpg-cleared-panel::before { + content: ""; + position: absolute; + width: min(560px, 80%); + height: 320px; + z-index: -1; + border: 1px solid var(--rpg-line); + border-radius: 50%; + background: radial-gradient(ellipse, color-mix(in srgb, var(--rpg-accent, #70b99f) 11%, transparent), transparent 70%); +} + +.rpg-hero-sigil { + width: 58px; + height: 58px; + margin-bottom: 9px; + display: grid; + place-items: center; + color: var(--rpg-accent, var(--rpg-gold)); + border: 1px solid color-mix(in srgb, var(--rpg-accent, var(--rpg-gold)) 55%, transparent); + border-radius: 50%; + background: rgba(6, 16, 14, 0.85); + box-shadow: 0 0 28px color-mix(in srgb, var(--rpg-accent, var(--rpg-gold)) 16%, transparent); + font-size: 27px; +} + +.rpg-centered-panel > h3 { + margin: 4px 0 0; + color: var(--rpg-accent, var(--rpg-gold)); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +.rpg-centered-panel > p, +.rpg-cleared-panel > p { + width: min(540px, 85%); + margin: 9px 0; + color: var(--rpg-muted); + font-size: 12px; + line-height: 1.35; +} + +.rpg-large-action { + min-width: 210px; + min-height: 39px; + margin-top: 11px; +} + +.rpg-challenge-objective { + width: min(470px, 78%); + margin-top: 13px; + padding: 10px 12px; + color: var(--rpg-ink); + border: 1px solid var(--rpg-line); + border-radius: 5px; + background: rgba(7, 18, 15, 0.88); + text-align: left; +} + +.rpg-challenge-objective > div:first-child { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.rpg-challenge-objective small { + color: var(--rpg-gold); + font-size: 8px; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.rpg-challenge-objective strong { + font-size: 13px; +} + +.rpg-challenge-objective p, +.rpg-challenge-objective footer { + margin: 6px 0 0; + color: var(--rpg-muted); + font-size: 10px; +} + +.rpg-objective-progress { + position: relative; + height: 16px; + margin-top: 7px; + overflow: hidden; + border-radius: 3px; + background: rgba(0, 0, 0, 0.48); +} + +.rpg-objective-progress > i { + position: absolute; + inset: 0 auto 0 0; + background: linear-gradient(90deg, #4a9c7b, #77d1a8); +} + +.rpg-objective-progress > span { + position: absolute; + inset: 0; + color: white; + font-size: 9px; + font-weight: 800; + line-height: 16px; + text-align: center; + text-shadow: 0 1px 2px black; +} + +.rpg-soft-fail-copy { + margin: 7px 0 0 !important; + color: #b8c8c2 !important; + font-size: 10px !important; +} + +.rpg-challenge-result { + margin-top: 7px; + padding: 5px 9px; + border-radius: 3px; + font-size: 9px; + font-weight: 700; + letter-spacing: 0.04em; +} + +.rpg-challenge-result.is-success { + color: #9de1b8; + background: rgba(70, 167, 108, 0.15); +} + +.rpg-challenge-result.is-failure { + color: #d6b69c; + background: rgba(182, 100, 63, 0.12); +} + +.rpg-exit-arrow { + margin-bottom: 18px; + display: grid; + place-items: center; + color: var(--rpg-gold); + font-size: 28px; + line-height: 0.7; +} + +.rpg-exit-arrow i { + width: 70px; + height: 3px; + margin-bottom: 8px; + background: var(--rpg-gold); + box-shadow: 0 0 15px rgba(239, 200, 88, 0.65); +} + +.rpg-exit-arrow span { + margin-top: 10px; + font-size: 8px; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.rpg-reward-grid { + flex: 1; + min-height: 0; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 11px; + align-items: stretch; +} + +.rpg-reward-card { + position: relative; + min-width: 0; + padding: 20px 17px; + cursor: pointer; + border: 1px solid color-mix(in srgb, var(--rpg-accent) 45%, transparent); + border-radius: 7px; + background: linear-gradient(150deg, color-mix(in srgb, var(--rpg-accent) 13%, transparent), transparent 55%), rgba(8, 20, 17, 0.96); + text-align: left; +} + +.rpg-reward-card > i { + width: 42px; + height: 42px; + display: grid; + place-items: center; + color: var(--rpg-accent); + border: 1px solid color-mix(in srgb, var(--rpg-accent) 45%, transparent); + border-radius: 50%; + background: rgba(3, 10, 8, 0.5); + font-size: 22px; + font-style: normal; +} + +.rpg-reward-card > small { + display: block; + margin-top: 16px; + color: var(--rpg-accent); + font-size: 8px; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.rpg-reward-card h3 { + margin: 4px 0; + font-family: "Cinzel", Georgia, serif; + font-size: 16px; +} + +.rpg-reward-card p { + margin: 0; + color: var(--rpg-muted); + font-size: 11px; + line-height: 1.3; +} + +.rpg-reward-card > b { + position: absolute; + right: 12px; + bottom: 10px; + color: var(--rpg-accent); + font-size: 9px; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.rpg-controller-hint { + min-height: 30px; + margin: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + color: var(--rpg-muted); + font-size: 9px; +} + +.rpg-controller-hint b { + color: #dce8e2; +} + +.rpg-controller-hint i { + width: 1px; + height: 12px; + background: var(--rpg-line); +} + +.rpg-shop-layout { + flex: 1; + min-height: 0; + display: grid; + grid-template-columns: minmax(0, 1.55fr) minmax(240px, 0.9fr); + gap: 12px; +} + +.rpg-shop-layout > section { + min-height: 0; + display: flex; + flex-direction: column; +} + +.rpg-shop-layout h3 { + margin: 0 0 5px; + color: var(--rpg-muted); + font-size: 9px; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.rpg-shop-grid, +.rpg-tactical-shop-grid { + min-height: 0; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px; +} + +.rpg-gear-card { + min-height: 72px; + padding: 8px 8px 22px 43px; +} + +.rpg-gear-card > i { + position: absolute; + top: 10px; + left: 9px; + width: 26px; + height: 26px; + display: grid; + place-items: center; + color: #75bceb; + border: 1px solid rgba(101, 174, 242, 0.35); + border-radius: 4px; + font-style: normal; +} + +.rpg-gear-card h3 { + margin-top: 2px; + font-size: 11px; +} + +.rpg-gear-card small { + color: #75bceb; + font-size: 7px; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.rpg-gear-card p { + font-size: 9px; +} + +.rpg-shop-side-list { + min-height: 0; + overflow-y: auto; + display: grid; + align-content: start; + gap: 5px; + scrollbar-color: #557269 transparent; + scrollbar-width: thin; +} + +.rpg-shop-row, +.rpg-service-row { + min-height: 42px; + padding: 5px 8px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + cursor: pointer; + color: #dce8e2; + border: 1px solid var(--rpg-line); + border-radius: 4px; + background: rgba(8, 21, 18, 0.9); + text-align: left; +} + +.rpg-shop-row span, +.rpg-service-row span { + min-width: 0; + display: grid; + font-size: 10px; + font-weight: 700; +} + +.rpg-shop-row small, +.rpg-service-row small { + overflow: hidden; + color: var(--rpg-muted); + font-size: 7px; + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-shop-row > b, +.rpg-service-row > b { + flex: 0 0 auto; + color: var(--rpg-gold); + font-size: 9px; +} + +.rpg-empty-copy { + margin: 0; + padding: 9px; + color: #6f837b; + border: 1px dashed var(--rpg-line); + border-radius: 4px; + font-size: 9px; + text-align: center; +} + +.rpg-terminal-actions { + margin-top: 10px; + display: flex; + gap: 8px; +} + +.rpg-terminal.is-victory .rpg-hero-sigil, +.rpg-tactical-terminal.is-victory > i { + color: var(--rpg-gold); + box-shadow: 0 0 34px rgba(239, 200, 88, 0.22); +} + +.rpg-terminal.is-defeat .rpg-hero-sigil, +.rpg-tactical-terminal.is-defeat > i { + color: #d27567; + border-color: rgba(210, 117, 103, 0.5); +} + +.rpg-compact-combat-hud { + position: absolute; + top: 62px; + left: 50%; + width: min(440px, calc(100% - 280px)); + min-height: 40px; + padding: 6px 11px; + transform: translateX(-50%); + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + border: 1px solid rgba(179, 210, 199, 0.16); + border-radius: 5px; + background: rgba(3, 11, 9, 0.78); + backdrop-filter: blur(4px); +} + +.rpg-compact-combat-hud > div:first-child { + min-width: 0; + display: grid; +} + +.rpg-compact-combat-hud small { + color: var(--rpg-gold); + font-size: 7px; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.rpg-compact-combat-hud strong { + overflow: hidden; + font-family: "Cinzel", Georgia, serif; + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-compact-combat-hud .rpg-route-pips { + width: 180px; + height: auto; + padding: 0; + background: none; +} + +.rpg-challenge-objective.is-compact { + width: 220px; + margin: 0; + padding: 0; + border: 0; + background: none; +} + +.rpg-challenge-objective.is-compact > div:first-child { + display: none; +} + +.rpg-challenge-objective.is-compact .rpg-objective-progress { + margin: 0; +} + +/* Bottom / tactical display */ + +.rpg-run-tactical { + width: 100%; + height: 100%; + min-height: 0; + display: flex; + flex-direction: column; + container: rpg-bottom / inline-size; + background: + radial-gradient(circle at 90% 0, rgba(94, 199, 176, 0.08), transparent 42%), + #07120f; +} + +.rpg-run-tactical .rpg-run-header { + height: 47px; + flex: 0 0 47px; + padding: 6px 10px 5px; +} + +.rpg-run-tactical .rpg-run-header strong { + font-size: 14px; +} + +.rpg-run-tactical .rpg-run-resources span { + min-width: 56px; + padding: 4px 6px; + font-size: 10px; +} + +.rpg-run-tactical > .rpg-route-pips { + height: 15px; + flex: 0 0 15px; + padding: 4px 10px; +} + +.rpg-tactical-body { + min-height: 0; + flex: 1; + overflow-x: hidden; + overflow-y: auto; + padding: 8px 10px 10px; + overscroll-behavior: contain; + scrollbar-color: #48675d transparent; + scrollbar-width: thin; +} + +.rpg-tactical-section + .rpg-tactical-section, +.rpg-tactical-hero + .rpg-tactical-section, +.rpg-challenge-objective + .rpg-tactical-section { + margin-top: 8px; +} + +.rpg-tactical-section > header { + min-height: 24px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.rpg-tactical-section > header h3 { + margin: 0; + color: #dce8e2; + font-size: 10px; + letter-spacing: 0.11em; + text-transform: uppercase; +} + +.rpg-tactical-section > header span { + color: var(--rpg-gold); + font-size: 9px; +} + +.rpg-tactical-list { + display: grid; + gap: 4px; +} + +.rpg-tactical-offer { + position: relative; + min-height: 50px; + padding: 5px 61px 5px 38px; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + cursor: pointer; + color: #e7f0ec; + border: 1px solid color-mix(in srgb, var(--rpg-accent) 36%, transparent); + border-radius: 5px; + background: linear-gradient(110deg, color-mix(in srgb, var(--rpg-accent) 10%, transparent), transparent 55%), rgba(8, 21, 18, 0.92); + text-align: left; +} + +.rpg-tactical-offer > i { + position: absolute; + left: 8px; + width: 22px; + height: 22px; + display: grid; + place-items: center; + color: var(--rpg-accent); + border: 1px solid color-mix(in srgb, var(--rpg-accent) 35%, transparent); + border-radius: 50%; + font-size: 12px; + font-style: normal; +} + +.rpg-tactical-offer > span { + min-width: 0; + display: grid; +} + +.rpg-tactical-offer > span small { + color: var(--rpg-accent); + font-size: 7px; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.rpg-tactical-offer > span strong { + overflow: hidden; + font-family: "Cinzel", Georgia, serif; + font-size: 11px; + line-height: 1.15; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-tactical-offer > span em { + overflow: hidden; + color: var(--rpg-muted); + font-size: 8px; + font-style: normal; + line-height: 1.1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-tactical-offer > b { + display: grid; + color: #cddbd5; + font-size: 8px; + text-align: right; +} + +.rpg-tactical-offer > b small { + color: var(--rpg-muted); + font-size: 7px; + font-weight: 500; +} + +.rpg-tactical-offer > u { + position: absolute; + right: 6px; + min-width: 46px; + padding: 3px 5px; + color: #07100d; + border-radius: 3px; + background: var(--rpg-gold); + font-size: 7px; + font-weight: 800; + letter-spacing: 0.06em; + text-align: center; + text-decoration: none; + text-transform: uppercase; +} + +.rpg-tactical-offer[aria-pressed="true"] { + background: linear-gradient(110deg, color-mix(in srgb, var(--rpg-accent) 20%, transparent), transparent 60%), rgba(8, 21, 18, 0.96); +} + +.rpg-tactical-offer.is-spell { + min-height: 63px; + grid-template-columns: 1fr; +} + +.rpg-tactical-offer.is-spell > span em { + max-width: 420px; +} + +.rpg-tactical-party-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 4px; +} + +.rpg-party-card.is-compact { + min-height: 72px; + padding: 6px; +} + +.rpg-party-card.is-compact h3 { + margin-top: 4px; + font-size: 10px; +} + +.rpg-party-card.is-compact > p { + overflow: hidden; + font-size: 7px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-party-card.is-compact .rpg-card-kicker { + font-size: 6px; +} + +.rpg-party-card.is-compact .rpg-vital-bar { + height: 10px; + margin-top: 5px; +} + +.rpg-party-card.is-compact .rpg-vital-bar small { + font-size: 6px; + line-height: 10px; +} + +.rpg-tactical-spell-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 4px; +} + +.rpg-tactical-spell { + min-width: 0; + min-height: 40px; + padding: 4px 5px; + display: grid; + grid-template-columns: auto auto minmax(0, 1fr) auto; + align-items: center; + gap: 5px; + color: #dce8e2; + border: 1px solid color-mix(in srgb, var(--rpg-accent) 30%, transparent); + border-radius: 4px; + background: rgba(8, 21, 18, 0.9); + text-align: left; +} + +button.rpg-tactical-spell { + cursor: pointer; +} + +.rpg-tactical-spell > b { + color: #71857d; + font-size: 7px; +} + +.rpg-tactical-spell > i { + color: var(--rpg-accent); + font-size: 13px; + font-style: normal; +} + +.rpg-tactical-spell > span { + overflow: hidden; + display: grid; + font-size: 8px; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-tactical-spell > span small { + color: var(--rpg-muted); + font-size: 6px; + font-weight: 500; +} + +.rpg-tactical-spell > em { + color: #71857d; + font-style: normal; +} + +.rpg-tactical-continue { + width: 100%; + min-height: 42px; + margin-top: 8px; + padding: 7px 12px; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + color: #07100d; + border: 1px solid rgba(255, 225, 147, 0.5); + border-radius: 5px; + background: linear-gradient(180deg, #dcbc58, #a17c27); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.rpg-tactical-continue > b { + min-width: 25px; + height: 25px; + display: grid; + place-items: center; + border: 1px solid rgba(7, 16, 13, 0.35); + border-radius: 50%; +} + +.rpg-tactical-hero { + padding: 10px 12px 10px 55px; + position: relative; + min-height: 82px; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--rpg-accent, #70b99f) 35%, transparent); + border-radius: 6px; + background: linear-gradient(120deg, color-mix(in srgb, var(--rpg-accent, #70b99f) 12%, transparent), transparent 58%), rgba(8, 21, 18, 0.92); +} + +.rpg-tactical-hero > i { + position: absolute; + top: 13px; + left: 12px; + width: 32px; + height: 32px; + display: grid; + place-items: center; + color: var(--rpg-accent, var(--rpg-gold)); + border: 1px solid color-mix(in srgb, var(--rpg-accent, var(--rpg-gold)) 40%, transparent); + border-radius: 50%; + font-size: 18px; + font-style: normal; +} + +.rpg-tactical-hero h2 { + margin-top: 2px; + font-size: 15px; +} + +.rpg-tactical-hero p { + margin: 5px 0 0; + color: var(--rpg-muted); + font-size: 9px; + line-height: 1.25; +} + +.rpg-run-tactical .rpg-challenge-objective { + width: 100%; + margin-top: 0; +} + +.rpg-tactical-cleared > i { + color: var(--rpg-gold); + font-size: 24px; +} + +.rpg-tactical-rewards { + display: grid; + gap: 6px; +} + +.rpg-tactical-reward { + min-height: 94px; + padding: 9px 64px 9px 49px; + position: relative; + cursor: pointer; + color: #e7f0ec; + border: 1px solid color-mix(in srgb, var(--rpg-accent) 42%, transparent); + border-radius: 5px; + background: linear-gradient(110deg, color-mix(in srgb, var(--rpg-accent) 12%, transparent), transparent 58%), rgba(8, 21, 18, 0.92); + text-align: left; +} + +.rpg-tactical-reward > i { + position: absolute; + top: 12px; + left: 10px; + width: 30px; + height: 30px; + display: grid; + place-items: center; + color: var(--rpg-accent); + border: 1px solid color-mix(in srgb, var(--rpg-accent) 40%, transparent); + border-radius: 50%; + font-size: 16px; + font-style: normal; +} + +.rpg-tactical-reward > span { + display: grid; +} + +.rpg-tactical-reward small { + color: var(--rpg-accent); + font-size: 7px; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.rpg-tactical-reward strong { + font-family: "Cinzel", Georgia, serif; + font-size: 12px; +} + +.rpg-tactical-reward p { + margin: 4px 0 0; + color: var(--rpg-muted); + font-size: 9px; +} + +.rpg-tactical-reward > b { + position: absolute; + right: 11px; + color: var(--rpg-accent); + font-size: 8px; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.rpg-run-tactical .rpg-tactical-shop-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.rpg-run-tactical .rpg-gear-card { + min-height: 80px; +} + +.rpg-service-row { + width: 100%; +} + +.rpg-tactical-terminal { + min-height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +.rpg-tactical-terminal > i { + width: 58px; + height: 58px; + margin-bottom: 9px; + display: grid; + place-items: center; + color: var(--rpg-gold); + border: 1px solid rgba(239, 200, 88, 0.45); + border-radius: 50%; + font-size: 26px; + font-style: normal; +} + +.rpg-tactical-terminal h2 { + margin-top: 3px; + font-size: 20px; +} + +.rpg-tactical-terminal p { + margin: 7px 0 12px; + color: var(--rpg-muted); + font-size: 10px; +} + +.rpg-tactical-terminal .rpg-secondary-action { + width: 100%; + min-height: 40px; + margin-top: 6px; +} + +.rpg-live-party-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 4px; +} + +.rpg-live-party-grid > button { + min-width: 0; + min-height: 64px; + padding: 5px; + display: grid; + grid-template-columns: 24px minmax(0, 1fr); + align-items: center; + gap: 5px; + color: #dce8e2; + border: 1px solid var(--rpg-line); + border-radius: 5px; + background: rgba(8, 21, 18, 0.92); + text-align: left; +} + +.rpg-live-party-grid > button.is-selected { + border-color: var(--rpg-gold); + box-shadow: inset 0 0 0 1px rgba(239, 200, 88, 0.22), 0 0 8px rgba(239, 200, 88, 0.12); +} + +.rpg-live-party-grid > button.is-down { + filter: grayscale(0.8); + opacity: 0.5; +} + +.rpg-live-party-grid > button > i { + width: 24px; + height: 24px; + display: grid; + place-items: center; + color: #07100d; + border-radius: 50%; + background: var(--rpg-member-color); + font-size: 10px; + font-style: normal; + font-weight: 900; +} + +.rpg-live-party-grid > button > span { + min-width: 0; + display: grid; +} + +.rpg-live-party-grid strong, +.rpg-live-party-grid small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rpg-live-party-grid strong { font-size: 9px; } +.rpg-live-party-grid small { color: var(--rpg-muted); font-size: 8px; } + +.rpg-live-party-grid em { + height: 4px; + margin-top: 3px; + overflow: hidden; + border-radius: 2px; + background: rgba(255, 255, 255, 0.08); +} + +.rpg-live-party-grid em b { + height: 100%; + display: block; + background: #65ba8d; +} + +.rpg-live-party-grid u { + grid-column: 1 / -1; + color: var(--rpg-muted); + font-size: 8px; + text-align: center; + text-decoration: none; +} + +.rpg-live-resource-bar { + height: 6px; + overflow: hidden; + border-radius: 3px; + background: rgba(65, 121, 171, 0.2); +} + +.rpg-live-resource-bar i { + height: 100%; + display: block; + background: linear-gradient(90deg, #3976b6, #62a7e5); +} + +.rpg-live-resources { + margin: 4px 0; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 5px; +} + +.rpg-live-resources span { + padding: 3px 6px; + display: flex; + justify-content: space-between; + gap: 5px; + color: var(--rpg-gold); + border: 1px solid var(--rpg-line); + border-radius: 3px; + background: rgba(3, 10, 8, 0.68); + font-size: 8px; + white-space: nowrap; +} + +.rpg-live-resources b { color: var(--rpg-muted); } + +.rpg-live-cast { + margin: 4px 0; + display: grid; + gap: 2px; + color: #dce8e2; + font-size: 9px; +} + +.rpg-live-cast > i { + height: 4px; + overflow: hidden; + border-radius: 2px; + background: rgba(239, 200, 88, 0.15); +} + +.rpg-live-cast > i > em { + width: var(--rpg-cast-progress); + height: 100%; + display: block; + background: var(--rpg-gold); +} + +.rpg-live-spell-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 4px; +} + +.rpg-live-spell { + min-width: 0; + min-height: 50px; + padding: 5px; + display: grid; + grid-template-columns: 20px 22px minmax(0, 1fr) auto; + align-items: center; + gap: 4px; + color: #dce8e2; + border: 1px solid color-mix(in srgb, var(--rpg-accent, #71857d) 36%, transparent); + border-radius: 4px; + background: linear-gradient(110deg, color-mix(in srgb, var(--rpg-accent, #71857d) 8%, transparent), transparent 60%), rgba(8, 21, 18, 0.92); + text-align: left; +} + +.rpg-live-spell > b, +.rpg-live-spell > u { color: var(--rpg-gold); font-size: 8px; text-decoration: none; } +.rpg-live-spell > i { color: var(--rpg-accent); font-size: 14px; font-style: normal; } +.rpg-live-spell > span { min-width: 0; display: grid; } +.rpg-live-spell strong, +.rpg-live-spell small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.rpg-live-spell strong { font-size: 9px; } +.rpg-live-spell small { color: var(--rpg-muted); font-size: 8px; } +.rpg-live-spell.is-cooling { opacity: 0.65; } +.rpg-live-spell.is-empty { grid-template-columns: auto 1fr; color: #71857d; } + +/* Lower-panel text must remain readable at the 620x540 Android layout viewport. */ +.rpg-run-tactical small, +.rpg-tactical-offer > u, +.rpg-party-card.is-compact .rpg-card-kicker, +.rpg-tactical-spell > b { + font-size: max(8px, 1.3cqw); +} + +.large-interface-text .rpg-run-overlay small, +.large-interface-text .rpg-run-tactical small { font-size: 10px; } +.large-interface-text .rpg-run-overlay p, +.large-interface-text .rpg-run-tactical p { font-size: 12px; } +.large-interface-text .rpg-run-overlay button, +.large-interface-text .rpg-run-tactical button { font-size: 11px; } + +@container rpg-top (max-width: 720px) { + .rpg-phase-panel { + inset-inline: 10px; + } + + .rpg-phase-title { + grid-template-columns: 1fr auto; + } + + .rpg-phase-title > p { + display: none; + } + + .rpg-party-offer-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + overflow-y: auto; + } + + .rpg-party-card { + min-height: 135px; + } + + .rpg-spell-offer-grid { + grid-template-columns: 1fr; + overflow-y: auto; + } + + .rpg-spell-card { + min-height: 92px; + } + + .rpg-picked-strip, + .rpg-spellbook-strip { + overflow-x: auto; + } + + .rpg-picked-chip, + .rpg-empty-chip, + .rpg-spellbook-chip { + min-width: 100px; + } + + .rpg-shop-layout { + grid-template-columns: 1fr; + overflow-y: auto; + } +} + +@container rpg-bottom (max-width: 480px) { + .rpg-tactical-party-grid, + .rpg-tactical-spell-grid, + .rpg-run-tactical .rpg-tactical-shop-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .rpg-tactical-offer > b { + display: none; + } +} + +/* Single-display browser fallback gets a usable full-height draft surface. */ +@media (max-width: 760px) { + .top-display:has(.rpg-run-overlay.is-gated) { + height: max(480px, min(540px, calc(100dvh - 20px))); + aspect-ratio: auto; + } +} + +@media (prefers-reduced-motion: reduce) { + .rpg-run-overlay *, + .rpg-run-tactical * { + scroll-behavior: auto !important; + transition: none !important; + animation: none !important; + } +} diff --git a/src/frontend/appearanceStore.test.ts b/src/frontend/appearanceStore.test.ts new file mode 100644 index 0000000..0ed5cb7 --- /dev/null +++ b/src/frontend/appearanceStore.test.ts @@ -0,0 +1,90 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { createDefaultHealerAppearance } from "../game/healerVisuals"; +import { hasPendingSaveSync } from "./saveSync"; +import { getFrontendSnapshot, useFrontendStore } from "./store"; + +const originalState = useFrontendStore.getState(); + +afterEach(() => { + useFrontendStore.getState().deleteSlot(3); + useFrontendStore.setState({ + activeSlotId: originalState.activeSlotId, + selectedSlotId: originalState.selectedSlotId, + screen: originalState.screen, + appearanceClassId: originalState.appearanceClassId, + appearanceDrafts: structuredClone(originalState.appearanceDrafts), + previewMode: originalState.previewMode, + previewAnimation: originalState.previewAnimation, + notice: originalState.notice, + }); +}); + +describe("Appearance Lab frontend state", () => { + it("opens from saved looks, previews drafts, saves explicitly, and cancels unsaved edits", () => { + const frontend = useFrontendStore.getState(); + frontend.deleteSlot(3); + expect(frontend.createSlot(3, "Wardrobe Tester")).toBe(true); + frontend.playSlot(3); + + useFrontendStore.getState().openAppearanceLab(); + let state = useFrontendStore.getState(); + const savedPriest = state.slots[2].local!.healers.priest.appearance; + expect(state.screen).toBe("appearance"); + expect(state.appearanceClassId).toBe("priest"); + expect(state.appearanceDrafts.priest).toEqual(savedPriest); + expect(state.appearanceDrafts.priest).not.toBe(savedPriest); + expect(state.previewMode).toBe("modular"); + expect(state.previewAnimation).toBe("idle"); + + const mixedPriest = { + ...state.appearanceDrafts.priest, + headPartId: "rogue-head" as const, + }; + state.updateAppearanceDraft(mixedPriest); + state.setAppearancePreviewMode("legacy"); + state.setAppearancePreviewAnimation("cast"); + state = useFrontendStore.getState(); + expect(state.appearanceDrafts.priest.headPartId).toBe("rogue-head"); + expect(state.slots[2].local!.healers.priest.appearance.headPartId).toBe("mage-head"); + expect(state.previewMode).toBe("legacy"); + expect(state.previewAnimation).toBe("cast"); + + expect(state.saveAppearanceDraft()).toBe(true); + state = useFrontendStore.getState(); + expect(state.slots[2].local!.healers.priest.appearance.headPartId).toBe("rogue-head"); + expect(hasPendingSaveSync(3)).toBe(true); + + state.updateAppearanceDraft({ + ...state.appearanceDrafts.priest, + headPartId: "ranger-head", + }); + useFrontendStore.getState().closeAppearanceLab(); + state = useFrontendStore.getState(); + expect(state.screen).toBe("home"); + expect(state.appearanceDrafts.priest.headPartId).toBe("rogue-head"); + expect(state.slots[2].local!.healers.priest.appearance.headPartId).toBe("rogue-head"); + }); + + it("resets only the selected class and includes state but no actions in snapshots", () => { + useFrontendStore.setState((state) => ({ + appearanceClassId: "paladin", + appearanceDrafts: { + ...state.appearanceDrafts, + paladin: { ...state.appearanceDrafts.paladin, headPartId: "rogue-head" }, + }, + previewMode: "legacy", + previewAnimation: "walk", + })); + + useFrontendStore.getState().resetAppearanceDraft(); + const snapshot = getFrontendSnapshot(); + expect(snapshot.appearanceClassId).toBe("paladin"); + expect(snapshot.appearanceDrafts.paladin).toEqual(createDefaultHealerAppearance("paladin")); + expect(snapshot.previewMode).toBe("legacy"); + expect(snapshot.previewAnimation).toBe("walk"); + expect("openAppearanceLab" in snapshot).toBe(false); + expect("updateAppearanceDraft" in snapshot).toBe(false); + expect("saveAppearanceDraft" in snapshot).toBe(false); + expect("setAppearancePreviewAnimation" in snapshot).toBe(false); + }); +}); diff --git a/src/frontend/data.test.ts b/src/frontend/data.test.ts index 0fe1820..6e8db5b 100644 --- a/src/frontend/data.test.ts +++ b/src/frontend/data.test.ts @@ -1,12 +1,26 @@ import { describe, expect, it } from "vitest"; -import { buildCollections, MODE_COPY, selectRandomBoss } from "./data"; +import { buildCollections, createHunterSave, MODE_COPY, selectRandomBoss } from "./data"; +import { HEALER_CLASS_ORDER } from "../game/healers"; +import { HEALER_VISUAL_PROFILES } from "../game/healerVisuals"; import { selectRandomBossPair } from "../game/roguelike"; import { GROUP_DROP_TABLES, createEmptyCollectionLog } from "../game/progression/loot"; import { AVAILABLE_BOSS_IDS, BOSS_GROUPS } from "../game/bossCatalog"; describe("game mode configuration", () => { - it("separates randomized PVE from selectable Dungeons", () => { - expect(MODE_COPY["roguelike-pve"].title).toBe("PVE"); + it("initializes progression for every playable healer", () => { + const save = createHunterSave(1, "2026-07-16T00:00:00.000Z", "Aelia"); + expect(Object.keys(save.healers)).toEqual(HEALER_CLASS_ORDER); + expect(save.healers.paladin.inventory.length).toBeGreaterThan(0); + expect(save.healers.chronomancer.inventory.length).toBeGreaterThan(0); + for (const classId of HEALER_CLASS_ORDER) { + expect(save.healers[classId].appearance).toEqual(HEALER_VISUAL_PROFILES[classId].appearance); + expect(save.healers[classId].appearance).not.toBe(HEALER_VISUAL_PROFILES[classId].appearance); + } + }); + + it("separates RPG Roguelike from Rogue Trials and selectable Dungeons", () => { + expect(MODE_COPY["roguelike-pve"].title).toBe("RPG Roguelike"); + expect(MODE_COPY["roguelike-pve"].description).toContain("Draft"); expect(MODE_COPY["rogue-trials"].detail).toContain("Endless"); expect(MODE_COPY.dungeons.title).toBe("Dungeons"); }); diff --git a/src/frontend/data.ts b/src/frontend/data.ts index 270d069..b75a717 100644 --- a/src/frontend/data.ts +++ b/src/frontend/data.ts @@ -1,9 +1,11 @@ import type { BossGroupCollection, GameModeId, GameSettings, HunterSave, SaveSlotId } from "./types"; import { AVAILABLE_BOSS_IDS, BOSS_DEFINITIONS, BOSS_GROUPS } from "../game/bossCatalog"; -import { createClassInventory } from "../game/healers"; +import { createClassInventory, HEALER_CLASS_ORDER } from "../game/healers"; import type { BossId } from "../game/types"; import { createDefaultGearProgress } from "../game/progression/gear"; import { BOSS_PET_DROPS, GROUP_DROP_TABLES, createEmptyCollectionLog, type CollectionLog, type LootRarity, type MaterialStack } from "../game/progression/loot"; +import { BLOCKBREAKER_BREACH_DAMAGE } from "../game/blockbreaker"; +import { createDefaultHealerAppearance } from "../game/healerVisuals"; export const DEFAULT_SETTINGS: GameSettings = { masterVolume: 80, @@ -66,10 +68,10 @@ export const DEFAULT_COLLECTIONS: BossGroupCollection[] = buildCollections(DEFAU export const MODE_COPY: Record = { "roguelike-pve": { - eyebrow: "1–4 hunters · randomized PVE", - title: "PVE", - description: "Enter without an encounter briefing, adapt to two randomized guardians, and build toward a full roguelike run.", - detail: "Two bosses selected when the run begins", + eyebrow: "Solo healer · drafted RPG expedition", + title: "RPG Roguelike", + description: "Draft a random four-companion party and mixed healing spellbook, clear escalating arcade hallways, defeat ten bosses, and build a run-only loadout.", + detail: "Three acts, three shops, and a final guardian", status: "Playable now", }, "rogue-trials": { @@ -86,11 +88,39 @@ export const MODE_COPY: Record [classId, { + level: 1, + inventory: createClassInventory(classId), + appearance: createDefaultHealerAppearance(classId), + }])) as HunterSave["healers"], location: "Ember Vault Approach", playSeconds: 0, updatedAt: now, @@ -140,6 +170,17 @@ export function createHunterSave(slotId: SaveSlotId, now: string, hunterName: st bossKills: {}, highestRoguelikeRound: 0, highestRogueTrialsEndlessKills: 0, + highestHockeyHealingReturns: 0, + longestHockeyHealingSecondsAtBest: 0, + hockeyHealingPvpWins: 0, + hockeyHealingPvpLosses: 0, + hockeyHealingPvpBossKills: 0, + highestBlockbreakerBricks: 0, + longestBlockbreakerSeconds: 0, + highestBlockbreakerScore: 0, + highestAetherAssaultScore: 0, + highestAetherAssaultWaveAtBest: 0, + longestAetherAssaultSecondsAtBest: 0, }, materials: [] as MaterialStack[], collectionLog: createEmptyCollectionLog(), diff --git a/src/frontend/leaderboardCache.test.ts b/src/frontend/leaderboardCache.test.ts new file mode 100644 index 0000000..069bf91 --- /dev/null +++ b/src/frontend/leaderboardCache.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; +import { LeaderboardCache } from "./leaderboardCache"; +import type { LeaderboardResult } from "./onlineRepository"; + +function memoryStorage() { + const values = new Map(); + return { + values, + getItem: (key: string) => values.get(key) ?? null, + setItem: (key: string, value: string) => { values.set(key, value); }, + }; +} + +const result: LeaderboardResult = { + kind: "roguelike", + top: [{ rank: 1, username: "hunter", hunterName: "Aelia", slotId: 1, value: 12 }], + current: { rank: 1, username: "hunter", hunterName: "Aelia", slotId: 1, value: 12 }, +}; + +describe("leaderboard cache", () => { + it("persists rankings for signed-in and offline reads", () => { + const storage = memoryStorage(); + const cache = new LeaderboardCache(storage, () => "2026-07-14T00:00:00.000Z"); + cache.write("hunter", "Aelia", 1, "roguelike", result); + + expect(cache.read(1, "roguelike", "Aelia", "hunter")?.result).toEqual(result); + expect(cache.read(1, "roguelike", "Aelia", null)?.accountId).toBe("hunter"); + expect(cache.read(1, "roguelike", "Other", null)).toBeNull(); + expect(cache.read(1, "roguelike", "Aelia", "different-account")).toBeNull(); + }); + + it("ignores corrupt persisted responses", () => { + const storage = memoryStorage(); + storage.setItem("i-want-to-heal:leaderboards:cache:v1", JSON.stringify({ + "1:roguelike": { accountId: "hunter", hunterName: "Aelia", slotId: 1, statId: "roguelike", updatedAt: "today", result: {} }, + })); + expect(new LeaderboardCache(storage).read(1, "roguelike", "Aelia", "hunter")).toBeNull(); + }); + + it("preserves Hockey Healing duration tiebreakers", () => { + const cache = new LeaderboardCache(memoryStorage()); + const hockeyResult: LeaderboardResult = { + kind: "hockey-healing", + top: [{ rank: 1, username: "hunter", hunterName: "Aelia", slotId: 1, value: 20, secondaryValue: 95.5 }], + current: { rank: 1, username: "hunter", hunterName: "Aelia", slotId: 1, value: 20, secondaryValue: 95.5 }, + }; + + cache.write("hunter", "Aelia", 1, "hockey-healing", hockeyResult); + expect(cache.read(1, "hockey-healing", "Aelia", "hunter")?.result).toEqual(hockeyResult); + }); + + it("validates Blockbreaker metric boards for offline reads", () => { + const cache = new LeaderboardCache(memoryStorage()); + const blockbreakerResult: LeaderboardResult = { + kind: "blockbreaker-score", + top: [{ rank: 1, username: "hunter", hunterName: "Aelia", slotId: 1, value: 12_500 }], + current: { rank: 1, username: "hunter", hunterName: "Aelia", slotId: 1, value: 12_500 }, + }; + cache.write("hunter", "Aelia", 1, "blockbreaker-score", blockbreakerResult); + expect(cache.read(1, "blockbreaker-score", "Aelia", "hunter")?.result).toEqual(blockbreakerResult); + }); + + it("preserves Aether Assault wave tiebreakers for offline reads", () => { + const cache = new LeaderboardCache(memoryStorage()); + const aetherResult: LeaderboardResult = { + kind: "aether-assault", + top: [{ rank: 1, username: "hunter", hunterName: "Aelia", slotId: 1, value: 48_500, secondaryValue: 9 }], + current: { rank: 1, username: "hunter", hunterName: "Aelia", slotId: 1, value: 48_500, secondaryValue: 9 }, + }; + cache.write("hunter", "Aelia", 1, "aether-assault", aetherResult); + expect(cache.read(1, "aether-assault", "Aelia", "hunter")?.result).toEqual(aetherResult); + }); +}); diff --git a/src/frontend/leaderboardCache.ts b/src/frontend/leaderboardCache.ts new file mode 100644 index 0000000..1752f9b --- /dev/null +++ b/src/frontend/leaderboardCache.ts @@ -0,0 +1,145 @@ +import type { LeaderboardEntry, LeaderboardResult } from "./onlineRepository"; +import type { ProfileStatId, SaveSlotId } from "./types"; + +interface StorageAdapter { + getItem(key: string): string | null; + setItem(key: string, value: string): void; +} + +export interface CachedLeaderboard { + accountId: string; + hunterName: string; + slotId: SaveSlotId; + statId: ProfileStatId; + updatedAt: string; + result: LeaderboardResult; +} + +const CACHE_KEY = "i-want-to-heal:leaderboards:cache:v1"; +const fallbackMemory = new Map(); +const fallbackStorage: StorageAdapter = { + getItem: (key) => fallbackMemory.get(key) ?? null, + setItem: (key, value) => { fallbackMemory.set(key, value); }, +}; + +function browserStorage(): StorageAdapter { + try { + if (typeof localStorage !== "undefined") return localStorage; + } catch { + // Android WebView can deny storage before its host is ready. + } + return fallbackStorage; +} + +function cacheId(slotId: SaveSlotId, statId: ProfileStatId) { + return `${slotId}:${statId}`; +} + +function leaderboardEntry(value: unknown): LeaderboardEntry | null { + if (!value || typeof value !== "object") return null; + const candidate = value as Partial; + if (!Number.isInteger(candidate.rank) || Number(candidate.rank) < 1) return null; + if (typeof candidate.username !== "string" || typeof candidate.hunterName !== "string") return null; + if (candidate.slotId !== 1 && candidate.slotId !== 2 && candidate.slotId !== 3) return null; + if (!Number.isFinite(candidate.value) || Number(candidate.value) < 0) return null; + if (candidate.secondaryValue !== undefined && (!Number.isFinite(candidate.secondaryValue) || Number(candidate.secondaryValue) < 0)) return null; + return { + rank: Number(candidate.rank), + username: candidate.username, + hunterName: candidate.hunterName, + slotId: candidate.slotId, + value: Number(candidate.value), + ...(candidate.secondaryValue === undefined ? {} : { secondaryValue: Number(candidate.secondaryValue) }), + }; +} + +function leaderboardResult(value: unknown): LeaderboardResult | null { + if (!value || typeof value !== "object") return null; + const candidate = value as Partial; + if (candidate.kind !== "boss" + && candidate.kind !== "roguelike" + && candidate.kind !== "rogue-trials-endless" + && candidate.kind !== "hockey-healing" + && candidate.kind !== "hockey-pvp-wins" + && candidate.kind !== "hockey-pvp-boss-kills" + && candidate.kind !== "blockbreaker-bricks" + && candidate.kind !== "blockbreaker-time" + && candidate.kind !== "blockbreaker-score" + && candidate.kind !== "aether-assault") return null; + if (!Array.isArray(candidate.top)) return null; + const top = candidate.top.map(leaderboardEntry); + if (top.some((entry) => !entry)) return null; + const current = candidate.current === null ? null : leaderboardEntry(candidate.current); + if (candidate.current !== null && !current) return null; + return { + kind: candidate.kind, + ...(candidate.kind === "boss" && typeof candidate.bossId === "string" ? { bossId: candidate.bossId } : {}), + top: top as LeaderboardEntry[], + current, + }; +} + +function cachedLeaderboard(value: unknown): CachedLeaderboard | null { + if (!value || typeof value !== "object") return null; + const candidate = value as Partial; + if (typeof candidate.accountId !== "string" || typeof candidate.hunterName !== "string") return null; + if (candidate.slotId !== 1 && candidate.slotId !== 2 && candidate.slotId !== 3) return null; + if (typeof candidate.statId !== "string" || typeof candidate.updatedAt !== "string") return null; + if (Number.isNaN(Date.parse(candidate.updatedAt))) return null; + const result = leaderboardResult(candidate.result); + if (!result) return null; + return { ...candidate, result } as CachedLeaderboard; +} + +export class LeaderboardCache { + constructor( + private readonly storage: StorageAdapter = browserStorage(), + private readonly now: () => string = () => new Date().toISOString(), + ) {} + + read(slotId: SaveSlotId, statId: ProfileStatId, hunterName: string, accountId: string | null): CachedLeaderboard | null { + const entry = this.readAll()[cacheId(slotId, statId)]; + if (!entry || entry.slotId !== slotId || entry.statId !== statId || entry.hunterName !== hunterName) return null; + if (accountId && entry.accountId !== accountId) return null; + return structuredClone(entry); + } + + write(accountId: string, hunterName: string, slotId: SaveSlotId, statId: ProfileStatId, result: LeaderboardResult): CachedLeaderboard { + const entries = this.readAll(); + const entry: CachedLeaderboard = { + accountId, + hunterName, + slotId, + statId, + updatedAt: this.now(), + result: structuredClone(result), + }; + entries[cacheId(slotId, statId)] = entry; + this.storage.setItem(CACHE_KEY, JSON.stringify(entries)); + return structuredClone(entry); + } + + clearSlot(slotId: SaveSlotId) { + const entries = this.readAll(); + for (const key of Object.keys(entries)) { + if (entries[key].slotId === slotId) delete entries[key]; + } + this.storage.setItem(CACHE_KEY, JSON.stringify(entries)); + } + + private readAll(): Record { + try { + const raw = this.storage.getItem(CACHE_KEY); + const parsed = raw ? JSON.parse(raw) as Record : {}; + if (!parsed || typeof parsed !== "object") return {}; + return Object.fromEntries(Object.entries(parsed).flatMap(([key, value]) => { + const entry = cachedLeaderboard(value); + return entry ? [[key, entry]] : []; + })); + } catch { + return {}; + } + } +} + +export const leaderboardCache = new LeaderboardCache(); diff --git a/src/frontend/offlineLeaderboardFlow.test.ts b/src/frontend/offlineLeaderboardFlow.test.ts new file mode 100644 index 0000000..5c929e4 --- /dev/null +++ b/src/frontend/offlineLeaderboardFlow.test.ts @@ -0,0 +1,42 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { onlineRepository } from "./onlineRepository"; +import { hasPendingSaveSync } from "./saveSync"; +import { useFrontendStore } from "./store"; + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +describe("offline leaderboard publishing", () => { + it("keeps earned stats pending without attempting an offline upload", async () => { + const original = useFrontendStore.getState(); + original.deleteSlot(3); + expect(original.createSlot(3, "Offline Hunter")).toBe(true); + original.playSlot(3); + useFrontendStore.setState({ accountId: "offline-account" }); + useFrontendStore.getState().recordBossVictory("bulldrome", "initiate"); + useFrontendStore.getState().recordAetherAssaultDefeat(18_750, 6, 214.5); + expect(hasPendingSaveSync(3)).toBe(true); + + vi.stubGlobal("navigator", { onLine: false }); + const write = vi.spyOn(onlineRepository, "writeSave"); + expect(await useFrontendStore.getState().uploadSlot(3)).toBe(false); + expect(write).not.toHaveBeenCalled(); + expect(useFrontendStore.getState().slots[2].local?.stats.bossKills.bulldrome).toBe(1); + expect(useFrontendStore.getState().slots[2].local?.stats).toMatchObject({ + highestAetherAssaultScore: 18_750, + highestAetherAssaultWaveAtBest: 6, + longestAetherAssaultSecondsAtBest: 214.5, + }); + + useFrontendStore.getState().deleteSlot(3); + useFrontendStore.setState({ + accountId: original.accountId, + activeSlotId: original.activeSlotId, + selectedSlotId: original.selectedSlotId, + screen: original.screen, + notice: original.notice, + }); + }); +}); diff --git a/src/frontend/onlineRepository.ts b/src/frontend/onlineRepository.ts index 4b5bec8..e07e168 100644 --- a/src/frontend/onlineRepository.ts +++ b/src/frontend/onlineRepository.ts @@ -1,6 +1,7 @@ import { Capacitor } from "@capacitor/core"; import type { HunterSave, SaveSlotId } from "./types"; import type { BossId } from "../game/types"; +import type { HockeyPvpRemoteSnapshot, HockeyPvpRole } from "../game/hockeyHealingPvp"; export interface OnlineAccount { id: number; @@ -19,15 +20,32 @@ export interface LeaderboardEntry { hunterName: string; slotId: SaveSlotId; value: number; + secondaryValue?: number; } export interface LeaderboardResult { - kind: "boss" | "roguelike" | "rogue-trials-endless"; + kind: "boss" | "roguelike" | "rogue-trials-endless" | "hockey-healing" | "hockey-pvp-wins" | "hockey-pvp-boss-kills" | "blockbreaker-bricks" | "blockbreaker-time" | "blockbreaker-score" | "aether-assault"; bossId?: BossId; top: LeaderboardEntry[]; current: LeaderboardEntry | null; } +export interface HockeyPvpQueueResult { + ticketId: string; + status: "waiting" | "matched"; + match?: { + id: string; + seed: number; + opponentName: string; + role: Exclude; + }; +} + +export interface HockeyPvpExchangeResult { + opponentSnapshot: HockeyPvpRemoteSnapshot | null; + hostSnapshot: HockeyPvpRemoteSnapshot | null; +} + interface TokenStorage { getItem(key: string): string | null; setItem(key: string, value: string): void; @@ -151,6 +169,58 @@ export class OnlineRepository { rogueTrialsEndlessLeaderboard(slotId: SaveSlotId): Promise { return this.request(`/api/leaderboards/rogue-trials-endless?slot=${slotId}`); } + + hockeyHealingLeaderboard(slotId: SaveSlotId): Promise { + return this.request(`/api/leaderboards/hockey-healing?slot=${slotId}`); + } + + hockeyPvpWinsLeaderboard(slotId: SaveSlotId): Promise { + return this.request(`/api/leaderboards/hockey-pvp-wins?slot=${slotId}`); + } + + hockeyPvpBossKillsLeaderboard(slotId: SaveSlotId): Promise { + return this.request(`/api/leaderboards/hockey-pvp-boss-kills?slot=${slotId}`); + } + + blockbreakerBricksLeaderboard(slotId: SaveSlotId): Promise { + return this.request(`/api/leaderboards/blockbreaker-bricks?slot=${slotId}`); + } + + blockbreakerTimeLeaderboard(slotId: SaveSlotId): Promise { + return this.request(`/api/leaderboards/blockbreaker-time?slot=${slotId}`); + } + + blockbreakerScoreLeaderboard(slotId: SaveSlotId): Promise { + return this.request(`/api/leaderboards/blockbreaker-score?slot=${slotId}`); + } + + aetherAssaultLeaderboard(slotId: SaveSlotId): Promise { + return this.request(`/api/leaderboards/aether-assault?slot=${slotId}`); + } + + joinHockeyPvpQueue(slotId: SaveSlotId, hunterName: string): Promise { + return this.request("/api/hockey-pvp/queue", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ slotId, hunterName }), + }); + } + + pollHockeyPvpQueue(ticketId: string): Promise { + return this.request(`/api/hockey-pvp/queue/${encodeURIComponent(ticketId)}`); + } + + cancelHockeyPvpQueue(ticketId: string): Promise { + return this.request(`/api/hockey-pvp/queue/${encodeURIComponent(ticketId)}`, { method: "DELETE" }); + } + + exchangeHockeyPvpState(matchId: string, snapshot: HockeyPvpRemoteSnapshot): Promise { + return this.request(`/api/hockey-pvp/matches/${encodeURIComponent(matchId)}/state`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ snapshot }), + }); + } } export const onlineRepository = new OnlineRepository(); diff --git a/src/frontend/profileSections.test.ts b/src/frontend/profileSections.test.ts new file mode 100644 index 0000000..16b0513 --- /dev/null +++ b/src/frontend/profileSections.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { buildCollections } from "./data"; +import { alphabeticalBosses, defaultStatForSection, profileSectionForStat } from "./profileSections"; + +describe("hunter profile sections", () => { + const bosses = alphabeticalBosses(buildCollections({ dropsFound: {}, petsFound: {} }, {})); + + it("groups related records under one section", () => { + expect(profileSectionForStat("blockbreaker-bricks")).toBe("blockbreaker"); + expect(profileSectionForStat("blockbreaker-time")).toBe("blockbreaker"); + expect(profileSectionForStat("hockey-pvp-boss-kills")).toBe("hockey-pvp"); + expect(profileSectionForStat("aether-assault")).toBe("aether-assault"); + expect(profileSectionForStat("bulldrome")).toBe("bosses"); + }); + + it("builds one alphabetical boss index across mechanic groups", () => { + const names = bosses.map((boss) => boss.bossName); + expect(names).toEqual([...names].sort((left, right) => left.localeCompare(right))); + expect(new Set(bosses.map((boss) => boss.bossId)).size).toBe(bosses.length); + }); + + it("opens each section on its primary record", () => { + expect(defaultStatForSection("hockey-pvp", bosses)).toBe("hockey-pvp-wins"); + expect(defaultStatForSection("blockbreaker", bosses)).toBe("blockbreaker-score"); + expect(defaultStatForSection("aether-assault", bosses)).toBe("aether-assault"); + expect(defaultStatForSection("bosses", bosses)).toBe(bosses[0].bossId); + }); +}); diff --git a/src/frontend/profileSections.ts b/src/frontend/profileSections.ts new file mode 100644 index 0000000..420946b --- /dev/null +++ b/src/frontend/profileSections.ts @@ -0,0 +1,45 @@ +import { AVAILABLE_BOSS_IDS } from "../game/bossCatalog"; +import type { BossId } from "../game/types"; +import type { GroupBossCollection, ProfileStatId } from "./types"; + +export type ProfileSectionId = "roguelike" | "rogue-trials" | "hockey" | "hockey-pvp" | "blockbreaker" | "aether-assault" | "bosses"; + +export interface ProfileSectionDefinition { + id: ProfileSectionId; + label: string; + copy: string; + icon: string; + statIds: readonly ProfileStatId[]; +} + +export const PROFILE_SECTIONS: readonly ProfileSectionDefinition[] = [ + { id: "roguelike", label: "Roguelike", copy: "Highest completed round", icon: "∞", statIds: ["roguelike"] }, + { id: "rogue-trials", label: "Trials Endless", copy: "Best endless boss run", icon: "Ⅲ", statIds: ["rogue-trials-endless"] }, + { id: "hockey", label: "Hockey", copy: "Returns and survival", icon: "◌", statIds: ["hockey-healing"] }, + { id: "hockey-pvp", label: "Hockey PVP", copy: "Record and race kills", icon: "◇", statIds: ["hockey-pvp-wins", "hockey-pvp-boss-kills"] }, + { id: "blockbreaker", label: "Blockbreaker", copy: "Score, bricks, survival", icon: "▦", statIds: ["blockbreaker-score", "blockbreaker-bricks", "blockbreaker-time"] }, + { id: "aether-assault", label: "Aether Assault", copy: "Score, wave, survival", icon: "⌁", statIds: ["aether-assault"] }, + { id: "bosses", label: "Bosses", copy: "Kills and boss pets", icon: "♛", statIds: [] }, +] as const; + +const BOSS_IDS = new Set(AVAILABLE_BOSS_IDS); + +export function isBossProfileStat(statId: ProfileStatId): statId is BossId { + return BOSS_IDS.has(statId); +} + +export function profileSectionForStat(statId: ProfileStatId): ProfileSectionId { + if (isBossProfileStat(statId)) return "bosses"; + return PROFILE_SECTIONS.find((section) => section.statIds.includes(statId))?.id ?? "roguelike"; +} + +export function alphabeticalBosses(groups: readonly { bosses: readonly GroupBossCollection[] }[]): GroupBossCollection[] { + return groups + .flatMap((group) => group.bosses) + .sort((left, right) => left.bossName.localeCompare(right.bossName)); +} + +export function defaultStatForSection(sectionId: ProfileSectionId, bosses: readonly GroupBossCollection[]): ProfileStatId { + if (sectionId === "bosses") return bosses[0]?.bossId ?? "roguelike"; + return PROFILE_SECTIONS.find((section) => section.id === sectionId)?.statIds[0] ?? "roguelike"; +} diff --git a/src/frontend/saveRepository.test.ts b/src/frontend/saveRepository.test.ts index 5cf87e1..ce070ff 100644 --- a/src/frontend/saveRepository.test.ts +++ b/src/frontend/saveRepository.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { SaveRepository, type StorageAdapter } from "./saveRepository"; import { groupDrop } from "../game/progression/loot"; import { RUN_BUFF_ORDER } from "../game/roguelike"; +import { HEALER_VISUAL_PROFILES } from "../game/healerVisuals"; function memoryStorage(): StorageAdapter { const data = new Map(); @@ -45,10 +46,22 @@ describe("SaveRepository", () => { const repository = new SaveRepository(memoryStorage(), () => now); const serverSave = repository.create(1, "Aelia"); serverSave.healers.priest.level = 40; + const legacyStats = structuredClone(serverSave.stats) as unknown as Record; + delete legacyStats.highestAetherAssaultScore; + delete legacyStats.highestAetherAssaultWaveAtBest; + delete legacyStats.longestAetherAssaultSecondsAtBest; now = "2026-07-10T14:00:00.000Z"; - repository.replaceLocal(serverSave); + repository.replaceLocal({ ...serverSave, schemaVersion: 5, stats: legacyStats } as never); expect(repository.listLocal()[0].local?.healers.priest.level).toBe(40); expect(repository.listLocal()[0].local?.updatedAt).toBe(now); + expect(repository.listLocal()[0].local).toMatchObject({ + schemaVersion: 6, + stats: { + highestAetherAssaultScore: 0, + highestAetherAssaultWaveAtBest: 0, + longestAetherAssaultSecondsAtBest: 0, + }, + }); }); it("deletes the local copy without inventing an online record", () => { @@ -68,7 +81,11 @@ describe("SaveRepository", () => { activeClassId: "druid", healers: { ...save.healers, - druid: { level: 8, inventory: [...save.healers.druid.inventory, { ...save.healers.druid.inventory[0], id: "druid-drop" }] }, + druid: { + ...save.healers.druid, + level: 8, + inventory: [...save.healers.druid.inventory, { ...save.healers.druid.inventory[0], id: "druid-drop" }], + }, }, })); @@ -81,7 +98,95 @@ describe("SaveRepository", () => { expect(save.healers.priest.inventory).toHaveLength(4); }); - it("resets every legacy save into fresh v5 progression while preserving identity and timestamp", () => { + it("adds default appearances to existing v6 saves without resetting progression or timestamps", () => { + const storage = memoryStorage(); + const repository = new SaveRepository(storage, () => "2026-07-16T12:00:00.000Z"); + const created = repository.create(1, "Veteran"); + const legacy = structuredClone(created) as unknown as { + updatedAt: string; + healers: Record; + }; + legacy.healers.priest.level = 37; + legacy.updatedAt = "2026-07-15T09:30:00.000Z"; + for (const healer of Object.values(legacy.healers)) delete healer.appearance; + storage.setItem("i-want-to-heal:saves:local:v1", JSON.stringify({ 1: legacy })); + + const migrated = repository.listLocal()[0].local!; + expect(migrated.schemaVersion).toBe(6); + expect(migrated.updatedAt).toBe("2026-07-15T09:30:00.000Z"); + expect(migrated.healers.priest.level).toBe(37); + for (const [classId, profile] of Object.entries(HEALER_VISUAL_PROFILES)) { + expect(migrated.healers[classId as keyof typeof migrated.healers].appearance).toEqual(profile.appearance); + } + const persisted = JSON.parse(storage.getItem("i-want-to-heal:saves:local:v1") ?? "{}") as Record; + expect(persisted["1"].updatedAt).toBe("2026-07-15T09:30:00.000Z"); + expect(persisted["1"].healers.paladin.appearance).toEqual(HEALER_VISUAL_PROFILES.paladin.appearance); + }); + + it("repairs corrupt appearance fields without resetting safe choices or another healer", () => { + const storage = memoryStorage(); + const repository = new SaveRepository(storage, () => "2026-07-16T12:00:00.000Z"); + const created = repository.create(1, "Mixer"); + const druidAppearance = structuredClone(created.healers.druid.appearance); + const corrupt = structuredClone(created) as unknown as Record; + const corruptHealers = (corrupt.healers as Record>); + corruptHealers.priest.level = 22; + corruptHealers.priest.appearance = { + version: 1, + rigId: "medium", + scaleSourceMemberId: "unknown-member", + headPartId: "knight-upper", + upperBodyPartId: "knight-upper", + lowerBodyPartId: "unknown-lower", + headwearPartId: "druid-backpack", + backPartId: "ranger-cape", + mainHand: { modelId: "sword", grip: "staff" }, + offHand: { modelId: "unknown-weapon", grip: "prop" }, + }; + storage.setItem("i-want-to-heal:saves:local:v1", JSON.stringify({ 1: corrupt })); + + const normalized = repository.listLocal()[0].local!; + expect(normalized.healers.priest.level).toBe(22); + expect(normalized.healers.priest.appearance).toEqual({ + ...HEALER_VISUAL_PROFILES.priest.appearance, + upperBodyPartId: "knight-upper", + backPartId: "ranger-cape", + mainHand: { modelId: "cc/adv_sword_1handed", grip: "upright" }, + }); + expect(normalized.healers.priest.appearance.offHand).toBeUndefined(); + expect(normalized.healers.druid.appearance).toEqual(druidAppearance); + }); + + it("round-trips and deep-copies a valid mixed appearance", () => { + const repository = new SaveRepository(memoryStorage(), () => "2026-07-16T12:00:00.000Z"); + repository.create(1, "Mixer"); + repository.updateLocal(1, (save) => ({ + ...save, + healers: { + ...save.healers, + priest: { + ...save.healers.priest, + appearance: { + ...save.healers.priest.appearance, + headPartId: "rogue-head", + upperBodyPartId: "knight-upper", + lowerBodyPartId: "ranger-lower", + headwearPartId: "mage-hat", + backPartId: "druid-backpack", + mainHand: { modelId: "cc/wand_b", grip: "wand" }, + offHand: { modelId: "cc/spellbook_open", grip: "prop" }, + }, + }, + }, + })); + repository.copyLocal(1, 2); + const [source, copy] = repository.listLocal().map((slot) => slot.local); + expect(copy?.healers.priest.appearance).toEqual(source?.healers.priest.appearance); + expect(copy?.healers.priest.appearance).not.toBe(source?.healers.priest.appearance); + expect(copy?.healers.priest.appearance.mainHand).not.toBe(source?.healers.priest.appearance.mainHand); + }); + + it("resets every legacy save into fresh v6 progression while preserving identity and timestamp", () => { const storage = memoryStorage(); const repository = new SaveRepository(storage, () => "2026-07-10T12:00:00.000Z"); const created = repository.create(1, "Legacy"); @@ -103,16 +208,35 @@ describe("SaveRepository", () => { storage.setItem("i-want-to-heal:saves:local:v1", JSON.stringify({ 1: legacy })); const migrated = repository.listLocal()[0].local!; - expect(migrated.schemaVersion).toBe(5); + expect(migrated.schemaVersion).toBe(6); expect(migrated.hunterName).toBe("Legacy"); expect(migrated.activeClassId).toBe("priest"); expect(migrated.playSeconds).toBe(0); expect(Object.values(migrated.healers).every((healer) => healer.level === 1 && healer.inventory.length > 0)).toBe(true); - expect(migrated.stats).toEqual({ totalBossKills: 0, flawlessClears: 0, alliesSaved: 0, healingDone: 0, bossKills: {}, highestRoguelikeRound: 0, highestRogueTrialsEndlessKills: 0 }); + expect(migrated.stats).toEqual({ + totalBossKills: 0, + flawlessClears: 0, + alliesSaved: 0, + healingDone: 0, + bossKills: {}, + highestRoguelikeRound: 0, + highestRogueTrialsEndlessKills: 0, + highestHockeyHealingReturns: 0, + longestHockeyHealingSecondsAtBest: 0, + hockeyHealingPvpWins: 0, + hockeyHealingPvpLosses: 0, + hockeyHealingPvpBossKills: 0, + highestBlockbreakerBricks: 0, + longestBlockbreakerSeconds: 0, + highestBlockbreakerScore: 0, + highestAetherAssaultScore: 0, + highestAetherAssaultWaveAtBest: 0, + longestAetherAssaultSecondsAtBest: 0, + }); expect(migrated.materials).toEqual([]); expect(migrated.collectionLog).toEqual({ dropsFound: {}, petsFound: {} }); expect(Object.values(migrated.gearProgress).every((owner) => owner.infusionAbilityId === null && owner.passiveInfusionId === null && Object.values(owner.slots).every((slot) => slot.level === 0))).toBe(true); - expect(JSON.parse(storage.getItem("i-want-to-heal:saves:local:v1") ?? "{}")["1"].schemaVersion).toBe(5); + expect(JSON.parse(storage.getItem("i-want-to-heal:saves:local:v1") ?? "{}")["1"].schemaVersion).toBe(6); }); it("preserves valid v5 progression and group-drop inventory", () => { @@ -123,15 +247,31 @@ describe("SaveRepository", () => { created.healers.priest.level = 8; created.stats = { ...created.stats, totalBossKills: 2, bossKills: { bulldrome: 2 } }; created.stats.highestRogueTrialsEndlessKills = 14; + created.stats.highestHockeyHealingReturns = 31; + created.stats.longestHockeyHealingSecondsAtBest = 188.5; + created.stats.highestBlockbreakerBricks = 52; + created.stats.longestBlockbreakerSeconds = 245.25; + created.stats.highestBlockbreakerScore = 9_800; + created.stats.highestAetherAssaultScore = 12_400; + created.stats.highestAetherAssaultWaveAtBest = 7; + created.stats.longestAetherAssaultSecondsAtBest = 191.5; created.materials = [{ id: drop.id, name: drop.name, quantity: 4, rarity: drop.rarity, itemLevel: drop.itemLevel, glyph: drop.glyph }]; created.collectionLog = { dropsFound: { [drop.id]: 4 }, petsFound: { "bulldrome-pet": 1 } }; - storage.setItem("i-want-to-heal:saves:local:v1", JSON.stringify({ 1: created })); + storage.setItem("i-want-to-heal:saves:local:v1", JSON.stringify({ 1: { ...created, schemaVersion: 5 } })); const migrated = repository.listLocal()[0].local!; - expect(migrated.schemaVersion).toBe(5); + expect(migrated.schemaVersion).toBe(6); expect(migrated.healers.priest.level).toBe(8); expect(migrated.stats.bossKills).toEqual({ bulldrome: 2 }); expect(migrated.stats.highestRogueTrialsEndlessKills).toBe(14); + expect(migrated.stats.highestHockeyHealingReturns).toBe(31); + expect(migrated.stats.longestHockeyHealingSecondsAtBest).toBe(188.5); + expect(migrated.stats.highestBlockbreakerBricks).toBe(52); + expect(migrated.stats.longestBlockbreakerSeconds).toBe(245.25); + expect(migrated.stats.highestBlockbreakerScore).toBe(9_800); + expect(migrated.stats.highestAetherAssaultScore).toBe(12_400); + expect(migrated.stats.highestAetherAssaultWaveAtBest).toBe(7); + expect(migrated.stats.longestAetherAssaultSecondsAtBest).toBe(191.5); expect(migrated.materials[0]).toMatchObject({ id: drop.id, quantity: 4 }); expect(migrated.collectionLog).toEqual(created.collectionLog); }); @@ -145,10 +285,10 @@ describe("SaveRepository", () => { created.gearProgress.druid.passiveInfusionId = "mend-echo"; created.gearProgress.brann.infusionAbilityId = "removed-infusion"; created.gearProgress.brann.passiveInfusionId = "deep-wells" as never; - storage.setItem("i-want-to-heal:saves:local:v1", JSON.stringify({ 1: created })); + storage.setItem("i-want-to-heal:saves:local:v1", JSON.stringify({ 1: { ...created, schemaVersion: 5 } })); const migrated = repository.listLocal()[0].local!; - expect(migrated.schemaVersion).toBe(5); + expect(migrated.schemaVersion).toBe(6); expect(migrated.gearProgress.priest.infusionAbilityId).toBe("priest-sanctuary"); expect(migrated.gearProgress.priest.passiveInfusionId).toBeNull(); expect(migrated.gearProgress.druid.passiveInfusionId).toBe("mend-echo"); diff --git a/src/frontend/saveRepository.ts b/src/frontend/saveRepository.ts index f4fdddc..73cfc41 100644 --- a/src/frontend/saveRepository.ts +++ b/src/frontend/saveRepository.ts @@ -1,10 +1,11 @@ import { createHunterSave } from "./data"; -import { createClassInventory } from "../game/healers"; +import { createClassInventory, HEALER_CLASS_ORDER } from "../game/healers"; import { AVAILABLE_BOSS_IDS, BOSS_DEFINITIONS } from "../game/bossCatalog"; import { createDefaultGearProgress, GEAR_OWNER_ORDER, GEAR_SLOT_ORDER, MAX_GEAR_LEVEL, type GearProgress } from "../game/progression/gear"; import { normalizeActiveInfusionId, normalizePassiveInfusionId } from "../game/progression/infusions"; import { GROUP_DROP_TABLES, type CollectionLog, type MaterialStack } from "../game/progression/loot"; import type { BossId, HealerClassId } from "../game/types"; +import { normalizeHealerAppearance } from "../game/healerVisuals"; import type { HunterSave, SaveSlotId, SaveSlotState } from "./types"; export interface StorageAdapter { @@ -48,7 +49,7 @@ interface LegacyHunterSave { gearProgress?: GearProgress; } -const HEALER_IDS: HealerClassId[] = ["priest", "druid", "shaman"]; +const HEALER_IDS: readonly HealerClassId[] = HEALER_CLASS_ORDER; function positiveCounts(value: unknown): Record { if (!value || typeof value !== "object") return {}; @@ -121,7 +122,7 @@ function normalizeSave(value: unknown): HunterSave | null { if (!value || typeof value !== "object") return null; const candidate = value as LegacyHunterSave; if (!candidate.slotId || !candidate.hunterName) return null; - if (candidate.schemaVersion !== 5) { + if (candidate.schemaVersion !== 5 && candidate.schemaVersion !== 6) { try { return createHunterSave(candidate.slotId, typeof candidate.updatedAt === "string" ? candidate.updatedAt : new Date(0).toISOString(), candidate.hunterName); } catch { @@ -132,13 +133,14 @@ function normalizeSave(value: unknown): HunterSave | null { const bossKills = normalizeBossKills(candidate.stats?.bossKills); const activeClassId = HEALER_IDS.includes(candidate.activeClassId as HealerClassId) ? candidate.activeClassId as HealerClassId : "priest"; return { - schemaVersion: 5, + schemaVersion: 6, slotId: candidate.slotId, hunterName: candidate.hunterName, activeClassId, healers: Object.fromEntries(HEALER_IDS.map((classId) => [classId, { level: Math.max(1, candidate.healers?.[classId]?.level ?? (classId === "priest" ? candidate.level ?? 1 : 1)), inventory: candidate.healers?.[classId]?.inventory ?? createClassInventory(classId), + appearance: normalizeHealerAppearance(classId, candidate.healers?.[classId]?.appearance), }])) as HunterSave["healers"], location: candidate.location ?? "Ember Vault Approach", playSeconds: Math.max(0, candidate.playSeconds ?? 0), @@ -151,6 +153,17 @@ function normalizeSave(value: unknown): HunterSave | null { bossKills, highestRoguelikeRound: Math.max(0, Math.floor(candidate.stats?.highestRoguelikeRound ?? 0)), highestRogueTrialsEndlessKills: Math.max(0, Math.floor(candidate.stats?.highestRogueTrialsEndlessKills ?? 0)), + highestHockeyHealingReturns: Math.max(0, Math.floor(candidate.stats?.highestHockeyHealingReturns ?? 0)), + longestHockeyHealingSecondsAtBest: Math.max(0, Number(candidate.stats?.longestHockeyHealingSecondsAtBest) || 0), + hockeyHealingPvpWins: Math.max(0, Math.floor(candidate.stats?.hockeyHealingPvpWins ?? 0)), + hockeyHealingPvpLosses: Math.max(0, Math.floor(candidate.stats?.hockeyHealingPvpLosses ?? 0)), + hockeyHealingPvpBossKills: Math.max(0, Math.floor(candidate.stats?.hockeyHealingPvpBossKills ?? 0)), + highestBlockbreakerBricks: Math.max(0, Math.floor(candidate.stats?.highestBlockbreakerBricks ?? 0)), + longestBlockbreakerSeconds: Math.max(0, Number(candidate.stats?.longestBlockbreakerSeconds) || 0), + highestBlockbreakerScore: Math.max(0, Math.floor(candidate.stats?.highestBlockbreakerScore ?? 0)), + highestAetherAssaultScore: Math.max(0, Math.floor(candidate.stats?.highestAetherAssaultScore ?? 0)), + highestAetherAssaultWaveAtBest: Math.max(0, Math.floor(candidate.stats?.highestAetherAssaultWaveAtBest ?? 0)), + longestAetherAssaultSecondsAtBest: Math.max(0, Number(candidate.stats?.longestAetherAssaultSecondsAtBest) || 0), }, materials: normalizeMaterials(candidate.materials, collectionLog), collectionLog, @@ -224,7 +237,7 @@ export class SaveRepository { } replaceLocal(save: HunterSave): HunterSave { - const normalized = { ...cloneSave(save), updatedAt: this.now() }; + const normalized = { ...(normalizeSave(save) ?? cloneSave(save)), updatedAt: this.now() }; this.setLocal(normalized); return normalized; } diff --git a/src/frontend/saveSync.test.ts b/src/frontend/saveSync.test.ts new file mode 100644 index 0000000..9fab8ba --- /dev/null +++ b/src/frontend/saveSync.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; +import { PendingSaveSyncRepository, SAVE_SYNC_RETRY_DELAYS_MS, SaveSyncRetryCoordinator } from "./saveSync"; + +function memoryStorage(initial = "[]") { + const values = new Map([["i-want-to-heal:saves:pending-sync:v1", initial]]); + return { + getItem: (key: string) => values.get(key) ?? null, + setItem: (key: string, value: string) => { values.set(key, value); }, + }; +} + +describe("pending save sync", () => { + it("persists unique dirty slots and tolerates corrupt state", () => { + const storage = memoryStorage(); + const pending = new PendingSaveSyncRepository(storage); + pending.mark(2); + pending.mark(2); + pending.mark(1); + expect(pending.list()).toEqual([2, 1]); + pending.clear(2); + expect(pending.list()).toEqual([1]); + expect(new PendingSaveSyncRepository(memoryStorage("not-json")).list()).toEqual([]); + }); + + it("waits offline and caps automatic retries", async () => { + const pending = new PendingSaveSyncRepository(memoryStorage()); + pending.mark(1); + let online = false; + const uploads: number[] = []; + const tasks: Array<{ run: () => void; delay: number }> = []; + const coordinator = new SaveSyncRetryCoordinator( + pending, + async (slotId) => { uploads.push(slotId); return false; }, + () => online, + (run, delay) => { + tasks.push({ run, delay }); + return tasks.length as unknown as ReturnType; + }, + () => undefined, + ); + + coordinator.flush(); + expect(uploads).toEqual([]); + online = true; + coordinator.flush(true); + await Promise.resolve(); + expect(uploads).toEqual([1]); + + for (const delay of SAVE_SYNC_RETRY_DELAYS_MS) { + coordinator.failed(1); + expect(tasks[0]?.delay).toBe(delay); + tasks.shift()!.run(); + await Promise.resolve(); + } + coordinator.failed(1); + expect(tasks).toEqual([]); + expect(uploads).toHaveLength(1 + SAVE_SYNC_RETRY_DELAYS_MS.length); + coordinator.dispose(); + }); +}); diff --git a/src/frontend/saveSync.ts b/src/frontend/saveSync.ts new file mode 100644 index 0000000..bd7ce35 --- /dev/null +++ b/src/frontend/saveSync.ts @@ -0,0 +1,153 @@ +import type { SaveSlotId } from "./types"; + +interface StorageAdapter { + getItem(key: string): string | null; + setItem(key: string, value: string): void; +} + +type TimerHandle = ReturnType; +type UploadSave = (slotId: SaveSlotId) => Promise; + +const PENDING_KEY = "i-want-to-heal:saves:pending-sync:v1"; +export const SAVE_SYNC_RETRY_DELAYS_MS = [2_000, 10_000, 30_000, 120_000] as const; +const fallbackMemory = new Map(); +const fallbackStorage: StorageAdapter = { + getItem: (key) => fallbackMemory.get(key) ?? null, + setItem: (key, value) => { fallbackMemory.set(key, value); }, +}; + +function browserStorage(): StorageAdapter { + try { + if (typeof localStorage !== "undefined") return localStorage; + } catch { + // Android WebView can deny storage before its host is ready. + } + return fallbackStorage; +} + +function isSlotId(value: number): value is SaveSlotId { + return value === 1 || value === 2 || value === 3; +} + +export function networkAppearsOnline() { + return typeof navigator === "undefined" || navigator.onLine !== false; +} + +export class PendingSaveSyncRepository { + constructor(private readonly storage: StorageAdapter = browserStorage()) {} + + list(): SaveSlotId[] { + try { + const parsed = JSON.parse(this.storage.getItem(PENDING_KEY) ?? "[]") as unknown; + if (!Array.isArray(parsed)) return []; + return [...new Set(parsed.map(Number).filter(isSlotId))]; + } catch { + return []; + } + } + + has(slotId: SaveSlotId) { + return this.list().includes(slotId); + } + + mark(slotId: SaveSlotId) { + const slots = this.list(); + if (!slots.includes(slotId)) this.write([...slots, slotId]); + } + + clear(slotId: SaveSlotId) { + this.write(this.list().filter((candidate) => candidate !== slotId)); + } + + private write(slots: readonly SaveSlotId[]) { + this.storage.setItem(PENDING_KEY, JSON.stringify(slots)); + } +} + +export class SaveSyncRetryCoordinator { + private readonly attempts = new Map(); + private readonly timers = new Map(); + private readonly active = new Set(); + private disposed = false; + + constructor( + private readonly pending: PendingSaveSyncRepository, + private readonly upload: UploadSave, + private readonly isOnline: () => boolean = networkAppearsOnline, + private readonly schedule: (run: () => void, delay: number) => TimerHandle = setTimeout, + private readonly cancel: (timer: TimerHandle) => void = clearTimeout, + ) {} + + flush(resetAttempts = false) { + if (resetAttempts) this.attempts.clear(); + if (!this.isOnline()) return; + for (const slotId of this.pending.list()) this.run(slotId); + } + + failed(slotId: SaveSlotId) { + if (this.disposed || !this.pending.has(slotId) || !this.isOnline() || this.timers.has(slotId)) return; + const attempt = this.attempts.get(slotId) ?? 0; + const delay = SAVE_SYNC_RETRY_DELAYS_MS[attempt]; + if (delay === undefined) return; + this.attempts.set(slotId, attempt + 1); + const timer = this.schedule(() => { + this.timers.delete(slotId); + this.run(slotId); + }, delay); + this.timers.set(slotId, timer); + } + + succeeded(slotId: SaveSlotId) { + this.attempts.delete(slotId); + const timer = this.timers.get(slotId); + if (timer !== undefined) this.cancel(timer); + this.timers.delete(slotId); + } + + dispose() { + this.disposed = true; + for (const timer of this.timers.values()) this.cancel(timer); + this.timers.clear(); + this.active.clear(); + } + + private run(slotId: SaveSlotId) { + if (this.disposed || this.active.has(slotId) || this.timers.has(slotId) || !this.pending.has(slotId) || !this.isOnline()) return; + this.active.add(slotId); + void this.upload(slotId).finally(() => this.active.delete(slotId)); + } +} + +const pendingSaveSync = new PendingSaveSyncRepository(); +let activeCoordinator: SaveSyncRetryCoordinator | null = null; + +export function markSaveSyncPending(slotId: SaveSlotId) { + pendingSaveSync.mark(slotId); +} + +export function hasPendingSaveSync(slotId: SaveSlotId) { + return pendingSaveSync.has(slotId); +} + +export function clearSaveSyncPending(slotId: SaveSlotId) { + pendingSaveSync.clear(slotId); + activeCoordinator?.succeeded(slotId); +} + +export function scheduleSaveSyncRetry(slotId: SaveSlotId) { + activeCoordinator?.failed(slotId); +} + +export function startSaveSyncCoordinator(upload: UploadSave) { + const coordinator = new SaveSyncRetryCoordinator(pendingSaveSync, upload); + activeCoordinator?.dispose(); + activeCoordinator = coordinator; + const onOnline = () => coordinator.flush(true); + window.addEventListener("online", onOnline); + coordinator.flush(); + return () => { + window.removeEventListener("online", onOnline); + coordinator.dispose(); + if (activeCoordinator === coordinator) activeCoordinator = null; + }; +} diff --git a/src/frontend/store.ts b/src/frontend/store.ts index ba9fc03..9923471 100644 --- a/src/frontend/store.ts +++ b/src/frontend/store.ts @@ -2,9 +2,27 @@ import { create } from "zustand"; import { DEFAULT_SETTINGS, normalizeHunterName } from "./data"; import { SaveRepository } from "./saveRepository"; import { AccountRepository, type AccountResult } from "./accountRepository"; -import { onlineRepository, type OnlineSaveSlot } from "./onlineRepository"; -import type { AppScreen, GameModeId, GameSettings, HunterSave, SaveSlotId, SaveSlotState } from "./types"; -import type { AbilityId, BossId, HealerClassId, InventoryItem, RunBuffId } from "../game/types"; +import { OnlineApiError, onlineRepository, type OnlineSaveSlot } from "./onlineRepository"; +import { leaderboardCache } from "./leaderboardCache"; +import { + clearSaveSyncPending, + markSaveSyncPending, + networkAppearsOnline, + scheduleSaveSyncRetry, +} from "./saveSync"; +import type { + AppScreen, + GameModeId, + GameSettings, + HunterSave, + ProfileCollectionView, + ProfileStatId, + SaveSlotId, + SaveSlotState, +} from "./types"; +import type { BossGroupId } from "../game/bossCatalog"; +import type { AbilitySlotId, BossId, HealerClassId, InventoryItem, RunBuffId } from "../game/types"; +import { HEALER_CLASS_ORDER } from "../game/healers"; import { RUN_BUFF_ORDER, RUN_BUFFS } from "../game/roguelike"; import { upgradeGearSlot, type GearOwnerId, type GearSlotId } from "../game/progression/gear"; import { @@ -13,20 +31,27 @@ import { infusionsForOwner, } from "../game/progression/infusions"; import { normalizeDifficultySlug, rollBossReward, type BossRewardAward, type DifficultySlug } from "../game/progression/loot"; -import { highestEndlessBossKillsAfterDefeat, highestRoguelikeRoundAfterDefeat } from "../game/progression/hunterStats"; +import { bestAetherAssaultRecord, bestBlockbreakerRecords, bestHockeyHealingRecord, highestEndlessBossKillsAfterDefeat, highestRoguelikeRoundAfterDefeat, hockeyPvpRecordAfterMatch } from "../game/progression/hunterStats"; +import { + cloneCharacterAppearance, + type CharacterAppearanceV1, + type CharacterModelMode, +} from "../game/characterAppearance"; +import { createDefaultHealerAppearance, normalizeHealerAppearance } from "../game/healerVisuals"; const repository = new SaveRepository(); const accounts = new AccountRepository(); const SETTINGS_KEY = "i-want-to-heal:settings:v1"; -const onlineSaveQueues = new Map>(); +const onlineSaveQueues = new Map }>(); function writeServerSaveSerially(save: HunterSave): Promise { const previous = onlineSaveQueues.get(save.slotId); - const next = (previous ? previous.catch(() => save) : Promise.resolve(save)) + if (previous?.updatedAt === save.updatedAt) return previous.promise; + const next = (previous ? previous.promise.catch(() => save) : Promise.resolve(save)) .then(() => onlineRepository.writeSave(save)); - onlineSaveQueues.set(save.slotId, next); + onlineSaveQueues.set(save.slotId, { updatedAt: save.updatedAt, promise: next }); void next.finally(() => { - if (onlineSaveQueues.get(save.slotId) === next) onlineSaveQueues.delete(save.slotId); + if (onlineSaveQueues.get(save.slotId)?.promise === next) onlineSaveQueues.delete(save.slotId); }).catch(() => undefined); return next; } @@ -76,6 +101,16 @@ function replaceOnlineSlot(current: readonly SaveSlotState[], save: HunterSave): return refreshLocalSlots(current).map((slot) => slot.id === save.slotId ? { ...slot, online: save } : slot); } +export type AppearancePreviewAnimation = "idle" | "walk" | "cast"; +export type AppearanceDrafts = Record; + +function appearanceDraftsFor(save: HunterSave | null): AppearanceDrafts { + return Object.fromEntries(HEALER_CLASS_ORDER.map((classId) => [ + classId, + cloneCharacterAppearance(save?.healers[classId].appearance ?? createDefaultHealerAppearance(classId)), + ])) as AppearanceDrafts; +} + export interface FrontendState { screen: AppScreen; accountId: string | null; @@ -89,8 +124,15 @@ export interface FrontendState { selectedGearSlotId: GearSlotId; gearWorkshopMode: "upgrade" | "infusion"; selectedInfusionId: string; - selectedPassiveAbilityId: AbilityId; + selectedPassiveAbilityId: AbilitySlotId; selectedPassiveInfusionId: RunBuffId; + profileCollectionView: ProfileCollectionView; + selectedProfileGroupId: BossGroupId; + selectedProfileStatId: ProfileStatId; + appearanceClassId: HealerClassId; + appearanceDrafts: AppearanceDrafts; + previewMode: CharacterModelMode; + previewAnimation: AppearancePreviewAnimation; recentRewards: BossRewardAward[]; settings: GameSettings; notice: string; @@ -105,7 +147,7 @@ export interface FrontendState { playSlot: (slotId: SaveSlotId) => void; deleteSlot: (slotId: SaveSlotId) => void; copySlot: (sourceId: SaveSlotId, targetId: SaveSlotId) => void; - uploadSlot: (slotId: SaveSlotId) => Promise; + uploadSlot: (slotId: SaveSlotId) => Promise; downloadSlot: (slotId: SaveSlotId) => Promise; selectMode: (mode: GameModeId) => void; selectBoss: (bossId: BossId) => void; @@ -114,8 +156,19 @@ export interface FrontendState { selectGearSlot: (slotId: GearSlotId) => void; selectGearWorkshopMode: (mode: "upgrade" | "infusion") => void; selectInfusion: (infusionId: string) => void; - selectPassiveAbility: (abilityId: AbilityId) => void; + selectPassiveAbility: (abilityId: AbilitySlotId) => void; selectPassiveInfusion: (passiveId: RunBuffId) => void; + selectProfileCollectionView: (view: ProfileCollectionView) => void; + selectProfileGroup: (groupId: BossGroupId) => void; + selectProfileStat: (statId: ProfileStatId) => void; + openAppearanceLab: () => void; + selectAppearanceClass: (classId: HealerClassId) => void; + updateAppearanceDraft: (appearance: CharacterAppearanceV1) => void; + resetAppearanceDraft: () => void; + saveAppearanceDraft: () => boolean; + closeAppearanceLab: () => void; + setAppearancePreviewMode: (mode: CharacterModelMode) => void; + setAppearancePreviewAnimation: (animation: AppearancePreviewAnimation) => void; upgradeSelectedGear: () => boolean; equipSelectedInfusion: () => boolean; equipPassiveInfusion: (passiveId: RunBuffId) => boolean; @@ -126,6 +179,11 @@ export interface FrontendState { recordBossVictory: (bossId: BossId, difficultySlug: DifficultySlug) => BossRewardAward | null; recordRoguelikeDefeat: (round: number) => void; recordRogueTrialsEndlessDefeat: (bossKills: number) => void; + recordHockeyHealingDefeat: (returns: number, durationSeconds: number) => void; + recordHockeyPvpResult: (won: boolean) => void; + recordHockeyPvpBossKill: () => void; + recordBlockbreakerDefeat: (bricks: number, durationSeconds: number, score: number) => void; + recordAetherAssaultDefeat: (score: number, wave: number, durationSeconds: number) => void; clearRecentRewards: () => void; clearNotice: () => void; } @@ -147,8 +205,15 @@ export const useFrontendStore = create((set, get) => ({ selectedGearSlotId: "weapon", gearWorkshopMode: "upgrade", selectedInfusionId: infusionsForOwner("priest")[0].id, - selectedPassiveAbilityId: "mend", + selectedPassiveAbilityId: "ability1", selectedPassiveInfusionId: "mend-echo", + profileCollectionView: "stats", + selectedProfileGroupId: "charge", + selectedProfileStatId: "roguelike", + appearanceClassId: "priest", + appearanceDrafts: appearanceDraftsFor(null), + previewMode: "modular", + previewAnimation: "idle", recentRewards: [], settings: loadSettings(), notice: "", @@ -200,7 +265,15 @@ export const useFrontendStore = create((set, get) => ({ void accounts.logout(); set({ accountId: null, slots: repository.listLocal(), activeSlotId: null, screen: "login", notice: "Signed out. Offline saves remain on this device." }); }, - navigate: (screen) => set({ screen, notice: "" }), + navigate: (screen) => set(screen === "profile" + ? { + screen, + profileCollectionView: "stats", + selectedProfileGroupId: "charge", + selectedProfileStatId: "roguelike", + notice: "", + } + : { screen, notice: "" }), selectSlot: (selectedSlotId) => set({ selectedSlotId, notice: "" }), createSlot: (slotId, rawHunterName) => { const hunterName = normalizeHunterName(rawHunterName); @@ -209,6 +282,7 @@ export const useFrontendStore = create((set, get) => ({ return false; } repository.create(slotId, hunterName); + leaderboardCache.clearSlot(slotId); set((state) => ({ slots: refreshLocalSlots(state.slots), selectedSlotId: slotId, notice: `${hunterName} created in offline slot ${slotId}.` })); return true; }, @@ -219,6 +293,8 @@ export const useFrontendStore = create((set, get) => ({ }, deleteSlot: (slotId) => { repository.deleteLocal(slotId); + leaderboardCache.clearSlot(slotId); + clearSaveSyncPending(slotId); set((state) => ({ slots: refreshLocalSlots(state.slots), activeSlotId: state.activeSlotId === slotId ? null : state.activeSlotId, @@ -228,30 +304,48 @@ export const useFrontendStore = create((set, get) => ({ copySlot: (sourceId, targetId) => { const copy = repository.copyLocal(sourceId, targetId); if (!copy) return; + leaderboardCache.clearSlot(targetId); set((state) => ({ slots: refreshLocalSlots(state.slots), selectedSlotId: targetId, notice: `Slot ${sourceId} copied to slot ${targetId}.` })); }, uploadSlot: async (slotId) => { const { accountId } = get(); - if (!accountId) return set({ notice: "Sign in before syncing online." }); + if (!accountId) { + set({ notice: "Sign in before syncing online." }); + return false; + } const local = repository.listLocal().find((slot) => slot.id === slotId)?.local; - if (!local) return set({ notice: "No offline save to sync." }); + if (!local) { + set({ notice: "No offline save to sync." }); + return false; + } + markSaveSyncPending(slotId); + if (!networkAppearsOnline()) { + set({ notice: `Slot ${slotId} saved locally. Online sync waits for connection.` }); + return false; + } try { const uploaded = await writeServerSaveSerially(local); if (get().accountId === accountId) { set((state) => ({ slots: replaceOnlineSlot(state.slots, uploaded), notice: `Slot ${slotId} synced to TrueNAS.` })); } + clearSaveSyncPending(slotId); + return true; } catch (error) { + const retryable = !(error instanceof OnlineApiError) || error.status === 0 || error.status >= 500; + if (retryable) scheduleSaveSyncRetry(slotId); set({ notice: error instanceof Error ? error.message : "Save upload failed." }); + return false; } }, downloadSlot: async (slotId) => { const { accountId } = get(); if (!accountId) return set({ notice: "Sign in before downloading an online save." }); try { - await onlineSaveQueues.get(slotId)?.catch(() => undefined); + await onlineSaveQueues.get(slotId)?.promise.catch(() => undefined); const serverSave = await onlineRepository.readSave(slotId); if (!serverSave) return set({ notice: "No online version exists for this slot." }); const downloaded = repository.replaceLocal(serverSave); + clearSaveSyncPending(slotId); if (get().accountId === accountId) { set((state) => ({ slots: replaceOnlineSlot(state.slots, downloaded), notice: `Slot ${slotId} downloaded from TrueNAS.` })); } @@ -271,14 +365,88 @@ export const useFrontendStore = create((set, get) => ({ selectGearWorkshopMode: (gearWorkshopMode) => set({ gearWorkshopMode, notice: "" }), selectInfusion: (selectedInfusionId) => set({ selectedInfusionId, notice: "" }), selectPassiveAbility: (selectedPassiveAbilityId) => { - const selectedPassiveInfusionId = RUN_BUFF_ORDER.find((id) => RUN_BUFFS[id].abilityId === selectedPassiveAbilityId) ?? "mend-echo"; + const selectedPassiveInfusionId = RUN_BUFF_ORDER.find((id) => RUN_BUFFS[id].abilitySlotId === selectedPassiveAbilityId) ?? "mend-echo"; set({ selectedPassiveAbilityId, selectedPassiveInfusionId, notice: "" }); }, selectPassiveInfusion: (selectedPassiveInfusionId) => set({ - selectedPassiveAbilityId: RUN_BUFFS[selectedPassiveInfusionId].abilityId, + selectedPassiveAbilityId: RUN_BUFFS[selectedPassiveInfusionId].abilitySlotId, selectedPassiveInfusionId, notice: "", }), + selectProfileCollectionView: (profileCollectionView) => set({ profileCollectionView }), + selectProfileGroup: (selectedProfileGroupId) => set({ selectedProfileGroupId }), + selectProfileStat: (selectedProfileStatId) => set({ selectedProfileStatId }), + openAppearanceLab: () => set((state) => { + const save = activeSave(state.slots, state.activeSlotId); + return { + screen: "appearance", + appearanceClassId: save?.activeClassId ?? state.appearanceClassId, + appearanceDrafts: appearanceDraftsFor(save), + previewMode: "modular", + previewAnimation: "idle", + notice: "Appearance Lab opened. Changes remain drafts until saved.", + }; + }), + selectAppearanceClass: (appearanceClassId) => set({ appearanceClassId, notice: "" }), + updateAppearanceDraft: (appearance) => set((state) => ({ + appearanceDrafts: { + ...state.appearanceDrafts, + [state.appearanceClassId]: cloneCharacterAppearance(appearance), + }, + notice: "Preview updated. Save to keep this look.", + })), + resetAppearanceDraft: () => set((state) => ({ + appearanceDrafts: { + ...state.appearanceDrafts, + [state.appearanceClassId]: createDefaultHealerAppearance(state.appearanceClassId), + }, + notice: "Class default restored in preview. Save to keep it.", + })), + saveAppearanceDraft: () => { + const { activeSlotId, appearanceClassId, appearanceDrafts } = get(); + if (!activeSlotId) { + set({ notice: "Load a hunter save before changing appearance." }); + return false; + } + const appearance = normalizeHealerAppearance(appearanceClassId, appearanceDrafts[appearanceClassId]); + const updated = repository.updateLocal(activeSlotId, (save) => ({ + ...save, + healers: { + ...save.healers, + [appearanceClassId]: { + ...save.healers[appearanceClassId], + appearance, + }, + }, + })); + if (!updated) { + set({ notice: "Appearance could not be saved." }); + return false; + } + markSaveSyncPending(activeSlotId); + set((state) => ({ + slots: refreshLocalSlots(state.slots), + appearanceDrafts: { + ...state.appearanceDrafts, + [appearanceClassId]: cloneCharacterAppearance(appearance), + }, + notice: `${appearanceClassId[0].toUpperCase() + appearanceClassId.slice(1)} appearance saved locally.`, + })); + return true; + }, + closeAppearanceLab: () => set((state) => { + const save = activeSave(state.slots, state.activeSlotId); + return { + screen: "home", + appearanceClassId: save?.activeClassId ?? state.appearanceClassId, + appearanceDrafts: appearanceDraftsFor(save), + previewMode: "modular", + previewAnimation: "idle", + notice: "Appearance Lab closed.", + }; + }), + setAppearancePreviewMode: (previewMode) => set({ previewMode }), + setAppearancePreviewAnimation: (previewAnimation) => set({ previewAnimation }), upgradeSelectedGear: () => { const { activeSlotId, accountId, selectedGearOwnerId, selectedGearSlotId } = get(); if (!activeSlotId) return false; @@ -392,6 +560,7 @@ export const useFrontendStore = create((set, get) => ({ recentRewards: awarded ? [...state.recentRewards, awarded].slice(-12) : state.recentRewards, notice: awarded ? `${awarded.drop.name} x${awarded.quantity} saved.` : "Boss clear saved.", })); + markSaveSyncPending(activeSlotId); return awarded; }, recordRoguelikeDefeat: (round) => { @@ -405,6 +574,7 @@ export const useFrontendStore = create((set, get) => ({ }, })); if (!updated) return; + markSaveSyncPending(activeSlotId); set((state) => ({ slots: refreshLocalSlots(state.slots) })); }, recordRogueTrialsEndlessDefeat: (bossKills) => { @@ -418,6 +588,118 @@ export const useFrontendStore = create((set, get) => ({ }, })); if (!updated) return; + markSaveSyncPending(activeSlotId); + set((state) => ({ slots: refreshLocalSlots(state.slots) })); + }, + recordHockeyHealingDefeat: (returns, durationSeconds) => { + const { activeSlotId } = get(); + if (!activeSlotId) return; + const updated = repository.updateLocal(activeSlotId, (save) => { + const record = bestHockeyHealingRecord( + save.stats.highestHockeyHealingReturns, + save.stats.longestHockeyHealingSecondsAtBest, + returns, + durationSeconds, + ); + return { + ...save, + stats: { + ...save.stats, + highestHockeyHealingReturns: record.returns, + longestHockeyHealingSecondsAtBest: record.durationSeconds, + }, + }; + }); + if (!updated) return; + markSaveSyncPending(activeSlotId); + set((state) => ({ slots: refreshLocalSlots(state.slots) })); + }, + recordHockeyPvpResult: (won) => { + const { activeSlotId } = get(); + if (!activeSlotId) return; + const updated = repository.updateLocal(activeSlotId, (save) => { + const record = hockeyPvpRecordAfterMatch( + save.stats.hockeyHealingPvpWins, + save.stats.hockeyHealingPvpLosses, + won, + ); + return { + ...save, + stats: { + ...save.stats, + hockeyHealingPvpWins: record.wins, + hockeyHealingPvpLosses: record.losses, + }, + }; + }); + if (!updated) return; + markSaveSyncPending(activeSlotId); + set((state) => ({ slots: refreshLocalSlots(state.slots) })); + }, + recordHockeyPvpBossKill: () => { + const { activeSlotId } = get(); + if (!activeSlotId) return; + const updated = repository.updateLocal(activeSlotId, (save) => ({ + ...save, + stats: { + ...save.stats, + hockeyHealingPvpBossKills: save.stats.hockeyHealingPvpBossKills + 1, + }, + })); + if (!updated) return; + markSaveSyncPending(activeSlotId); + set((state) => ({ slots: refreshLocalSlots(state.slots) })); + }, + recordBlockbreakerDefeat: (bricks, durationSeconds, score) => { + const { activeSlotId } = get(); + if (!activeSlotId) return; + const updated = repository.updateLocal(activeSlotId, (save) => { + const record = bestBlockbreakerRecords( + save.stats.highestBlockbreakerBricks, + save.stats.longestBlockbreakerSeconds, + save.stats.highestBlockbreakerScore, + bricks, + durationSeconds, + score, + ); + return { + ...save, + stats: { + ...save.stats, + highestBlockbreakerBricks: record.bricks, + longestBlockbreakerSeconds: record.durationSeconds, + highestBlockbreakerScore: record.score, + }, + }; + }); + if (!updated) return; + markSaveSyncPending(activeSlotId); + set((state) => ({ slots: refreshLocalSlots(state.slots) })); + }, + recordAetherAssaultDefeat: (score, wave, durationSeconds) => { + const { activeSlotId } = get(); + if (!activeSlotId) return; + const updated = repository.updateLocal(activeSlotId, (save) => { + const record = bestAetherAssaultRecord( + save.stats.highestAetherAssaultScore, + save.stats.highestAetherAssaultWaveAtBest, + save.stats.longestAetherAssaultSecondsAtBest, + score, + wave, + durationSeconds, + ); + return { + ...save, + stats: { + ...save.stats, + highestAetherAssaultScore: record.score, + highestAetherAssaultWaveAtBest: record.wave, + longestAetherAssaultSecondsAtBest: record.durationSeconds, + }, + }; + }); + if (!updated) return; + markSaveSyncPending(activeSlotId); set((state) => ({ slots: refreshLocalSlots(state.slots) })); }, clearRecentRewards: () => set({ recentRewards: [] }), @@ -447,6 +729,17 @@ export type FrontendSnapshot = Omit; @@ -485,6 +783,17 @@ export function getFrontendSnapshot(): FrontendSnapshot { selectInfusion: _selectInfusion, selectPassiveAbility: _selectPassiveAbility, selectPassiveInfusion: _selectPassiveInfusion, + selectProfileCollectionView: _selectProfileCollectionView, + selectProfileGroup: _selectProfileGroup, + selectProfileStat: _selectProfileStat, + openAppearanceLab: _openAppearanceLab, + selectAppearanceClass: _selectAppearanceClass, + updateAppearanceDraft: _updateAppearanceDraft, + resetAppearanceDraft: _resetAppearanceDraft, + saveAppearanceDraft: _saveAppearanceDraft, + closeAppearanceLab: _closeAppearanceLab, + setAppearancePreviewMode: _setAppearancePreviewMode, + setAppearancePreviewAnimation: _setAppearancePreviewAnimation, upgradeSelectedGear: _upgradeSelectedGear, equipSelectedInfusion: _equipSelectedInfusion, equipPassiveInfusion: _equipPassiveInfusion, @@ -495,6 +804,11 @@ export function getFrontendSnapshot(): FrontendSnapshot { recordBossVictory: _recordBossVictory, recordRoguelikeDefeat: _recordRoguelikeDefeat, recordRogueTrialsEndlessDefeat: _recordRogueTrialsEndlessDefeat, + recordHockeyHealingDefeat: _recordHockeyHealingDefeat, + recordHockeyPvpResult: _recordHockeyPvpResult, + recordHockeyPvpBossKill: _recordHockeyPvpBossKill, + recordBlockbreakerDefeat: _recordBlockbreakerDefeat, + recordAetherAssaultDefeat: _recordAetherAssaultDefeat, clearRecentRewards: _clearRecentRewards, clearNotice: _clearNotice, ...snapshot diff --git a/src/frontend/types.ts b/src/frontend/types.ts index d74ca73..ffbfe9b 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -2,10 +2,13 @@ import type { BossGroupId } from "../game/bossCatalog"; import type { BossId, HealerClassId, InventoryItem } from "../game/types"; import type { GearProgress } from "../game/progression/gear"; import type { CollectionLog, MaterialStack } from "../game/progression/loot"; +import type { CharacterAppearanceV1 } from "../game/characterAppearance"; export type SaveSlotId = 1 | 2 | 3; -export type AppScreen = "login" | "saves" | "home" | "profile" | "gear" | "settings" | "mode" | "game"; -export type GameModeId = "roguelike-pve" | "rogue-trials" | "dungeons" | "roguelike-pvp" | "stadium-pvp"; +export type AppScreen = "login" | "saves" | "home" | "profile" | "gear" | "appearance" | "settings" | "mode" | "game"; +export type GameModeId = "roguelike-pve" | "rogue-trials" | "dungeons" | "hockey-healing" | "hockey-healing-pvp" | "blockbreaker" | "aether-assault" | "roguelike-pvp" | "stadium-pvp"; +export type ProfileCollectionView = "loot" | "trophies" | "stats"; +export type ProfileStatId = BossId | "roguelike" | "rogue-trials-endless" | "hockey-healing" | "hockey-pvp-wins" | "hockey-pvp-boss-kills" | "blockbreaker-bricks" | "blockbreaker-time" | "blockbreaker-score" | "aether-assault"; export interface CollectionDrop { id: string; @@ -43,15 +46,27 @@ export interface HunterStats { bossKills: Record; highestRoguelikeRound: number; highestRogueTrialsEndlessKills: number; + highestHockeyHealingReturns: number; + longestHockeyHealingSecondsAtBest: number; + hockeyHealingPvpWins: number; + hockeyHealingPvpLosses: number; + hockeyHealingPvpBossKills: number; + highestBlockbreakerBricks: number; + longestBlockbreakerSeconds: number; + highestBlockbreakerScore: number; + highestAetherAssaultScore: number; + highestAetherAssaultWaveAtBest: number; + longestAetherAssaultSecondsAtBest: number; } export interface HealerProgress { level: number; inventory: InventoryItem[]; + appearance: CharacterAppearanceV1; } export interface HunterSave { - schemaVersion: 5; + schemaVersion: 6; slotId: SaveSlotId; hunterName: string; activeClassId: HealerClassId; diff --git a/src/game/abilityLoadout.test.ts b/src/game/abilityLoadout.test.ts new file mode 100644 index 0000000..39b4a54 --- /dev/null +++ b/src/game/abilityLoadout.test.ts @@ -0,0 +1,46 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { createClassAbilityLoadout, HEALER_ABILITIES, HEALER_ABILITY_IDS, HEALER_CLASSES, HEALER_CLASS_ORDER, resolveSlottedAbility } from "./healers"; +import { healingEffect } from "./healerEffects"; +import { useGameStore } from "./store"; + +describe("composable healer ability loadouts", () => { + beforeEach(() => { + useGameStore.getState().configureHealer("priest", "Aelia", []); + }); + + it("projects every class kit through the flat spell registry", () => { + for (const classId of HEALER_CLASS_ORDER) { + const loadout = createClassAbilityLoadout(classId); + for (const [slotId, definition] of Object.entries(HEALER_CLASSES[classId].abilities)) { + expect(resolveSlottedAbility(loadout, slotId as keyof typeof loadout)).toBe(HEALER_ABILITIES[definition.id]); + } + } + expect(new Set(HEALER_ABILITY_IDS).size).toBe(HEALER_ABILITY_IDS.length); + expect(Object.keys(HEALER_ABILITIES)).toEqual(HEALER_ABILITY_IDS); + }); + + it("casts a spell assigned outside its native class and slot", () => { + useGameStore.getState().setAbilityLoadout({ ability1: "shaman-riptide" }); + useGameStore.getState().startEncounter(); + useGameStore.getState().selectMember("brann"); + + expect(useGameStore.getState().castAbility("ability1")).toBe(true); + expect(healingEffect(useGameStore.getState().party[1], "riptide")).toBeDefined(); + }); + + it("resolves cast completion from recorded spell identity after cross-slot assignment", () => { + useGameStore.getState().setAbilityLoadout({ ability6: "druid-regrowth" }); + useGameStore.getState().startEncounter(); + useGameStore.setState((state) => ({ + party: state.party.map((member) => member.id === "brann" ? { ...member, hp: 50 } : member), + })); + useGameStore.getState().selectMember("brann"); + + expect(useGameStore.getState().castAbility("ability6")).toBe(true); + useGameStore.getState().tick(0.51); + + const brann = useGameStore.getState().party[1]; + expect(brann.hp).toBeGreaterThan(50); + expect(healingEffect(brann, "regrowth")).toBeDefined(); + }); +}); diff --git a/src/game/aetherAssault.test.ts b/src/game/aetherAssault.test.ts new file mode 100644 index 0000000..b905e34 --- /dev/null +++ b/src/game/aetherAssault.test.ts @@ -0,0 +1,224 @@ +import { describe, expect, it } from "vitest"; +import { + AETHER_ARMORED_SCORE, + AETHER_DIVE_DAMAGE, + AETHER_ENEMY_SHOT_DAMAGE, + AETHER_MAX_ENEMY_SHOTS, + AETHER_MAX_PLAYER_SHOTS, + AETHER_MAX_SHIPS, + AETHER_STANDARD_SCORE, + advanceAetherAssault, + aetherMultiplier, + aetherShipCount, + aetherWaveClearBonus, + createAetherAssaultState, +} from "./aetherAssault"; + +describe("Aether Assault", () => { + it("creates deterministic eight-ship opening formations", () => { + const first = createAetherAssaultState(true, 42); + const second = createAetherAssaultState(true, 42); + expect(first.ships).toEqual(second.ships); + expect(first.ships).toHaveLength(8); + expect(first.wave).toBe(1); + }); + + it("grows formations by two ships and caps them at twenty", () => { + expect(aetherShipCount(1)).toBe(8); + expect(aetherShipCount(4)).toBe(14); + expect(aetherShipCount(99)).toBe(AETHER_MAX_SHIPS); + }); + + it("fires five fixed-forward player shots per second only while enabled", () => { + let state = createAetherAssaultState(true, 7); + state = advanceAetherAssault(state, { + delta: 0.8, + time: 0.8, + playerPosition: [3, 8], + canAutoFire: true, + }).state; + expect(state.playerShots.length).toBe(5); + expect(state.playerShots.every((shot) => shot.velocity[0] === 0 && shot.velocity[1] < 0)).toBe(true); + + const paused = advanceAetherAssault(state, { + delta: 1, + time: 1.8, + playerPosition: [3, 8], + canAutoFire: false, + }).state; + expect(paused.playerShots.length).toBeLessThanOrEqual(state.playerShots.length); + }); + + it("awards kills, streak multiplier, armored points, and wave bonuses", () => { + expect(aetherMultiplier(0)).toBe(1); + expect(aetherMultiplier(5)).toBe(1.25); + expect(aetherMultiplier(100)).toBe(5); + expect(AETHER_STANDARD_SCORE).toBe(100); + expect(AETHER_ARMORED_SCORE).toBe(250); + expect(aetherWaveClearBonus(3)).toBe(2250); + }); + + it("applies the fifth-kill multiplier to an armored kill", () => { + const state = createAetherAssaultState(true, 17); + const ship = state.ships[0]; + ship.kind = "armored"; + ship.hp = 1; + ship.maxHp = 3; + ship.phase = "formation"; + ship.phaseStartedAt = 0; + ship.position = [0, -5]; + ship.formationPosition = [0, -5]; + state.ships = [ship]; + state.killStreak = 4; + state.multiplier = 1; + state.playerShots = [{ id: 99, position: [0, 5], velocity: [0, -18] }]; + state.nextPlayerShotAt = 10; + + const next = advanceAetherAssault(state, { + delta: 0.75, + time: 1, + playerPosition: [4, 8], + canAutoFire: false, + }).state; + expect(next.killStreak).toBe(5); + expect(next.multiplier).toBe(1.25); + expect(next.lastKillScore).toBe(Math.round(AETHER_ARMORED_SCORE * 1.25)); + expect(next.score).toBe(next.lastKillScore + aetherWaveClearBonus(1)); + }); + + it("uses segment collision for fast player projectiles", () => { + const state = createAetherAssaultState(true, 9); + const ship = state.ships[0]; + ship.phase = "formation"; + ship.phaseStartedAt = 0; + ship.position = [0, -5]; + ship.formationPosition = [0, -5]; + state.ships = [ship]; + state.playerShots = [{ id: 99, position: [0, 5], velocity: [0, -18] }]; + state.nextPlayerShotAt = 10; + + const next = advanceAetherAssault(state, { + delta: 0.75, + time: 1, + playerPosition: [4, 8], + canAutoFire: false, + }).state; + expect(next.ships).toHaveLength(0); + expect(next.kills).toBe(1); + expect(next.score).toBe(AETHER_STANDARD_SCORE + aetherWaveClearBonus(1)); + }); + + it("damages only through returned player damage and resets streak with hit grace", () => { + const state = createAetherAssaultState(true, 3); + state.killStreak = 8; + state.multiplier = aetherMultiplier(8); + state.enemyShots = [ + { id: 1, position: [0, 7], velocity: [0, 5] }, + { id: 2, position: [0.1, 7], velocity: [0, 5] }, + ]; + state.nextPlayerShotAt = 10; + + const hit = advanceAetherAssault(state, { + delta: 0.4, + time: 1, + playerPosition: [0, 8], + canAutoFire: false, + }); + expect(hit.playerDamage).toBe(AETHER_ENEMY_SHOT_DAMAGE); + expect(hit.state.killStreak).toBe(0); + expect(hit.state.multiplier).toBe(1); + }); + + it("launches formation dives and resolves dive contact once", () => { + const state = createAetherAssaultState(true, 23); + const ship = state.ships[0]; + ship.phase = "formation"; + ship.phaseStartedAt = 0; + ship.position = [0, -5]; + ship.formationPosition = [0, -5]; + state.ships = [ship]; + state.nextDiveAt = 0; + state.nextEnemyShotAt = 99; + state.nextPlayerShotAt = 99; + + const launched = advanceAetherAssault(state, { + delta: 0.01, + time: 0.01, + playerPosition: [3, 8], + canAutoFire: false, + }).state; + expect(launched.ships[0].phase).toBe("diving"); + expect(launched.ships[0].targetPosition[0]).toBe(3); + + launched.ships[0].position = [0, 0]; + launched.ships[0].startPosition = [0, 0]; + launched.ships[0].targetPosition = [0, 0]; + launched.ships[0].phaseStartedAt = 0; + launched.nextDiveAt = 99; + const contact = advanceAetherAssault(launched, { + delta: 0.1, + time: 0.1, + playerPosition: [0, 0], + canAutoFire: false, + }); + expect(contact.playerDamage).toBe(AETHER_DIVE_DAMAGE); + expect(contact.state.ships[0].contactResolved).toBe(true); + }); + + it("advances to a ten-ship second wave after the clear delay", () => { + const state = createAetherAssaultState(true, 31); + const ship = state.ships[0]; + ship.phase = "formation"; + ship.phaseStartedAt = 0; + ship.position = [0, -5]; + ship.formationPosition = [0, -5]; + state.ships = [ship]; + state.playerShots = [{ id: 99, position: [0, 5], velocity: [0, -18] }]; + state.nextPlayerShotAt = 99; + state.nextEnemyShotAt = 99; + state.nextDiveAt = 99; + + const cleared = advanceAetherAssault(state, { + delta: 0.75, + time: 1, + playerPosition: [4, 8], + canAutoFire: false, + }).state; + expect(cleared.nextWaveAt).toBe(2.5); + + const next = advanceAetherAssault(cleared, { + delta: 1.6, + time: 2.6, + playerPosition: [4, 8], + canAutoFire: false, + }).state; + expect(next.wave).toBe(2); + expect(next.ships).toHaveLength(10); + expect(next.nextWaveAt).toBeNull(); + }); + + it("never exceeds projectile caps", () => { + const state = createAetherAssaultState(true, 12); + state.nextPlayerShotAt = 0; + state.nextEnemyShotAt = 0; + state.playerShots = Array.from({ length: AETHER_MAX_PLAYER_SHOTS }, (_, id) => ({ + id, + position: [9, 0] as [number, number], + velocity: [0, -18] as [number, number], + })); + state.enemyShots = Array.from({ length: AETHER_MAX_ENEMY_SHOTS }, (_, id) => ({ + id: 100 + id, + position: [9, -8] as [number, number], + velocity: [0, 5] as [number, number], + })); + + const next = advanceAetherAssault(state, { + delta: 0.01, + time: 0.01, + playerPosition: [-9, 12], + canAutoFire: true, + }).state; + expect(next.playerShots.length).toBeLessThanOrEqual(AETHER_MAX_PLAYER_SHOTS); + expect(next.enemyShots.length).toBeLessThanOrEqual(AETHER_MAX_ENEMY_SHOTS); + }); +}); diff --git a/src/game/aetherAssault.ts b/src/game/aetherAssault.ts new file mode 100644 index 0000000..b987487 --- /dev/null +++ b/src/game/aetherAssault.ts @@ -0,0 +1,432 @@ +import { + HOCKEY_ARENA_MAX_X, + HOCKEY_ARENA_MAX_Z, + HOCKEY_ARENA_MIN_X, + HOCKEY_ARENA_MIN_Z, +} from "./hockeyHealing"; +import type { WorldPosition } from "./types"; + +export type AetherAssaultStatus = "inactive" | "live"; +export type AetherShipKind = "standard" | "armored"; +export type AetherShipPhase = "entering" | "formation" | "diving" | "returning"; + +export interface AetherShip { + id: string; + kind: AetherShipKind; + hp: number; + maxHp: number; + position: WorldPosition; + formationPosition: WorldPosition; + phase: AetherShipPhase; + phaseStartedAt: number; + phaseEndsAt: number; + startPosition: WorldPosition; + targetPosition: WorldPosition; + contactResolved: boolean; +} + +export interface AetherProjectile { + id: number; + position: WorldPosition; + velocity: WorldPosition; +} + +export interface AetherAssaultState { + status: AetherAssaultStatus; + seed: number; + randomState: number; + wave: number; + score: number; + kills: number; + killStreak: number; + multiplier: number; + ships: AetherShip[]; + playerShots: AetherProjectile[]; + enemyShots: AetherProjectile[]; + nextProjectileId: number; + nextPlayerShotAt: number; + nextEnemyShotAt: number; + nextDiveAt: number; + nextWaveAt: number | null; + lastPlayerHitAt: number; + lastKillAt: number; + lastKillScore: number; +} + +export interface AetherAssaultStep { + delta: number; + time: number; + playerPosition: WorldPosition; + canAutoFire: boolean; +} + +export interface AetherAssaultAdvance { + state: AetherAssaultState; + playerDamage: number; +} + +export const AETHER_MAX_SHIPS = 20; +export const AETHER_MAX_PLAYER_SHOTS = 32; +export const AETHER_MAX_ENEMY_SHOTS = 64; +export const AETHER_PLAYER_SHOT_DAMAGE = 1; +export const AETHER_ENEMY_SHOT_DAMAGE = 10; +export const AETHER_DIVE_DAMAGE = 24; +export const AETHER_HIT_GRACE_SECONDS = 0.6; +export const AETHER_PLAYER_SHOTS_PER_SECOND = 5; +export const AETHER_WAVE_CLEAR_DELAY = 1.5; +export const AETHER_STANDARD_SCORE = 100; +export const AETHER_ARMORED_SCORE = 250; + +const PLAYER_SHOT_SPEED = 18; +const PLAYER_SHOT_RADIUS = 0.24; +const ENEMY_SHOT_RADIUS = 0.3; +const SHIP_RADIUS = 0.72; +const PLAYER_HIT_RADIUS = 0.62; +const DIVE_HIT_RADIUS = 1.05; +const ENTRY_DURATION = 1.45; +const RETURN_DURATION = 1.25; +const DIVE_DURATION = 2.15; + +function normalizeSeed(seed: number) { + const normalized = Math.floor(Number(seed)) >>> 0; + return normalized || 0x9e3779b9; +} + +function nextRandom(state: number) { + let next = normalizeSeed(state); + next ^= next << 13; + next ^= next >>> 17; + next ^= next << 5; + return next >>> 0; +} + +function randomUnit(state: number) { + const next = nextRandom(state); + return { state: next, value: next / 0x100000000 }; +} + +export function createAetherAssaultSeed(random: () => number = Math.random) { + const sample = Number(random()); + const normalized = Number.isFinite(sample) ? Math.max(0, Math.min(0.999999999999, sample)) : 0; + return normalizeSeed(Math.floor(normalized * 0x100000000)); +} + +export function aetherShipCount(wave: number) { + return Math.min(AETHER_MAX_SHIPS, 8 + Math.max(0, Math.floor(wave) - 1) * 2); +} + +export function aetherMultiplier(killStreak: number) { + return Math.min(5, 1 + Math.floor(Math.max(0, killStreak) / 5) * 0.25); +} + +export function aetherWaveClearBonus(wave: number) { + return Math.max(1, Math.floor(wave)) * 750; +} + +function formationPosition(index: number, count: number): WorldPosition { + const columns = 5; + const rows = Math.ceil(count / columns); + const row = Math.floor(index / columns); + const column = index % columns; + const itemsInRow = row === rows - 1 && count % columns !== 0 ? count % columns : columns; + const centeredColumn = column - (itemsInRow - 1) * 0.5; + return [centeredColumn * 3.35, -10.7 + row * 2.25]; +} + +function armoredCount(wave: number, count: number) { + if (wave % 3 !== 0) return 0; + return Math.min(count, 1 + Math.floor(wave / 6)); +} + +function createWave(wave: number, startsAt: number, randomState: number) { + const count = aetherShipCount(wave); + const armored = armoredCount(wave, count); + const ships: AetherShip[] = []; + let nextState = randomState; + for (let index = 0; index < count; index += 1) { + const random = randomUnit(nextState); + nextState = random.state; + const formation = formationPosition(index, count); + const entersFromLeft = index % 2 === 0; + const start: WorldPosition = [ + entersFromLeft ? HOCKEY_ARENA_MIN_X - 4 - random.value * 3 : HOCKEY_ARENA_MAX_X + 4 + random.value * 3, + HOCKEY_ARENA_MIN_Z - 2.5 - (index % 4) * 0.65, + ]; + const kind: AetherShipKind = index >= count - armored ? "armored" : "standard"; + const spawnAt = startsAt + index * 0.11; + ships.push({ + id: `${wave}:${index}`, + kind, + hp: kind === "armored" ? 3 : 1, + maxHp: kind === "armored" ? 3 : 1, + position: [...start], + formationPosition: formation, + phase: "entering", + phaseStartedAt: spawnAt, + phaseEndsAt: spawnAt + ENTRY_DURATION, + startPosition: [...start], + targetPosition: [...formation], + contactResolved: false, + }); + } + return { ships, randomState: nextState }; +} + +export function createAetherAssaultState(active = false, requestedSeed = 1): AetherAssaultState { + const seed = normalizeSeed(requestedSeed); + const wave = createWave(1, 0, seed); + return { + status: active ? "live" : "inactive", + seed, + randomState: wave.randomState, + wave: 1, + score: 0, + kills: 0, + killStreak: 0, + multiplier: 1, + ships: active ? wave.ships : [], + playerShots: [], + enemyShots: [], + nextProjectileId: 1, + nextPlayerShotAt: 0, + nextEnemyShotAt: 1.2, + nextDiveAt: 4, + nextWaveAt: null, + lastPlayerHitAt: Number.NEGATIVE_INFINITY, + lastKillAt: Number.NEGATIVE_INFINITY, + lastKillScore: 0, + }; +} + +function cloneShip(ship: AetherShip): AetherShip { + return { + ...ship, + position: [...ship.position], + formationPosition: [...ship.formationPosition], + startPosition: [...ship.startPosition], + targetPosition: [...ship.targetPosition], + }; +} + +function easeOutCubic(value: number) { + return 1 - (1 - value) ** 3; +} + +function lerpPosition(start: WorldPosition, end: WorldPosition, progress: number): WorldPosition { + return [start[0] + (end[0] - start[0]) * progress, start[1] + (end[1] - start[1]) * progress]; +} + +function divePosition(ship: AetherShip, progress: number): WorldPosition { + const controlX = ship.targetPosition[0] + Math.sign(ship.targetPosition[0] - ship.startPosition[0] || 1) * 3.2; + const controlZ = (ship.startPosition[1] + ship.targetPosition[1]) * 0.5 - 1.5; + const inverse = 1 - progress; + return [ + inverse * inverse * ship.startPosition[0] + 2 * inverse * progress * controlX + progress * progress * ship.targetPosition[0], + inverse * inverse * ship.startPosition[1] + 2 * inverse * progress * controlZ + progress * progress * ship.targetPosition[1], + ]; +} + +function segmentDistanceSquared(start: WorldPosition, end: WorldPosition, point: WorldPosition) { + const dx = end[0] - start[0]; + const dz = end[1] - start[1]; + const lengthSquared = dx * dx + dz * dz; + const projection = lengthSquared < 0.000001 + ? 0 + : Math.max(0, Math.min(1, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dz) / lengthSquared)); + const nearestX = start[0] + dx * projection; + const nearestZ = start[1] + dz * projection; + return (point[0] - nearestX) ** 2 + (point[1] - nearestZ) ** 2; +} + +function canDamagePlayer(state: AetherAssaultState, time: number) { + return time - state.lastPlayerHitAt >= AETHER_HIT_GRACE_SECONDS; +} + +function registerPlayerHit(state: AetherAssaultState, time: number, damage: number) { + if (!canDamagePlayer(state, time)) return 0; + state.lastPlayerHitAt = time; + state.killStreak = 0; + state.multiplier = 1; + return damage; +} + +function awardKill(state: AetherAssaultState, ship: AetherShip, time: number) { + state.kills += 1; + state.killStreak += 1; + state.multiplier = aetherMultiplier(state.killStreak); + const base = ship.kind === "armored" ? AETHER_ARMORED_SCORE : AETHER_STANDARD_SCORE; + const award = Math.round(base * state.multiplier); + state.score += award; + state.lastKillAt = time; + state.lastKillScore = award; +} + +function chooseShip(state: AetherAssaultState, candidates: readonly AetherShip[]) { + if (!candidates.length) return undefined; + const random = randomUnit(state.randomState); + state.randomState = random.state; + return candidates[Math.min(candidates.length - 1, Math.floor(random.value * candidates.length))]; +} + +function enemyFireInterval(wave: number) { + return Math.max(0.52, 1.85 - Math.max(0, wave - 1) * 0.055); +} + +function diveInterval(wave: number) { + return Math.max(1.7, 4.4 - Math.max(0, wave - 1) * 0.11); +} + +export function advanceAetherAssault(source: AetherAssaultState, step: AetherAssaultStep): AetherAssaultAdvance { + if (source.status !== "live" || step.delta <= 0) return { state: source, playerDamage: 0 }; + + const state: AetherAssaultState = { + ...source, + ships: source.ships.map(cloneShip), + playerShots: source.playerShots.map((shot) => ({ ...shot, position: [...shot.position], velocity: [...shot.velocity] })), + enemyShots: source.enemyShots.map((shot) => ({ ...shot, position: [...shot.position], velocity: [...shot.velocity] })), + }; + let playerDamage = 0; + + if (step.canAutoFire) { + while (state.nextPlayerShotAt <= step.time && state.playerShots.length < AETHER_MAX_PLAYER_SHOTS) { + state.playerShots.push({ + id: state.nextProjectileId, + position: [step.playerPosition[0], step.playerPosition[1] - 0.7], + velocity: [0, -PLAYER_SHOT_SPEED], + }); + state.nextProjectileId += 1; + state.nextPlayerShotAt += 1 / AETHER_PLAYER_SHOTS_PER_SECOND; + } + } else if (state.nextPlayerShotAt < step.time) { + state.nextPlayerShotAt = step.time; + } + + for (const ship of state.ships) { + if (step.time < ship.phaseStartedAt) continue; + const previous = [...ship.position] as WorldPosition; + if (ship.phase === "entering") { + const progress = Math.max(0, Math.min(1, (step.time - ship.phaseStartedAt) / Math.max(0.001, ship.phaseEndsAt - ship.phaseStartedAt))); + ship.position = lerpPosition(ship.startPosition, ship.formationPosition, easeOutCubic(progress)); + if (progress >= 1) { + ship.phase = "formation"; + ship.position = [...ship.formationPosition]; + } + } else if (ship.phase === "diving") { + const progress = Math.max(0, Math.min(1, (step.time - ship.phaseStartedAt) / DIVE_DURATION)); + ship.position = divePosition(ship, progress); + if (!ship.contactResolved + && segmentDistanceSquared(previous, ship.position, step.playerPosition) <= DIVE_HIT_RADIUS ** 2) { + ship.contactResolved = true; + playerDamage += registerPlayerHit(state, step.time, AETHER_DIVE_DAMAGE); + } + if (progress >= 1) { + ship.phase = "returning"; + ship.phaseStartedAt = step.time; + ship.phaseEndsAt = step.time + RETURN_DURATION; + ship.startPosition = [...ship.position]; + ship.targetPosition = [...ship.formationPosition]; + } + } else if (ship.phase === "returning") { + const progress = Math.max(0, Math.min(1, (step.time - ship.phaseStartedAt) / RETURN_DURATION)); + ship.position = lerpPosition(ship.startPosition, ship.formationPosition, easeOutCubic(progress)); + if (progress >= 1) { + ship.phase = "formation"; + ship.position = [...ship.formationPosition]; + ship.contactResolved = false; + } + } + } + + if (step.time >= state.nextDiveAt) { + const diver = chooseShip(state, state.ships.filter((ship) => ship.phase === "formation")); + if (diver) { + diver.phase = "diving"; + diver.phaseStartedAt = step.time; + diver.phaseEndsAt = step.time + DIVE_DURATION; + diver.startPosition = [...diver.position]; + diver.targetPosition = [step.playerPosition[0], HOCKEY_ARENA_MAX_Z + 1.8]; + diver.contactResolved = false; + state.nextDiveAt = step.time + diveInterval(state.wave); + } else { + state.nextDiveAt = step.time + 0.25; + } + } + + if (step.time >= state.nextEnemyShotAt && state.enemyShots.length < AETHER_MAX_ENEMY_SHOTS) { + const shooter = chooseShip(state, state.ships.filter((ship) => ship.phase === "formation" || ship.phase === "diving")); + if (shooter) { + const dx = step.playerPosition[0] - shooter.position[0]; + const dz = step.playerPosition[1] - shooter.position[1]; + const length = Math.max(0.001, Math.hypot(dx, dz)); + const speed = Math.min(8.5, 5.6 + state.wave * 0.08); + state.enemyShots.push({ + id: state.nextProjectileId, + position: [...shooter.position], + velocity: [dx / length * speed, dz / length * speed], + }); + state.nextProjectileId += 1; + state.nextEnemyShotAt = step.time + enemyFireInterval(state.wave); + } else { + state.nextEnemyShotAt = step.time + 0.25; + } + } + + const survivingPlayerShots: AetherProjectile[] = []; + for (const shot of state.playerShots) { + const start = [...shot.position] as WorldPosition; + const end: WorldPosition = [start[0] + shot.velocity[0] * step.delta, start[1] + shot.velocity[1] * step.delta]; + let hit: AetherShip | undefined; + let hitDistance = Number.POSITIVE_INFINITY; + for (const ship of state.ships) { + if (step.time < ship.phaseStartedAt) continue; + if (segmentDistanceSquared(start, end, ship.position) > (SHIP_RADIUS + PLAYER_SHOT_RADIUS) ** 2) continue; + const distance = Math.hypot(ship.position[0] - start[0], ship.position[1] - start[1]); + if (distance < hitDistance) { + hit = ship; + hitDistance = distance; + } + } + if (hit) { + hit.hp -= AETHER_PLAYER_SHOT_DAMAGE; + if (hit.hp <= 0) awardKill(state, hit, step.time); + continue; + } + shot.position = end; + if (end[1] >= HOCKEY_ARENA_MIN_Z - 3 && Math.abs(end[0]) <= HOCKEY_ARENA_MAX_X + 4) survivingPlayerShots.push(shot); + } + state.playerShots = survivingPlayerShots; + state.ships = state.ships.filter((ship) => ship.hp > 0); + + const survivingEnemyShots: AetherProjectile[] = []; + for (const shot of state.enemyShots) { + const start = [...shot.position] as WorldPosition; + const end: WorldPosition = [start[0] + shot.velocity[0] * step.delta, start[1] + shot.velocity[1] * step.delta]; + if (segmentDistanceSquared(start, end, step.playerPosition) <= (PLAYER_HIT_RADIUS + ENEMY_SHOT_RADIUS) ** 2) { + playerDamage += registerPlayerHit(state, step.time, AETHER_ENEMY_SHOT_DAMAGE); + continue; + } + shot.position = end; + if (end[1] <= HOCKEY_ARENA_MAX_Z + 3 + && end[1] >= HOCKEY_ARENA_MIN_Z - 3 + && Math.abs(end[0]) <= HOCKEY_ARENA_MAX_X + 4) { + survivingEnemyShots.push(shot); + } + } + state.enemyShots = survivingEnemyShots; + + if (state.ships.length === 0 && state.nextWaveAt === null) { + state.score += aetherWaveClearBonus(state.wave); + state.nextWaveAt = step.time + AETHER_WAVE_CLEAR_DELAY; + } + if (state.nextWaveAt !== null && step.time >= state.nextWaveAt) { + state.wave += 1; + const wave = createWave(state.wave, step.time, state.randomState); + state.ships = wave.ships; + state.randomState = wave.randomState; + state.nextWaveAt = null; + state.nextEnemyShotAt = step.time + Math.min(1.2, enemyFireInterval(state.wave)); + state.nextDiveAt = step.time + Math.min(3, diveInterval(state.wave)); + } + + return { state, playerDamage }; +} diff --git a/src/game/aetherAssaultStore.test.ts b/src/game/aetherAssaultStore.test.ts new file mode 100644 index 0000000..a6de7df --- /dev/null +++ b/src/game/aetherAssaultStore.test.ts @@ -0,0 +1,124 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { createClassInventory } from "./healers"; +import { HOCKEY_ARENA_MAX_Z, HOCKEY_ARENA_MIN_Z } from "./hockeyHealing"; +import { useGameStore } from "./store"; + +function disableBossPressure() { + useGameStore.setState((state) => ({ + boss: { ...state.boss, nextMeleeAt: 999 }, + additionalBosses: state.additionalBosses.map((entry) => ({ + ...entry, + boss: { ...entry.boss, nextMeleeAt: 999 }, + motion: { ...entry.motion, nextMechanicAt: 999 }, + })), + bossMotion: { ...state.bossMotion, nextMechanicAt: 999 }, + })); +} + +describe("Aether Assault store integration", () => { + beforeEach(() => { + useGameStore.getState().configureHealer( + "priest", + "Aelia", + createClassInventory("priest"), + ["bulldrome", "broodfang-spider"], + "aether-assault", + ); + useGameStore.getState().startEncounter(); + disableBossPressure(); + }); + + it("keeps auto-fire running while the healer casts and uses the global cooldown", () => { + useGameStore.setState((state) => ({ + party: state.party.map((member) => member.id === "brann" ? { ...member, hp: member.hp - 40 } : member), + })); + useGameStore.getState().selectMember("brann"); + + expect(useGameStore.getState().castAbility("ability1")).toBe(true); + expect(useGameStore.getState().activeCast).not.toBeNull(); + useGameStore.getState().tick(0.21); + + const state = useGameStore.getState(); + expect(state.activeCast).not.toBeNull(); + expect(state.globalCooldownUntil).toBeGreaterThan(state.time); + expect(state.aetherAssault.playerShots.length).toBeGreaterThanOrEqual(2); + }); + + it("lets the healer traverse the full rink", () => { + useGameStore.getState().setPlayerPosition([0, -100]); + expect(useGameStore.getState().playerPosition[1]).toBeCloseTo(HOCKEY_ARENA_MIN_Z + 0.65); + + useGameStore.getState().setPlayerPosition([0, 100]); + expect(useGameStore.getState().playerPosition[1]).toBeCloseTo(HOCKEY_ARENA_MAX_Z - 0.65); + }); + + it("applies mitigated ship damage only to the healer and grants hit protection", () => { + const before = useGameStore.getState(); + const healerPosition = before.partyPositions.aelia; + const initialHp = before.party.map((member) => member.hp); + useGameStore.setState((state) => ({ + barrier: { ...state.barrier, kind: "barrier", center: [...healerPosition], expiresAt: 10 }, + aetherAssault: { + ...state.aetherAssault, + killStreak: 5, + multiplier: 1.25, + enemyShots: [{ id: 900, position: [...healerPosition], velocity: [0, 0] }], + }, + })); + + useGameStore.getState().tick(0.01); + const afterFirst = useGameStore.getState(); + expect(afterFirst.party[0].hp).toBeCloseTo(initialHp[0] - 7); + expect(afterFirst.party.slice(1).map((member) => member.hp)).toEqual(initialHp.slice(1)); + expect(afterFirst.aetherAssault.killStreak).toBe(0); + expect(afterFirst.aetherAssault.multiplier).toBe(1); + + useGameStore.setState((state) => ({ + aetherAssault: { + ...state.aetherAssault, + enemyShots: [{ id: 901, position: [...state.partyPositions.aelia], velocity: [0, 0] }], + }, + })); + useGameStore.getState().tick(0.1); + expect(useGameStore.getState().party[0].hp).toBeCloseTo(afterFirst.party[0].hp); + }); + + it("keeps boss replacement independent from arcade waves", () => { + const originalInstance = useGameStore.getState().bossInstanceId; + useGameStore.setState((state) => ({ boss: { ...state.boss, hp: 1 } })); + useGameStore.getState().tick(2); + expect(useGameStore.getState().endlessBossKills).toBe(1); + expect(useGameStore.getState().boss.hp).toBe(0); + + useGameStore.getState().tick(2); + useGameStore.getState().tick(1.3); + + const state = useGameStore.getState(); + expect(state.endlessBossKills).toBe(1); + expect(state.boss.hp).toBeGreaterThan(0); + expect(state.bossInstanceId).not.toBe(originalInstance); + expect(state.aetherAssault.status).toBe("live"); + }); + + it("pauses arcade simulation, resets a run, and ends only on party wipe", () => { + useGameStore.getState().tick(0.21); + const beforePause = useGameStore.getState().aetherAssault; + useGameStore.getState().setPaused(true); + useGameStore.getState().tick(1); + expect(useGameStore.getState().aetherAssault).toBe(beforePause); + + useGameStore.getState().setPaused(false); + useGameStore.setState((state) => ({ + party: state.party.map((member) => ({ ...member, hp: 0 })), + })); + useGameStore.getState().tick(0.01); + expect(useGameStore.getState().phase).toBe("defeat"); + + useGameStore.getState().restart(); + const restarted = useGameStore.getState(); + expect(restarted.phase).toBe("briefing"); + expect(restarted.aetherAssault.wave).toBe(1); + expect(restarted.aetherAssault.score).toBe(0); + expect(restarted.aetherAssault.ships).toHaveLength(8); + }); +}); diff --git a/src/game/appearanceLab.test.ts b/src/game/appearanceLab.test.ts new file mode 100644 index 0000000..42e0c14 --- /dev/null +++ b/src/game/appearanceLab.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import { createDefaultHealerAppearance } from "./healerVisuals"; +import { + APPEARANCE_SLOT_DEFINITIONS, + appearanceSlotEnabled, + appearanceSlotLabel, + appearancesMatch, + cycleAppearanceSlot, +} from "./appearanceLab"; +import { weaponDefinitionsForSlot } from "./weaponCatalog"; + +describe("appearance lab choices", () => { + it("cycles every visible slot without mutating the saved appearance", () => { + const original = createDefaultHealerAppearance("priest"); + let draft = original; + for (const slot of APPEARANCE_SLOT_DEFINITIONS) { + const before = appearanceSlotLabel(draft, slot.id); + draft = cycleAppearanceSlot(draft, slot.id, 1); + expect(appearanceSlotLabel(draft, slot.id), slot.id).not.toBe(before); + } + expect(createDefaultHealerAppearance("priest")).toEqual(original); + expect(appearancesMatch(draft, original)).toBe(false); + }); + + it("wraps choices in both directions", () => { + const original = createDefaultHealerAppearance("druid"); + const previous = cycleAppearanceSlot(original, "main-hand", -1); + const restored = cycleAppearanceSlot(previous, "main-hand", 1); + expect(restored.mainHand).toEqual(original.mainHand); + }); + + it("makes every imported held design reachable without a 49-item flat cycle", () => { + let appearance = createDefaultHealerAppearance("priest"); + const firstFamily = appearanceSlotLabel(appearance, "main-hand-family"); + const visitedFamilies = new Set(); + const visitedMainHands = new Set(); + do { + const family = appearanceSlotLabel(appearance, "main-hand-family"); + expect(visitedFamilies.has(family)).toBe(false); + visitedFamilies.add(family); + const firstModelId = appearance.mainHand.modelId; + do { + visitedMainHands.add(appearance.mainHand.modelId); + appearance = cycleAppearanceSlot(appearance, "main-hand", 1); + } while (appearance.mainHand.modelId !== firstModelId); + appearance = cycleAppearanceSlot(appearance, "main-hand-family", 1); + } while (appearanceSlotLabel(appearance, "main-hand-family") !== firstFamily); + + expect(visitedFamilies).toHaveLength(10); + expect(visitedMainHands).toHaveLength(49); + expect(visitedMainHands).toEqual(new Set(weaponDefinitionsForSlot("main").map((entry) => entry.id))); + }); + + it("preserves a saved offhand while a two-handed style disables its row", () => { + const paladin = createDefaultHealerAppearance("paladin"); + const twoHanded = { + ...paladin, + mainHand: { modelId: "cc/staff_d", grip: "staff" } as const, + }; + expect(appearanceSlotEnabled(twoHanded, "off-hand")).toBe(false); + expect(appearanceSlotLabel(twoHanded, "off-hand")).toBe("Hidden by two-hander"); + expect(cycleAppearanceSlot(twoHanded, "off-hand", 1)).toBe(twoHanded); + expect(twoHanded.offHand).toEqual(paladin.offHand); + }); +}); diff --git a/src/game/appearanceLab.ts b/src/game/appearanceLab.ts new file mode 100644 index 0000000..d98cf49 --- /dev/null +++ b/src/game/appearanceLab.ts @@ -0,0 +1,203 @@ +import type { + CharacterAppearanceV1, + CharacterHeldItemVisual, + CharacterPartIdFor, +} from "./characterAppearance"; +import { + heldItemForModel, + weaponDefinition, + weaponDefinitionsForSlot, + weaponUsesBothHands, + type CharacterWeaponCategory, + type CharacterWeaponModelId, + type WeaponDefinition, +} from "./weaponCatalog"; + +export type AppearanceSlotId = "head" | "upper" | "lower" | "headwear" | "back" | "main-hand-family" | "main-hand" | "off-hand"; + +interface AppearanceChoice { + value: Value; + label: string; +} + +export interface AppearanceSlotDefinition { + id: AppearanceSlotId; + label: string; + assetNote: string; +} + +export const APPEARANCE_SLOT_DEFINITIONS: readonly AppearanceSlotDefinition[] = [ + { id: "head", label: "Face + hair", assetNote: "Combined in current character assets" }, + { id: "upper", label: "Shirt + arms", assetNote: "Combined in current character assets" }, + { id: "lower", label: "Pants + shoes", assetNote: "Combined in current character assets" }, + { id: "headwear", label: "Hat", assetNote: "Separate skinned equipment" }, + { id: "back", label: "Back item", assetNote: "Capes, packs, and skinned quiver" }, + { id: "main-hand-family", label: "Weapon type", assetNote: "10 Claudecraft families" }, + { id: "main-hand", label: "Weapon style", assetNote: "49 imported designs" }, + { id: "off-hand", label: "Offhand", assetNote: "Saved while a two-hander is active" }, +] as const; + +const HEAD_CHOICES: readonly AppearanceChoice>[] = [ + { value: "druid-head", label: "Grove" }, + { value: "mage-head", label: "Mystic" }, + { value: "ranger-head", label: "Wayfinder" }, + { value: "knight-head", label: "Vanguard" }, + { value: "rogue-head", label: "Chronicle" }, +]; + +const UPPER_CHOICES: readonly AppearanceChoice>[] = [ + { value: "druid-upper", label: "Grove leathers" }, + { value: "mage-upper", label: "Mystic robes" }, + { value: "ranger-upper", label: "Wayfinder mail" }, + { value: "knight-upper", label: "Vanguard plate" }, + { value: "rogue-upper", label: "Chronicle coat" }, +]; + +const LOWER_CHOICES: readonly AppearanceChoice>[] = [ + { value: "druid-lower", label: "Grove boots" }, + { value: "mage-lower", label: "Mystic boots" }, + { value: "ranger-lower", label: "Wayfinder boots" }, + { value: "knight-lower", label: "Vanguard greaves" }, + { value: "rogue-lower", label: "Chronicle boots" }, +]; + +const HEADWEAR_CHOICES: readonly AppearanceChoice | null>[] = [ + { value: null, label: "None" }, + { value: "mage-hat", label: "Mystic hat" }, + { value: "knight-helmet", label: "Vanguard helm" }, +]; + +const BACK_CHOICES: readonly AppearanceChoice | null>[] = [ + { value: null, label: "None" }, + { value: "druid-backpack", label: "Grove pack" }, + { value: "mage-cape", label: "Mystic cape" }, + { value: "ranger-cape", label: "Wayfinder cape" }, + { value: "ranger-quiver", label: "Wayfinder quiver" }, + { value: "knight-cape", label: "Vanguard cape" }, + { value: "rogue-cape", label: "Chronicle cape" }, +]; + +const MAIN_HAND_CATEGORY_ORDER = [ + "staff", + "wand", + "sword", + "axe", + "hammer", + "dagger", + "crossbow", + "halberd", + "scythe", + "spear", +] as const satisfies readonly CharacterWeaponCategory[]; + +const WEAPON_CATEGORY_LABELS: Record = { + axe: "Axes", + crossbow: "Crossbows", + dagger: "Daggers", + halberd: "Halberds", + hammer: "Hammers", + scythe: "Scythes", + shield: "Shields", + spear: "Spears", + spellbook: "Spellbooks", + staff: "Staves", + sword: "Swords", + wand: "Wands", +}; +const MAIN_HAND_CATEGORY_CHOICES: readonly AppearanceChoice[] = MAIN_HAND_CATEGORY_ORDER.map((value) => ({ + value, + label: WEAPON_CATEGORY_LABELS[value], +})); + +function definitionsInCategory( + definitions: readonly WeaponDefinition[], + category: CharacterWeaponCategory, +) { + return definitions.filter((definition) => definition.category === category); +} + +const MAIN_HAND_DEFINITIONS = MAIN_HAND_CATEGORY_ORDER.flatMap((category) => + definitionsInCategory(weaponDefinitionsForSlot("main"), category)); +const MAIN_HAND_CHOICES: readonly AppearanceChoice[] = MAIN_HAND_DEFINITIONS.map((definition) => ({ + value: heldItemForModel(definition.id as CharacterWeaponModelId), + label: definition.label, +})); +const OFF_HAND_CATEGORY_ORDER = ["shield", "spellbook", "dagger", "sword", "axe"] as const; +const OFF_HAND_DEFINITIONS = OFF_HAND_CATEGORY_ORDER.flatMap((category) => + definitionsInCategory(weaponDefinitionsForSlot("off"), category)); +const OFF_HAND_CHOICES: readonly AppearanceChoice[] = [ + { value: undefined, label: "None" }, + ...OFF_HAND_DEFINITIONS.map((definition) => ({ + value: heldItemForModel(definition.id as CharacterWeaponModelId), + label: definition.label, + })), +]; + +function cycleChoice( + choices: readonly AppearanceChoice[], + current: Value, + direction: -1 | 1, + equals: (left: Value, right: Value) => boolean = Object.is, +) { + const currentIndex = Math.max(0, choices.findIndex((choice) => equals(choice.value, current))); + return choices[(currentIndex + direction + choices.length) % choices.length]; +} + +function heldItemEquals(left: CharacterHeldItemVisual | undefined, right: CharacterHeldItemVisual | undefined) { + return left?.modelId === right?.modelId && left?.grip === right?.grip; +} + +function mainHandCategory(appearance: CharacterAppearanceV1): CharacterWeaponCategory { + return weaponDefinition(appearance.mainHand.modelId).category as CharacterWeaponCategory; +} + +export function appearanceSlotEnabled(appearance: CharacterAppearanceV1, slotId: AppearanceSlotId) { + return slotId !== "off-hand" || !weaponUsesBothHands(appearance.mainHand.modelId); +} + +export function appearanceSlotLabel(appearance: CharacterAppearanceV1, slotId: AppearanceSlotId) { + if (slotId === "head") return HEAD_CHOICES.find((choice) => choice.value === appearance.headPartId)?.label ?? appearance.headPartId; + if (slotId === "upper") return UPPER_CHOICES.find((choice) => choice.value === appearance.upperBodyPartId)?.label ?? appearance.upperBodyPartId; + if (slotId === "lower") return LOWER_CHOICES.find((choice) => choice.value === appearance.lowerBodyPartId)?.label ?? appearance.lowerBodyPartId; + if (slotId === "headwear") return HEADWEAR_CHOICES.find((choice) => choice.value === appearance.headwearPartId)?.label ?? "None"; + if (slotId === "back") return BACK_CHOICES.find((choice) => choice.value === appearance.backPartId)?.label ?? "None"; + if (slotId === "main-hand-family") return WEAPON_CATEGORY_LABELS[mainHandCategory(appearance)]; + if (slotId === "main-hand") return MAIN_HAND_CHOICES.find((choice) => heldItemEquals(choice.value, appearance.mainHand))?.label ?? appearance.mainHand.modelId; + if (!appearanceSlotEnabled(appearance, slotId)) return "Hidden by two-hander"; + return OFF_HAND_CHOICES.find((choice) => heldItemEquals(choice.value, appearance.offHand))?.label ?? "None"; +} + +export function cycleAppearanceSlot( + appearance: CharacterAppearanceV1, + slotId: AppearanceSlotId, + direction: -1 | 1, +): CharacterAppearanceV1 { + if (slotId === "head") return { ...appearance, headPartId: cycleChoice(HEAD_CHOICES, appearance.headPartId, direction).value }; + if (slotId === "upper") return { ...appearance, upperBodyPartId: cycleChoice(UPPER_CHOICES, appearance.upperBodyPartId, direction).value }; + if (slotId === "lower") return { ...appearance, lowerBodyPartId: cycleChoice(LOWER_CHOICES, appearance.lowerBodyPartId, direction).value }; + if (slotId === "headwear") return { ...appearance, headwearPartId: cycleChoice(HEADWEAR_CHOICES, appearance.headwearPartId, direction).value }; + if (slotId === "back") return { ...appearance, backPartId: cycleChoice(BACK_CHOICES, appearance.backPartId, direction).value }; + if (slotId === "main-hand-family") { + const category = cycleChoice(MAIN_HAND_CATEGORY_CHOICES, mainHandCategory(appearance), direction).value; + const definition = MAIN_HAND_DEFINITIONS.find((candidate) => candidate.category === category)!; + return { ...appearance, mainHand: heldItemForModel(definition.id as CharacterWeaponModelId) }; + } + if (slotId === "main-hand") { + const choices = MAIN_HAND_CHOICES.filter((choice) => weaponDefinition(choice.value.modelId).category === mainHandCategory(appearance)); + const mainHand = cycleChoice(choices, appearance.mainHand, direction, heldItemEquals).value; + return { ...appearance, mainHand: { ...mainHand } }; + } + if (!appearanceSlotEnabled(appearance, slotId)) return appearance; + const offHand = cycleChoice(OFF_HAND_CHOICES, appearance.offHand, direction, heldItemEquals).value; + return { ...appearance, offHand: offHand ? { ...offHand } : undefined }; +} + +export function appearancesMatch(left: CharacterAppearanceV1, right: CharacterAppearanceV1) { + return left.headPartId === right.headPartId + && left.upperBodyPartId === right.upperBodyPartId + && left.lowerBodyPartId === right.lowerBodyPartId + && left.headwearPartId === right.headwearPartId + && left.backPartId === right.backPartId + && heldItemEquals(left.mainHand, right.mainHand) + && heldItemEquals(left.offHand, right.offHand); +} diff --git a/src/game/arena.ts b/src/game/arena.ts index 6411d76..55a4f5a 100644 --- a/src/game/arena.ts +++ b/src/game/arena.ts @@ -1,4 +1,11 @@ import type { BossMotionState, WorldPosition } from "./types"; +import { + HOCKEY_ARENA_MAX_X, + HOCKEY_ARENA_MAX_Z, + HOCKEY_ARENA_MIN_X, + HOCKEY_ARENA_MIN_Z, + HOCKEY_MIDLINE_Z, +} from "./hockeyHealing"; export const ARENA_CENTER: WorldPosition = [0, -1]; /** Keeps simulation limits and the room renderer in lockstep. */ @@ -29,3 +36,22 @@ export function constrainBossMotion(motion: BossMotionState): BossMotionState { export function isInsideArena(position: WorldPosition, tolerance = 0.001) { return Math.hypot(position[0] - ARENA_CENTER[0], position[1] - ARENA_CENTER[1]) <= ARENA_RADIUS + tolerance; } + +function clampToHockeyBounds(position: WorldPosition, minZ: number, maxZ: number, padding = 0): WorldPosition { + return [ + Math.max(HOCKEY_ARENA_MIN_X + padding, Math.min(HOCKEY_ARENA_MAX_X - padding, position[0])), + Math.max(minZ + padding, Math.min(maxZ - padding, position[1])), + ]; +} + +export function clampToHockeyArena(position: WorldPosition, padding = 0): WorldPosition { + return clampToHockeyBounds(position, HOCKEY_ARENA_MIN_Z, HOCKEY_ARENA_MAX_Z, padding); +} + +export function clampToHockeyHealerHalf(position: WorldPosition, padding = 0): WorldPosition { + return clampToHockeyBounds(position, HOCKEY_MIDLINE_Z, HOCKEY_ARENA_MAX_Z, padding); +} + +export function clampToHockeyEnemyHalf(position: WorldPosition, padding = 0): WorldPosition { + return clampToHockeyBounds(position, HOCKEY_ARENA_MIN_Z, HOCKEY_MIDLINE_Z, padding); +} diff --git a/src/game/blockbreaker.test.ts b/src/game/blockbreaker.test.ts new file mode 100644 index 0000000..a00af9e --- /dev/null +++ b/src/game/blockbreaker.test.ts @@ -0,0 +1,141 @@ +import { describe, expect, it } from "vitest"; +import { + BLOCKBREAKER_BRICK_COLORS, + BLOCKBREAKER_DANGER_Z, + BLOCKBREAKER_MINIMUM_ROW_INTERVAL, + BLOCKBREAKER_PUCK_SPEED, + BLOCKBREAKER_RESERVE_SECONDS, + advanceBlockbreaker, + blockbreakerClusterScore, + blockbreakerColumnX, + blockbreakerConnectedCluster, + blockbreakerRowInterval, + blockbreakerRowZ, + blockbreakerTimeMultiplier, + createBlockbreakerState, + type BlockbreakerBrick, +} from "./blockbreaker"; +import { HOCKEY_HEALER_GOAL_Z } from "./hockeyHealing"; + +describe("Blockbreaker", () => { + it("creates deterministic five-brick rows from four colors", () => { + const first = createBlockbreakerState(true, 12345); + const second = createBlockbreakerState(true, 12345); + expect(first.bricks).toHaveLength(5); + expect(first.bricks.map((brick) => brick.color)).toEqual(second.bricks.map((brick) => brick.color)); + expect(first.bricks.every((brick) => BLOCKBREAKER_BRICK_COLORS.includes(brick.color))).toBe(true); + expect(Math.hypot(...first.puckVelocity)).toBeCloseTo(BLOCKBREAKER_PUCK_SPEED); + }); + + it("accelerates rows by ten percent per minute with a three-second floor", () => { + expect(blockbreakerRowInterval(0)).toBe(10); + expect(blockbreakerRowInterval(60)).toBe(9); + expect(blockbreakerRowInterval(600)).toBeCloseTo(3.486784401, 8); + expect(blockbreakerRowInterval(2_000)).toBe(BLOCKBREAKER_MINIMUM_ROW_INTERVAL); + }); + + it("uses the per-brick ladder and current uncapped time multiplier", () => { + expect(blockbreakerTimeMultiplier(0)).toBe(1); + expect(blockbreakerTimeMultiplier(30)).toBe(1.1); + expect(blockbreakerTimeMultiplier(60)).toBe(1.2); + expect(blockbreakerTimeMultiplier(600)).toBe(3); + expect(blockbreakerClusterScore(1, 0)).toBe(10); + expect(blockbreakerClusterScore(3, 0)).toBe(60); + expect(blockbreakerClusterScore(3, 30)).toBe(66); + expect(blockbreakerClusterScore(3, 60)).toBe(72); + }); + + it("propagates through orthogonal matches but not diagonals", () => { + const bricks: BlockbreakerBrick[] = [ + { id: "source", row: 1, column: 1, color: "cyan" }, + { id: "right", row: 1, column: 2, color: "cyan" }, + { id: "up", row: 0, column: 2, color: "cyan" }, + { id: "diagonal", row: 0, column: 0, color: "cyan" }, + { id: "other", row: 2, column: 1, color: "amber" }, + ]; + expect(blockbreakerConnectedCluster(bricks, bricks[0]).map((brick) => brick.id).sort()).toEqual(["right", "source", "up"]); + }); + + it("breaks the swept-hit cluster and applies score at collision time", () => { + const state = createBlockbreakerState(true, 7); + state.nextRowAt = 999; + state.bricks = [ + { id: "front", row: 0, column: 2, color: "magenta" }, + { id: "behind", row: 1, column: 2, color: "magenta" }, + ]; + state.puckPosition = [blockbreakerColumnX(2), blockbreakerRowZ(0) + 2.2]; + state.puckVelocity = [0, -6.25]; + const next = advanceBlockbreaker(state, { delta: 0.4, time: 60, playerPosition: [8, 8] }); + expect(next.bricks).toEqual([]); + expect(next.bricksBroken).toBe(2); + expect(next.score).toBe(36); + expect(next.puckVelocity[1]).toBeGreaterThan(0); + }); + + it("removes a breached row and keeps the run live", () => { + let state = createBlockbreakerState(true, 8); + state.puckVelocity = [0, 0]; + while (state.breaches === 0) { + state = advanceBlockbreaker(state, { delta: 0.1, time: state.nextRowAt, playerPosition: [0, 8.5] }); + } + expect(state.status).toBe("live"); + expect(state.breaches).toBe(1); + expect(state.bricks.every((brick) => blockbreakerRowZ(brick.row) < BLOCKBREAKER_DANGER_Z)).toBe(true); + expect(state.lostAt).toBeNull(); + }); + + it("adds complete five-column rows on schedule", () => { + const state = createBlockbreakerState(true, 81); + state.puckVelocity = [0, 0]; + state.puckPosition = [0, 8]; + const next = advanceBlockbreaker(state, { delta: 0.1, time: 10, playerPosition: [0, 8.5] }); + expect(next.bricks).toHaveLength(10); + expect(next.bricks.filter((brick) => brick.row === 0).map((brick) => brick.column).sort()).toEqual([0, 1, 2, 3, 4]); + expect(next.bricks.filter((brick) => brick.row === 1).map((brick) => brick.column).sort()).toEqual([0, 1, 2, 3, 4]); + }); + + it("rebounds from side and far walls", () => { + const side = createBlockbreakerState(true, 82); + side.nextRowAt = 999; + side.bricks = []; + side.puckPosition = [9.45, 0]; + side.puckVelocity = [6.25, 0]; + const sideBounce = advanceBlockbreaker(side, { delta: 0.2, time: 1, playerPosition: [0, 8.5] }); + expect(sideBounce.puckVelocity[0]).toBeLessThan(0); + + const far = createBlockbreakerState(true, 83); + far.nextRowAt = 999; + far.bricks = []; + far.puckPosition = [0, -14]; + far.puckVelocity = [0, -6.25]; + const farBounce = advanceBlockbreaker(far, { delta: 0.2, time: 1, playerPosition: [8, 8.5] }); + expect(farBounce.puckVelocity[1]).toBeGreaterThan(0); + }); + + it("uses healer position and aim for player rebounds", () => { + const state = createBlockbreakerState(true, 84); + state.nextRowAt = 999; + state.bricks = []; + state.aimDirection = [1, -1]; + state.puckPosition = [0, 7.2]; + state.puckVelocity = [0, 6.25]; + const returned = advanceBlockbreaker(state, { delta: 0.2, time: 1, playerPosition: [0, 8] }); + expect(returned.puckVelocity[0]).toBeGreaterThan(0); + expect(returned.puckVelocity[1]).toBeLessThan(0); + }); + + it("safe re-serves one second after a missed puck without ending the run", () => { + const state = createBlockbreakerState(true, 9); + state.nextRowAt = 999; + state.bricks = []; + state.puckPosition = [0, HOCKEY_HEALER_GOAL_Z - 0.1]; + state.puckVelocity = [0, 6.25]; + const missed = advanceBlockbreaker(state, { delta: 0.1, time: 5, playerPosition: [8, 8] }); + expect(missed.status).toBe("reserving"); + expect(missed.lostAt).toBeNull(); + expect(missed.reServeAt).toBe(5 + BLOCKBREAKER_RESERVE_SECONDS); + const served = advanceBlockbreaker(missed, { delta: 0.1, time: missed.reServeAt!, playerPosition: [8, 8] }); + expect(served.status).toBe("live"); + expect(served.puckVelocity[1]).toBeGreaterThan(0); + }); +}); diff --git a/src/game/blockbreaker.ts b/src/game/blockbreaker.ts new file mode 100644 index 0000000..44b450b --- /dev/null +++ b/src/game/blockbreaker.ts @@ -0,0 +1,394 @@ +import { + HOCKEY_ARENA_MAX_X, + HOCKEY_ARENA_MIN_X, + HOCKEY_ARENA_WIDTH, + HOCKEY_HEALER_GOAL_Z, + HOCKEY_MIDLINE_Z, + HOCKEY_NPC_GOAL_Z, + hockeyReturnDirection, +} from "./hockeyHealing"; +import type { WorldPosition } from "./types"; + +export const BLOCKBREAKER_COLUMN_COUNT = 5; +export const BLOCKBREAKER_BRICK_COLORS = ["cyan", "amber", "magenta", "lime"] as const; +export type BlockbreakerBrickColor = typeof BLOCKBREAKER_BRICK_COLORS[number]; + +export interface BlockbreakerBrick { + id: string; + row: number; + column: number; + color: BlockbreakerBrickColor; +} + +export type BlockbreakerStatus = "inactive" | "live" | "reserving" | "lost"; + +export interface BlockbreakerState { + status: BlockbreakerStatus; + seed: number; + randomState: number; + bricks: BlockbreakerBrick[]; + puckPosition: WorldPosition; + puckVelocity: WorldPosition; + aimDirection: WorldPosition; + bricksBroken: number; + score: number; + rowsSpawned: number; + breaches: number; + nextRowAt: number; + serveIndex: number; + reServeAt: number | null; + lastBreakAt: number; + lastBreakCount: number; + lastScoreAward: number; + lostAt: number | null; +} + +export interface BlockbreakerStep { + delta: number; + time: number; + playerPosition: WorldPosition; +} + +export const BLOCKBREAKER_PUCK_RADIUS = 0.42; +export const BLOCKBREAKER_BRICK_WIDTH = HOCKEY_ARENA_WIDTH / BLOCKBREAKER_COLUMN_COUNT - 0.28; +export const BLOCKBREAKER_BRICK_DEPTH = 1.34; +export const BLOCKBREAKER_ROW_SPACING = 1.72; +export const BLOCKBREAKER_SPAWN_Z = HOCKEY_NPC_GOAL_Z + 1.42; +export const BLOCKBREAKER_DANGER_Z = HOCKEY_MIDLINE_Z + (HOCKEY_HEALER_GOAL_Z - HOCKEY_MIDLINE_Z) * 0.5; +export const BLOCKBREAKER_RESERVE_SECONDS = 1; +export const BLOCKBREAKER_BREACH_DAMAGE = 25; +export const BLOCKBREAKER_PUCK_SPEED = 7.25; +export const BLOCKBREAKER_STARTING_ROW_INTERVAL = 10; +export const BLOCKBREAKER_MINIMUM_ROW_INTERVAL = 3; +export const BLOCKBREAKER_MAX_ROWS = Math.ceil((BLOCKBREAKER_DANGER_Z - BLOCKBREAKER_SPAWN_Z) / BLOCKBREAKER_ROW_SPACING) + 1; +export const BLOCKBREAKER_MAX_BRICKS = BLOCKBREAKER_MAX_ROWS * BLOCKBREAKER_COLUMN_COUNT; + +const PLAYER_INTERCEPT_RADIUS = 1.05; +const MAX_SUBSTEPS = 12; +const MAX_SUBSTEP_DISTANCE = 0.3; +const MAX_ROW_SPAWNS_PER_STEP = 8; +const SERVE_LANES = [0, -0.62, 0.68, -0.3, 0.36, -0.8, 0.82] as const; + +function normalizeSeed(seed: number) { + const normalized = Math.floor(Number(seed)) >>> 0; + return normalized || 0x9e3779b9; +} + +export function createBlockbreakerSeed(random: () => number = Math.random) { + const sample = Number(random()); + const normalized = Number.isFinite(sample) ? Math.max(0, Math.min(0.999999999999, sample)) : 0; + return normalizeSeed(Math.floor(normalized * 0x100000000)); +} + +function nextRandom(state: number) { + let next = normalizeSeed(state); + next ^= next << 13; + next ^= next >>> 17; + next ^= next << 5; + return next >>> 0; +} + +export function blockbreakerColumnX(column: number) { + const laneWidth = HOCKEY_ARENA_WIDTH / BLOCKBREAKER_COLUMN_COUNT; + return HOCKEY_ARENA_MIN_X + laneWidth * (Math.max(0, Math.min(BLOCKBREAKER_COLUMN_COUNT - 1, column)) + 0.5); +} + +export function blockbreakerRowZ(row: number) { + return BLOCKBREAKER_SPAWN_Z + Math.max(0, row) * BLOCKBREAKER_ROW_SPACING; +} + +export function blockbreakerRowInterval(elapsedSeconds: number) { + const minute = Math.max(0, Math.floor((Number(elapsedSeconds) || 0) / 60)); + return Math.max(BLOCKBREAKER_MINIMUM_ROW_INTERVAL, BLOCKBREAKER_STARTING_ROW_INTERVAL * 0.9 ** minute); +} + +export function blockbreakerTimeMultiplierTenths(elapsedSeconds: number) { + return 10 + Math.max(0, Math.floor((Number(elapsedSeconds) || 0) / 30)); +} + +export function blockbreakerTimeMultiplier(elapsedSeconds: number) { + return blockbreakerTimeMultiplierTenths(elapsedSeconds) / 10; +} + +export function blockbreakerClusterBase(clusterSize: number) { + const size = Math.max(0, Math.floor(Number(clusterSize) || 0)); + return 10 * size * (size + 1) / 2; +} + +export function blockbreakerClusterScore(clusterSize: number, elapsedSeconds: number) { + return blockbreakerClusterBase(clusterSize) * blockbreakerTimeMultiplierTenths(elapsedSeconds) / 10; +} + +function generateRow(randomState: number, rowSequence: number) { + const bricks: BlockbreakerBrick[] = []; + let nextState = randomState; + for (let column = 0; column < BLOCKBREAKER_COLUMN_COUNT; column += 1) { + nextState = nextRandom(nextState); + bricks.push({ + id: `${rowSequence}:${column}`, + row: 0, + column, + color: BLOCKBREAKER_BRICK_COLORS[nextState % BLOCKBREAKER_BRICK_COLORS.length], + }); + } + return { bricks, randomState: nextState }; +} + +function servePosition(serveIndex: number): WorldPosition { + const lane = SERVE_LANES[serveIndex % SERVE_LANES.length]; + return [lane * (HOCKEY_ARENA_MAX_X - 1.25), BLOCKBREAKER_SPAWN_Z + BLOCKBREAKER_BRICK_DEPTH * 0.5 + 0.72]; +} + +function serveVelocity(position: WorldPosition, serveIndex: number): WorldPosition { + const lane = SERVE_LANES[(serveIndex + 3) % SERVE_LANES.length]; + const target: WorldPosition = [lane * (HOCKEY_ARENA_MAX_X - 1.4), HOCKEY_HEALER_GOAL_Z]; + const dx = target[0] - position[0]; + const dz = target[1] - position[1]; + const length = Math.max(0.0001, Math.hypot(dx, dz)); + return [dx / length * BLOCKBREAKER_PUCK_SPEED, dz / length * BLOCKBREAKER_PUCK_SPEED]; +} + +export function createBlockbreakerState(active = false, requestedSeed = 1): BlockbreakerState { + const seed = normalizeSeed(requestedSeed); + const row = generateRow(seed, 0); + const puckPosition = servePosition(0); + return { + status: active ? "live" : "inactive", + seed, + randomState: row.randomState, + bricks: row.bricks, + puckPosition, + puckVelocity: active ? serveVelocity(puckPosition, 0) : [0, 0], + aimDirection: [0, -1], + bricksBroken: 0, + score: 0, + rowsSpawned: 1, + breaches: 0, + nextRowAt: BLOCKBREAKER_STARTING_ROW_INTERVAL, + serveIndex: 0, + reServeAt: null, + lastBreakAt: Number.NEGATIVE_INFINITY, + lastBreakCount: 0, + lastScoreAward: 0, + lostAt: null, + }; +} + +export function setBlockbreakerAim(state: BlockbreakerState, aim: WorldPosition): BlockbreakerState { + const x = Number.isFinite(aim[0]) ? aim[0] : 0; + const z = Number.isFinite(aim[1]) ? aim[1] : 0; + if (state.aimDirection[0] === x && state.aimDirection[1] === z) return state; + return { ...state, aimDirection: [x, z] }; +} + +export function blockbreakerConnectedCluster(bricks: readonly BlockbreakerBrick[], source: BlockbreakerBrick) { + const byCell = new Map(bricks.map((brick) => [`${brick.row}:${brick.column}`, brick])); + const connected: BlockbreakerBrick[] = []; + const visited = new Set(); + const pending: BlockbreakerBrick[] = [source]; + while (pending.length) { + const brick = pending.pop()!; + if (visited.has(brick.id) || brick.color !== source.color) continue; + visited.add(brick.id); + connected.push(brick); + for (const [row, column] of [ + [brick.row - 1, brick.column], + [brick.row + 1, brick.column], + [brick.row, brick.column - 1], + [brick.row, brick.column + 1], + ]) { + const neighbor = byCell.get(`${row}:${column}`); + if (neighbor && !visited.has(neighbor.id) && neighbor.color === source.color) pending.push(neighbor); + } + } + return connected; +} + +function segmentDistanceSquared( + startX: number, + startZ: number, + endX: number, + endZ: number, + point: WorldPosition, +) { + const dx = endX - startX; + const dz = endZ - startZ; + const lengthSquared = dx * dx + dz * dz; + const projection = lengthSquared < 0.000001 + ? 0 + : Math.max(0, Math.min(1, ((point[0] - startX) * dx + (point[1] - startZ) * dz) / lengthSquared)); + const nearestX = startX + dx * projection; + const nearestZ = startZ + dz * projection; + return (point[0] - nearestX) ** 2 + (point[1] - nearestZ) ** 2; +} + +type SegmentHit = { time: number; normalX: number; normalZ: number }; + +function segmentExpandedBrickHit(start: WorldPosition, end: WorldPosition, brick: BlockbreakerBrick): SegmentHit | null { + const centerX = blockbreakerColumnX(brick.column); + const centerZ = blockbreakerRowZ(brick.row); + const minX = centerX - BLOCKBREAKER_BRICK_WIDTH * 0.5 - BLOCKBREAKER_PUCK_RADIUS; + const maxX = centerX + BLOCKBREAKER_BRICK_WIDTH * 0.5 + BLOCKBREAKER_PUCK_RADIUS; + const minZ = centerZ - BLOCKBREAKER_BRICK_DEPTH * 0.5 - BLOCKBREAKER_PUCK_RADIUS; + const maxZ = centerZ + BLOCKBREAKER_BRICK_DEPTH * 0.5 + BLOCKBREAKER_PUCK_RADIUS; + const dx = end[0] - start[0]; + const dz = end[1] - start[1]; + let entry = 0; + let exit = 1; + let normalX = 0; + let normalZ = 0; + for (const axis of [ + { start: start[0], delta: dx, min: minX, max: maxX, nx: -Math.sign(dx), nz: 0 }, + { start: start[1], delta: dz, min: minZ, max: maxZ, nx: 0, nz: -Math.sign(dz) }, + ]) { + if (Math.abs(axis.delta) < 0.000001) { + if (axis.start < axis.min || axis.start > axis.max) return null; + continue; + } + const near = (axis.min - axis.start) / axis.delta; + const far = (axis.max - axis.start) / axis.delta; + const axisEntry = Math.min(near, far); + const axisExit = Math.max(near, far); + if (axisEntry > entry) { + entry = axisEntry; + normalX = axis.nx; + normalZ = axis.nz; + } + exit = Math.min(exit, axisExit); + if (entry > exit) return null; + } + if (entry < 0 || entry > 1 || exit < 0) return null; + if (normalX === 0 && normalZ === 0) { + if (Math.abs(dx) > Math.abs(dz)) normalX = -Math.sign(dx); + else normalZ = -Math.sign(dz); + } + return { time: entry, normalX, normalZ }; +} + +function addBlockbreakerRow(state: BlockbreakerState, spawnedAt: number) { + const shifted = state.bricks.map((brick) => ({ ...brick, row: brick.row + 1 })); + const survivors = shifted.filter((brick) => blockbreakerRowZ(brick.row) < BLOCKBREAKER_DANGER_Z); + if (survivors.length < shifted.length) state.breaches += 1; + const row = generateRow(state.randomState, state.rowsSpawned); + state.randomState = row.randomState; + state.bricks = [...survivors, ...row.bricks]; + state.rowsSpawned += 1; +} + +function breakCluster(state: BlockbreakerState, hitBrick: BlockbreakerBrick, time: number) { + const cluster = blockbreakerConnectedCluster(state.bricks, hitBrick); + const ids = new Set(cluster.map((brick) => brick.id)); + const award = blockbreakerClusterScore(cluster.length, time); + state.bricks = state.bricks.filter((brick) => !ids.has(brick.id)); + state.bricksBroken += cluster.length; + state.score += award; + state.lastBreakAt = time; + state.lastBreakCount = cluster.length; + state.lastScoreAward = award; +} + +function beginReserve(state: BlockbreakerState, time: number) { + state.status = "reserving"; + state.puckVelocity = [0, 0]; + state.reServeAt = time + BLOCKBREAKER_RESERVE_SECONDS; +} + +function completeReserve(state: BlockbreakerState) { + state.serveIndex += 1; + state.puckPosition = servePosition(state.serveIndex); + state.puckVelocity = serveVelocity(state.puckPosition, state.serveIndex); + state.status = "live"; + state.reServeAt = null; +} + +export function blockbreakerAimPreviewVisible(state: BlockbreakerState, playerPosition: WorldPosition) { + if (state.status !== "live" || state.puckVelocity[1] <= 0) return false; + const distance = Math.hypot(playerPosition[0] - state.puckPosition[0], playerPosition[1] - state.puckPosition[1]); + return distance / Math.max(0.001, Math.hypot(state.puckVelocity[0], state.puckVelocity[1])) <= 1.5; +} + +export function advanceBlockbreaker(source: BlockbreakerState, step: BlockbreakerStep): BlockbreakerState { + if (source.status === "inactive" || source.status === "lost" || step.delta <= 0) return source; + const state: BlockbreakerState = { + ...source, + // Bricks are immutable between row shifts and breaks; both operations replace + // this array, so sharing it avoids cloning every brick on each 10 Hz tick. + bricks: source.bricks, + puckPosition: [...source.puckPosition], + puckVelocity: [...source.puckVelocity], + aimDirection: [...source.aimDirection], + }; + + let spawnedRows = 0; + while (state.status !== "lost" && step.time + 0.0001 >= state.nextRowAt && spawnedRows < MAX_ROW_SPAWNS_PER_STEP) { + const spawnedAt = state.nextRowAt; + addBlockbreakerRow(state, spawnedAt); + state.nextRowAt = spawnedAt + blockbreakerRowInterval(spawnedAt); + spawnedRows += 1; + } + if (state.status === "lost") return state; + if (state.status === "reserving") { + if (state.reServeAt !== null && step.time >= state.reServeAt) completeReserve(state); + else return state; + } + + const speed = Math.hypot(state.puckVelocity[0], state.puckVelocity[1]); + const substeps = Math.max(1, Math.min(MAX_SUBSTEPS, Math.ceil(speed * step.delta / MAX_SUBSTEP_DISTANCE))); + const subDelta = step.delta / substeps; + const interceptRadiusSquared = (PLAYER_INTERCEPT_RADIUS + BLOCKBREAKER_PUCK_RADIUS) ** 2; + + for (let substep = 0; substep < substeps && state.status === "live"; substep += 1) { + const start: WorldPosition = [...state.puckPosition]; + const end: WorldPosition = [ + start[0] + state.puckVelocity[0] * subDelta, + start[1] + state.puckVelocity[1] * subDelta, + ]; + + const minX = HOCKEY_ARENA_MIN_X + BLOCKBREAKER_PUCK_RADIUS; + const maxX = HOCKEY_ARENA_MAX_X - BLOCKBREAKER_PUCK_RADIUS; + if (end[0] < minX || end[0] > maxX) { + end[0] = Math.max(minX, Math.min(maxX, end[0])); + state.puckVelocity[0] *= -1; + } + + let closest: { brick: BlockbreakerBrick; hit: SegmentHit } | null = null; + for (const brick of state.bricks) { + const hit = segmentExpandedBrickHit(start, end, brick); + if (hit && (!closest || hit.time < closest.hit.time)) closest = { brick, hit }; + } + if (closest) { + state.puckPosition = [ + start[0] + (end[0] - start[0]) * closest.hit.time + closest.hit.normalX * 0.012, + start[1] + (end[1] - start[1]) * closest.hit.time + closest.hit.normalZ * 0.012, + ]; + if (closest.hit.normalX) state.puckVelocity[0] *= -1; + if (closest.hit.normalZ) state.puckVelocity[1] *= -1; + breakCluster(state, closest.brick, step.time); + continue; + } + + if (state.puckVelocity[1] > 0 + && segmentDistanceSquared(start[0], start[1], end[0], end[1], step.playerPosition) <= interceptRadiusSquared) { + const direction = hockeyReturnDirection(state.aimDirection); + state.puckPosition = [end[0], Math.min(end[1], step.playerPosition[1])]; + state.puckVelocity = [direction[0] * BLOCKBREAKER_PUCK_SPEED, direction[1] * BLOCKBREAKER_PUCK_SPEED]; + continue; + } + + if (state.puckVelocity[1] < 0 && end[1] <= HOCKEY_NPC_GOAL_Z + BLOCKBREAKER_PUCK_RADIUS) { + state.puckPosition = [end[0], HOCKEY_NPC_GOAL_Z + BLOCKBREAKER_PUCK_RADIUS]; + state.puckVelocity[1] = Math.abs(state.puckVelocity[1]); + continue; + } + + if (state.puckVelocity[1] > 0 && end[1] >= HOCKEY_HEALER_GOAL_Z) { + state.puckPosition = [end[0], HOCKEY_HEALER_GOAL_Z]; + beginReserve(state, step.time); + continue; + } + + state.puckPosition = end; + } + + return state; +} diff --git a/src/game/blockbreakerBiomes.test.ts b/src/game/blockbreakerBiomes.test.ts new file mode 100644 index 0000000..d88f014 --- /dev/null +++ b/src/game/blockbreakerBiomes.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { BLOCKBREAKER_BRICK_COLORS } from "./blockbreaker"; +import { + BLOCKBREAKER_BIOMES, + BLOCKBREAKER_BIOME_IDS, + blockbreakerBiomeForSeed, +} from "./blockbreakerBiomes"; + +describe("Blockbreaker arena biomes", () => { + it("defines five distinct arena and fixture styles", () => { + expect(BLOCKBREAKER_BIOMES.map((biome) => biome.id)).toEqual(BLOCKBREAKER_BIOME_IDS); + expect(new Set(BLOCKBREAKER_BIOMES.map((biome) => biome.name)).size).toBe(5); + expect(new Set(BLOCKBREAKER_BIOMES.map((biome) => biome.fixture)).size).toBe(5); + expect(new Set(BLOCKBREAKER_BIOMES.map((biome) => biome.background)).size).toBe(5); + expect(new Set(BLOCKBREAKER_BIOMES.map((biome) => biome.keyLight)).size).toBe(5); + }); + + it("keeps every gameplay brick color visually distinct in every biome", () => { + for (const biome of BLOCKBREAKER_BIOMES) { + const colors = BLOCKBREAKER_BRICK_COLORS.map((color) => biome.bricks[color]); + expect(colors.every((color) => /^#[0-9a-f]{6}$/i.test(color))).toBe(true); + expect(new Set(colors).size).toBe(BLOCKBREAKER_BRICK_COLORS.length); + } + }); + + it("selects a deterministic biome from the match seed", () => { + expect(blockbreakerBiomeForSeed(0x12345678)).toBe(blockbreakerBiomeForSeed(0x12345678)); + const sampledIds = new Set(Array.from({ length: 256 }, (_, seed) => blockbreakerBiomeForSeed(seed).id)); + expect(sampledIds).toEqual(new Set(BLOCKBREAKER_BIOME_IDS)); + }); +}); diff --git a/src/game/blockbreakerBiomes.ts b/src/game/blockbreakerBiomes.ts new file mode 100644 index 0000000..6a05e43 --- /dev/null +++ b/src/game/blockbreakerBiomes.ts @@ -0,0 +1,253 @@ +import type { BlockbreakerBrickColor } from "./blockbreaker"; + +export const BLOCKBREAKER_BIOME_IDS = [ + "prism-circuit", + "ember-foundry", + "frost-vault", + "void-grid", + "verdant-reactor", +] as const; + +export type BlockbreakerBiomeId = typeof BLOCKBREAKER_BIOME_IDS[number]; +export type BlockbreakerBiomeFixture = "crystal" | "forge" | "spire" | "monolith" | "reactor"; + +export interface BlockbreakerArenaBiome { + id: BlockbreakerBiomeId; + name: string; + fixture: BlockbreakerBiomeFixture; + background: string; + fog: string; + ambient: string; + sky: string; + ground: string; + keyLight: string; + fillLightA: string; + fillLightB: string; + foundation: string; + floor: string; + playfield: string; + wall: string; + wallEmissive: string; + boundary: string; + railA: string; + railB: string; + midline: string; + fixtureColor: string; + fixtureEmissive: string; + ambientIntensity: number; + hemisphereIntensity: number; + keyLightIntensity: number; + fillLightIntensityA: number; + fillLightIntensityB: number; + wallEmissiveIntensity: number; + floorRoughness: number; + floorMetalness: number; + bricks: Readonly>; +} + +export const BLOCKBREAKER_BIOMES: readonly BlockbreakerArenaBiome[] = [ + { + id: "prism-circuit", + name: "Prism Circuit", + fixture: "crystal", + background: "#163640", + fog: "#214b55", + ambient: "#dffcff", + sky: "#efffff", + ground: "#24535b", + keyLight: "#f4ffff", + fillLightA: "#8cefff", + fillLightB: "#fff0bd", + foundation: "#173c44", + floor: "#2d6972", + playfield: "#367985", + wall: "#39717c", + wallEmissive: "#143a43", + boundary: "#b9f8ff", + railA: "#9ef6ff", + railB: "#fff2c2", + midline: "#e8feff", + fixtureColor: "#49a6b7", + fixtureEmissive: "#70efff", + ambientIntensity: 0.72, + hemisphereIntensity: 2.3, + keyLightIntensity: 3.15, + fillLightIntensityA: 5.5, + fillLightIntensityB: 4.8, + wallEmissiveIntensity: 0.32, + floorRoughness: 0.58, + floorMetalness: 0.08, + bricks: { + cyan: "#42ddff", + amber: "#ffc247", + magenta: "#ff55ad", + lime: "#8bf065", + }, + }, + { + id: "ember-foundry", + name: "Ember Foundry", + fixture: "forge", + background: "#2a0d0a", + fog: "#4b1b11", + ambient: "#ffd7a8", + sky: "#ffb45c", + ground: "#240a0b", + keyLight: "#ffe0b2", + fillLightA: "#ff5b32", + fillLightB: "#ffc54d", + foundation: "#250b0a", + floor: "#5a2416", + playfield: "#7a321e", + wall: "#52241d", + wallEmissive: "#6a170c", + boundary: "#ffb15f", + railA: "#ff6a38", + railB: "#ffd15c", + midline: "#ffe2a6", + fixtureColor: "#5d2117", + fixtureEmissive: "#ff5a26", + ambientIntensity: 0.58, + hemisphereIntensity: 1.85, + keyLightIntensity: 2.75, + fillLightIntensityA: 6.4, + fillLightIntensityB: 5.4, + wallEmissiveIntensity: 0.48, + floorRoughness: 0.76, + floorMetalness: 0.18, + bricks: { + cyan: "#54dcff", + amber: "#ffbd3d", + magenta: "#ff5b78", + lime: "#d7ff5c", + }, + }, + { + id: "frost-vault", + name: "Frost Vault", + fixture: "spire", + background: "#071a2f", + fog: "#143b56", + ambient: "#dff8ff", + sky: "#c6f4ff", + ground: "#10233d", + keyLight: "#ffffff", + fillLightA: "#55dfff", + fillLightB: "#9b8cff", + foundation: "#09192a", + floor: "#1c4c67", + playfield: "#255f7c", + wall: "#24455f", + wallEmissive: "#0d4662", + boundary: "#d4fbff", + railA: "#6be9ff", + railB: "#b5a7ff", + midline: "#ffffff", + fixtureColor: "#5b9db9", + fixtureEmissive: "#9defff", + ambientIntensity: 0.66, + hemisphereIntensity: 2.05, + keyLightIntensity: 3.35, + fillLightIntensityA: 5.3, + fillLightIntensityB: 4.2, + wallEmissiveIntensity: 0.38, + floorRoughness: 0.42, + floorMetalness: 0.16, + bricks: { + cyan: "#38e4ff", + amber: "#ffd36f", + magenta: "#b47cff", + lime: "#6ef2c0", + }, + }, + { + id: "void-grid", + name: "Void Grid", + fixture: "monolith", + background: "#05020f", + fog: "#120526", + ambient: "#cebaff", + sky: "#8d65ff", + ground: "#0a0313", + keyLight: "#e8dcff", + fillLightA: "#784cff", + fillLightB: "#ff38c7", + foundation: "#07020e", + floor: "#160a2b", + playfield: "#211044", + wall: "#1d1030", + wallEmissive: "#280966", + boundary: "#b68cff", + railA: "#7d5cff", + railB: "#ff4ccc", + midline: "#d9c4ff", + fixtureColor: "#16082e", + fixtureEmissive: "#8d5cff", + ambientIntensity: 0.44, + hemisphereIntensity: 1.48, + keyLightIntensity: 2.45, + fillLightIntensityA: 6.2, + fillLightIntensityB: 5.7, + wallEmissiveIntensity: 0.64, + floorRoughness: 0.34, + floorMetalness: 0.28, + bricks: { + cyan: "#41d8ff", + amber: "#ffac45", + magenta: "#ff4bd0", + lime: "#a1ff4f", + }, + }, + { + id: "verdant-reactor", + name: "Verdant Reactor", + fixture: "reactor", + background: "#0d211a", + fog: "#193f31", + ambient: "#e1ffe3", + sky: "#b8ffc9", + ground: "#102217", + keyLight: "#f2ffe8", + fillLightA: "#63e6a3", + fillLightB: "#d8ff67", + foundation: "#0d1f18", + floor: "#25543d", + playfield: "#2d684a", + wall: "#294f3f", + wallEmissive: "#0d4b2d", + boundary: "#b9ffd2", + railA: "#67f4a7", + railB: "#e7ff73", + midline: "#f1ffd0", + fixtureColor: "#245642", + fixtureEmissive: "#76f7a9", + ambientIntensity: 0.62, + hemisphereIntensity: 2.02, + keyLightIntensity: 2.9, + fillLightIntensityA: 5.1, + fillLightIntensityB: 4.6, + wallEmissiveIntensity: 0.42, + floorRoughness: 0.68, + floorMetalness: 0.06, + bricks: { + cyan: "#4be3ff", + amber: "#ffca52", + magenta: "#ff6fb5", + lime: "#99f75d", + }, + }, +]; + +function mixSeed(seed: number) { + let mixed = Math.floor(Number(seed)) >>> 0; + mixed ^= mixed >>> 16; + mixed = Math.imul(mixed, 0x7feb352d); + mixed ^= mixed >>> 15; + mixed = Math.imul(mixed, 0x846ca68b); + mixed ^= mixed >>> 16; + return mixed >>> 0; +} + +export function blockbreakerBiomeForSeed(seed: number) { + return BLOCKBREAKER_BIOMES[mixSeed(seed) % BLOCKBREAKER_BIOMES.length]; +} diff --git a/src/game/blockbreakerStore.test.ts b/src/game/blockbreakerStore.test.ts new file mode 100644 index 0000000..9c22488 --- /dev/null +++ b/src/game/blockbreakerStore.test.ts @@ -0,0 +1,142 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { BOSS_DEATH_DESPAWN_SECONDS } from "./bossDeath"; +import { BLOCKBREAKER_BREACH_DAMAGE, BLOCKBREAKER_MAX_ROWS, blockbreakerRowZ } from "./blockbreaker"; +import { createClassInventory } from "./healers"; +import { HOCKEY_HEALER_GOAL_Z, HOCKEY_MIDLINE_Z } from "./hockeyHealing"; +import { useGameStore } from "./store"; + +describe("Blockbreaker encounter integration", () => { + beforeEach(() => { + useGameStore.getState().configureHealer( + "priest", + "Aelia", + createClassInventory("priest"), + ["bulldrome", "broodfang-spider"], + "blockbreaker", + ); + }); + + it("starts an endless two-boss rink with one five-brick row", () => { + const state = useGameStore.getState(); + expect(state.endlessMode).toBe(true); + expect(state.additionalBosses).toHaveLength(1); + expect(state.blockbreaker.bricks).toHaveLength(5); + expect(state.bossMotion.position[1]).toBeLessThan(HOCKEY_MIDLINE_Z); + expect(state.partyPositions.aelia[1]).toBeGreaterThan(HOCKEY_MIDLINE_Z); + }); + + it("deals 25 partywide damage once and continues when a row breaches", () => { + useGameStore.getState().startEncounter(); + useGameStore.setState((state) => ({ + boss: { ...state.boss, nextMeleeAt: 999 }, + additionalBosses: state.additionalBosses.map((entry) => ({ + ...entry, + boss: { ...entry.boss, nextMeleeAt: 999 }, + })), + blockbreaker: { + ...state.blockbreaker, + nextRowAt: state.time, + puckVelocity: [0, 0], + bricks: [{ id: "danger", row: BLOCKBREAKER_MAX_ROWS - 1, column: 2, color: "cyan" }], + }, + })); + expect(blockbreakerRowZ(BLOCKBREAKER_MAX_ROWS)).toBeGreaterThan(HOCKEY_MIDLINE_Z); + + useGameStore.getState().tick(0.1); + const breached = useGameStore.getState(); + expect(breached.blockbreaker.status).toBe("live"); + expect(breached.blockbreaker.breaches).toBe(1); + expect(breached.blockbreaker.bricks.some((brick) => brick.id === "danger")).toBe(false); + expect(breached.party.every((member) => member.hp === member.maxHp - BLOCKBREAKER_BREACH_DAMAGE)).toBe(true); + expect(breached.phase).toBe("combat"); + expect(breached.combatLog[0]?.message).toContain("25 damage to every party member"); + + const healthAfterBreach = breached.party.map((member) => member.hp); + useGameStore.getState().tick(0.1); + expect(useGameStore.getState().party.map((member) => member.hp)).toEqual(healthAfterBreach); + }); + + it("misses safely re-serve without ending the run", () => { + useGameStore.getState().startEncounter(); + useGameStore.setState((state) => ({ + blockbreaker: { + ...state.blockbreaker, + nextRowAt: 999, + bricks: [], + puckPosition: [0, HOCKEY_HEALER_GOAL_Z - 0.1], + puckVelocity: [0, 6.25], + }, + })); + useGameStore.getState().tick(0.1); + expect(useGameStore.getState().blockbreaker.status).toBe("reserving"); + expect(useGameStore.getState().phase).toBe("combat"); + useGameStore.getState().tick(1); + expect(useGameStore.getState().blockbreaker.status).toBe("live"); + expect(useGameStore.getState().phase).toBe("combat"); + }); + + it("fails when all four allies die even while the healer is alive", () => { + useGameStore.getState().startEncounter(); + useGameStore.setState((state) => ({ + boss: { ...state.boss, nextMeleeAt: 999 }, + additionalBosses: state.additionalBosses.map((entry) => ({ + ...entry, + boss: { ...entry.boss, nextMeleeAt: 999 }, + })), + blockbreaker: { ...state.blockbreaker, nextRowAt: 999, puckVelocity: [0, 0] }, + party: state.party.map((member) => ({ + ...member, + hp: member.id === "aelia" || member.id === "vale" ? member.maxHp : 0, + })), + })); + + useGameStore.getState().tick(0.1); + expect(useGameStore.getState().phase).toBe("combat"); + + useGameStore.setState((state) => ({ + party: state.party.map((member) => member.id === "vale" ? { ...member, hp: 0 } : member), + })); + useGameStore.getState().tick(0.1); + + const defeated = useGameStore.getState(); + expect(defeated.party.find((member) => member.id === "aelia")?.hp).toBeGreaterThan(0); + expect(defeated.blockbreaker.status).toBe("lost"); + expect(defeated.blockbreaker.puckVelocity).toEqual([0, 0]); + expect(defeated.phase).toBe("defeat"); + expect(defeated.combatLog[0]?.message).toContain("All four allies fell"); + const logLength = defeated.combatLog.length; + useGameStore.getState().tick(0.1); + expect(useGameStore.getState().combatLog).toHaveLength(logLength); + }); + + it("replaces fallen bosses and resets run state on retry", () => { + useGameStore.getState().startEncounter(); + const defeatedInstanceId = useGameStore.getState().bossInstanceId; + useGameStore.setState((state) => ({ + boss: { ...state.boss, hp: 1, nextMeleeAt: 999 }, + blockbreaker: { ...state.blockbreaker, nextRowAt: 999, puckVelocity: [0, 0], bricksBroken: 14, score: 720 }, + })); + let elapsed = 0; + while (useGameStore.getState().boss.hp > 0 && elapsed < 3) { + useGameStore.getState().tick(0.1); + elapsed += 0.1; + } + expect(useGameStore.getState().boss.hp).toBe(0); + while (elapsed <= BOSS_DEATH_DESPAWN_SECONDS + 3.2) { + useGameStore.getState().tick(0.1); + elapsed += 0.1; + } + const replaced = useGameStore.getState(); + expect(replaced.phase).toBe("combat"); + expect(replaced.bossInstanceId).not.toBe(defeatedInstanceId); + expect(replaced.bossInstanceId.startsWith("blockbreaker-")).toBe(true); + expect(replaced.endlessBossKills).toBe(1); + + replaced.restart(); + const restarted = useGameStore.getState(); + expect(restarted.phase).toBe("briefing"); + expect(restarted.blockbreaker.bricksBroken).toBe(0); + expect(restarted.blockbreaker.score).toBe(0); + expect(restarted.blockbreaker.bricks).toHaveLength(5); + }); +}); diff --git a/src/game/bossAnimation.test.ts b/src/game/bossAnimation.test.ts new file mode 100644 index 0000000..8f59c76 --- /dev/null +++ b/src/game/bossAnimation.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; +import { + BOSS_HIT_ANIMATION_SECONDS, + BOSS_MELEE_ANIMATION_SECONDS, + bossAnimationClipName, + bossAnimationTrigger, + isBossAnimationOneShot, + selectBossAnimationState, + shouldStartBossAnimation, + writeBossProceduralPose, + type BossAnimationClips, + type BossProceduralPose, +} from "./bossAnimation"; + +const CLIPS: BossAnimationClips = { + idle: "Idle", + move: "Move", + attack: "Attack", + special: "Special", + melee: "Melee", + death: "Death", +}; + +function pose(): BossProceduralPose { + return { x: 0, y: 0, z: 0, pitch: 0, yaw: 0, roll: 0, scaleX: 1, scaleY: 1, scaleZ: 1 }; +} + +describe("boss animation playback", () => { + it("prioritizes death, mechanics, melee, and hit reactions in gameplay order", () => { + const base = { defeated: false, activeMechanic: false, mechanicCue: "idle" as const }; + expect(selectBossAnimationState({ ...base, defeated: true, meleeElapsed: 0, hitElapsed: 0 })).toBe("death"); + expect(selectBossAnimationState({ ...base, activeMechanic: true, mechanicCue: "special", meleeElapsed: 0, hitElapsed: 0 })).toBe("special"); + expect(selectBossAnimationState({ ...base, meleeElapsed: 0, hitElapsed: 0 })).toBe("melee"); + expect(selectBossAnimationState({ ...base, meleeElapsed: BOSS_MELEE_ANIMATION_SECONDS, hitElapsed: 0 })).toBe("hit"); + expect(selectBossAnimationState({ ...base, meleeElapsed: BOSS_MELEE_ANIMATION_SECONDS, hitElapsed: BOSS_HIT_ANIMATION_SECONDS })).toBe("idle"); + }); + + it("restarts one-shot melee and hit clips only for new triggers", () => { + expect(shouldStartBossAnimation("melee", 2, "melee", 2)).toBe(false); + expect(shouldStartBossAnimation("melee", 2, "melee", 4.5)).toBe(true); + expect(shouldStartBossAnimation("hit", 1, "hit", 2)).toBe(true); + expect(shouldStartBossAnimation("move", 0, "move", 3)).toBe(false); + expect(isBossAnimationOneShot("special")).toBe(true); + }); + + it("uses idle plus procedural recoil when a source model lacks a hit clip", () => { + expect(bossAnimationClipName(CLIPS, "hit")).toBe("Idle"); + const next = pose(); + writeBossProceduralPose(next, "ricochet", "holding", "hit", BOSS_HIT_ANIMATION_SECONDS / 2, 1); + expect(next.z).toBeLessThan(0); + expect(next.pitch).toBeGreaterThan(0); + }); + + it("adds signature windup motion for breath and shockwave bosses", () => { + const dragon = pose(); + writeBossProceduralPose(dragon, "sky-sweeper", "breath_sweeping", "attack", 0.5, 1); + expect(dragon.y).toBeGreaterThan(0); + expect(dragon.pitch).toBeLessThan(0); + + const golem = pose(); + writeBossProceduralPose(golem, "golem", "golem_shockwave", "special", 0.5, 1); + expect(golem.scaleY).toBeLessThan(1); + expect(golem.scaleX).toBeGreaterThan(1); + }); + + it("uses semantic triggers for repeated one-shot clips", () => { + expect(bossAnimationTrigger("attack", 4, 2, 1)).toBe(4); + expect(bossAnimationTrigger("melee", 4, 2, 1)).toBe(2); + expect(bossAnimationTrigger("hit", 4, 2, 1)).toBe(1); + }); +}); diff --git a/src/game/bossAnimation.ts b/src/game/bossAnimation.ts new file mode 100644 index 0000000..5ad8b9e --- /dev/null +++ b/src/game/bossAnimation.ts @@ -0,0 +1,174 @@ +import type { BossArchetype } from "./bossCatalog"; +import type { BossAnimationCue, BossMotionMode } from "./types"; + +export type BossAnimationState = BossAnimationCue | "melee" | "hit" | "death"; + +export interface BossAnimationClips { + idle: string; + move: string; + attack: string; + special: string; + melee: string; + hit?: string; + death: string; +} + +export interface BossProceduralPose { + x: number; + y: number; + z: number; + pitch: number; + yaw: number; + roll: number; + scaleX: number; + scaleY: number; + scaleZ: number; +} + +export const BOSS_MELEE_ANIMATION_SECONDS = 0.62; +export const BOSS_HIT_ANIMATION_SECONDS = 0.42; +export const BOSS_HIT_REACTION_COOLDOWN_SECONDS = 0.55; + +export function bossAnimationClipName(clips: BossAnimationClips, state: BossAnimationState) { + return state === "hit" ? clips.hit ?? clips.idle : clips[state]; +} + +export function isBossAnimationOneShot(state: BossAnimationState) { + return state === "attack" + || state === "special" + || state === "melee" + || state === "hit" + || state === "death"; +} + +export function shouldStartBossAnimation( + activeState: BossAnimationState | undefined, + activeTrigger: number, + nextState: BossAnimationState, + nextTrigger: number, +) { + if (activeState !== nextState) return true; + if (nextState === "death" || !isBossAnimationOneShot(nextState)) return false; + return activeTrigger !== nextTrigger; +} + +export function selectBossAnimationState({ + defeated, + activeMechanic, + mechanicCue, + meleeElapsed, + hitElapsed, +}: { + defeated: boolean; + activeMechanic: boolean; + mechanicCue: BossAnimationCue; + meleeElapsed: number; + hitElapsed: number; +}): BossAnimationState { + if (defeated) return "death"; + if (activeMechanic) return mechanicCue; + if (meleeElapsed < BOSS_MELEE_ANIMATION_SECONDS) return "melee"; + if (hitElapsed < BOSS_HIT_ANIMATION_SECONDS) return "hit"; + return mechanicCue; +} + +export function bossAnimationTrigger( + state: BossAnimationState, + phaseStartedAt: number, + lastMeleeAt: number, + hitTrigger: number, +) { + if (state === "attack" || state === "special") return phaseStartedAt; + if (state === "melee") return lastMeleeAt; + if (state === "hit") return hitTrigger; + return 0; +} + +function pulse(elapsed: number, duration: number) { + if (elapsed < 0 || elapsed >= duration) return 0; + return Math.sin((elapsed / duration) * Math.PI); +} + +function normalizedPhase(elapsed: number, duration: number) { + return Math.max(0, Math.min(1, elapsed / Math.max(0.001, duration))); +} + +export function resetBossProceduralPose(pose: BossProceduralPose) { + pose.x = 0; + pose.y = 0; + pose.z = 0; + pose.pitch = 0; + pose.yaw = 0; + pose.roll = 0; + pose.scaleX = 1; + pose.scaleY = 1; + pose.scaleZ = 1; +} + +/** + * Adds readable root motion where source GLBs only provide generic combat clips. + * Caller owns and reuses `pose`; this function allocates nothing in render loops. + */ +export function writeBossProceduralPose( + pose: BossProceduralPose, + archetype: BossArchetype, + mode: BossMotionMode, + animationState: BossAnimationState, + stateElapsed: number, + phaseDuration: number, +) { + resetBossProceduralPose(pose); + + if (animationState === "melee") { + const amount = pulse(stateElapsed, BOSS_MELEE_ANIMATION_SECONDS); + pose.z = amount * 0.34; + pose.pitch = -amount * 0.09; + pose.scaleZ = 1 + amount * 0.035; + return; + } + + if (animationState === "hit") { + const amount = pulse(stateElapsed, BOSS_HIT_ANIMATION_SECONDS); + pose.z = -amount * 0.2; + pose.pitch = amount * 0.08; + pose.roll = Math.sin((stateElapsed / BOSS_HIT_ANIMATION_SECONDS) * Math.PI * 2) * 0.045; + return; + } + + const progress = normalizedPhase(stateElapsed, phaseDuration); + const windup = Math.sin(progress * Math.PI); + + if (archetype === "sky-sweeper" && (mode === "breath_telegraph" || mode === "breath_sweeping")) { + pose.y = windup * 0.24; + pose.pitch = -windup * 0.18; + pose.roll = mode === "breath_sweeping" ? Math.sin(progress * Math.PI * 2) * 0.07 : 0; + pose.scaleX = 1 + windup * 0.045; + pose.scaleZ = 1 + windup * 0.045; + return; + } + + if (archetype === "golem" && mode === "golem_shockwave") { + pose.y = -windup * 0.1; + pose.scaleX = 1 + windup * 0.08; + pose.scaleY = 1 - windup * 0.13; + pose.scaleZ = 1 + windup * 0.08; + return; + } + + if (archetype === "golem" && (mode === "golem_crownfall" || mode === "skyfall")) { + pose.y = windup * 0.3; + pose.pitch = -windup * 0.08; + pose.scaleX = 1 + windup * 0.04; + pose.scaleZ = 1 + windup * 0.04; + return; + } + + if ((archetype === "ghost" || archetype === "web-caster") + && (mode === "tethering" || mode === "venom_cast" || mode === "golem_crownfall")) { + pose.y = windup * 0.18; + pose.yaw = Math.sin(progress * Math.PI * 2) * 0.12; + pose.scaleX = 1 + windup * 0.045; + pose.scaleY = 1 + windup * 0.045; + pose.scaleZ = 1 + windup * 0.045; + } +} diff --git a/src/game/bossCatalog.test.ts b/src/game/bossCatalog.test.ts index 6a5f124..b6bc2e3 100644 --- a/src/game/bossCatalog.test.ts +++ b/src/game/bossCatalog.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { AVAILABLE_BOSS_IDS, BOSS_DEFINITIONS, BOSS_GROUPS } from "./bossCatalog"; import { createBossMotionState, createBossState } from "./bossMechanics"; import { BOSS_MECHANIC_POOL, BOSS_MECHANIC_REGISTRY, bossMechanicName } from "./bosses/mechanicPool"; -import { ALTERNATE_BOSS_CONFIG } from "./bossVisuals"; +import { ALTERNATE_BOSS_CONFIG, BULL_BOSS_ANIMATION_CONFIG } from "./bossVisuals"; describe("boss catalog", () => { it("derives the available roster from every catalog definition", () => { @@ -66,6 +66,23 @@ describe("boss catalog", () => { expect(ALTERNATE_BOSS_CONFIG["bristlequake-boar"].death).toBe("Dying"); }); + it("maps every boss to melee and available hit-reaction clips", () => { + const profiles = [BULL_BOSS_ANIMATION_CONFIG, ...Object.values(ALTERNATE_BOSS_CONFIG)]; + expect(profiles).toHaveLength(AVAILABLE_BOSS_IDS.length); + expect(profiles.every((profile) => profile.melee.length > 0)).toBe(true); + expect(profiles.filter((profile) => profile.hit).length).toBe(24); + }); + + it("uses signature-ready remaps for pounce, weapon, and tusk bosses", () => { + expect(BULL_BOSS_ANIMATION_CONFIG.attack).toBe("Attack_Headbutt"); + expect(ALTERNATE_BOSS_CONFIG["stormwool-alpaca"].special).toBe("Gallop_Jump"); + expect(ALTERNATE_BOSS_CONFIG["thorncrown-stag"].special).toBe("Gallop_Jump"); + expect(ALTERNATE_BOSS_CONFIG["riftclaw-demon"].attack).toBe("Weapon"); + expect(ALTERNATE_BOSS_CONFIG["warcaller-orc"].attack).toBe("Weapon"); + expect(ALTERNATE_BOSS_CONFIG["rimeclaw-yeti"].attack).toBe("Weapon"); + expect(ALTERNATE_BOSS_CONFIG["bristlequake-boar"].special).toBe("Attack2 (tusks)"); + }); + it("assigns Gravehorn to the underfilled burrow group with its optimized animation set", () => { expect(BOSS_GROUPS.find((group) => group.id === "burrow-eruption")?.bossIds).toContain("gravehorn-triceratops"); expect(ALTERNATE_BOSS_CONFIG["gravehorn-triceratops"]).toMatchObject({ diff --git a/src/game/bossDeath.ts b/src/game/bossDeath.ts index 0e52efe..eb75cdb 100644 --- a/src/game/bossDeath.ts +++ b/src/game/bossDeath.ts @@ -1,8 +1,24 @@ +import type { BossId } from "./types"; + export const BOSS_DEATH_HOLD_SECONDS = 2.5; export const BOSS_DEATH_FADE_SECONDS = 0.75; export const BOSS_DEATH_DESPAWN_SECONDS = BOSS_DEATH_HOLD_SECONDS + BOSS_DEATH_FADE_SECONDS; -export function bossDeathOpacity(elapsedSeconds: number) { - if (elapsedSeconds <= BOSS_DEATH_HOLD_SECONDS) return 1; - return Math.max(0, 1 - (elapsedSeconds - BOSS_DEATH_HOLD_SECONDS) / BOSS_DEATH_FADE_SECONDS); +const BOSS_DEATH_HOLD_OVERRIDES: Partial> = { + // Armature|Fall lasts 5.73 seconds. Hold through final authored pose before fade. + "gravehorn-triceratops": 5.85, +}; + +export function bossDeathHoldSeconds(bossId?: BossId) { + return bossId ? BOSS_DEATH_HOLD_OVERRIDES[bossId] ?? BOSS_DEATH_HOLD_SECONDS : BOSS_DEATH_HOLD_SECONDS; +} + +export function bossDeathDespawnSeconds(bossId?: BossId) { + return bossDeathHoldSeconds(bossId) + BOSS_DEATH_FADE_SECONDS; +} + +export function bossDeathOpacity(elapsedSeconds: number, bossId?: BossId) { + const holdSeconds = bossDeathHoldSeconds(bossId); + if (elapsedSeconds <= holdSeconds) return 1; + return Math.max(0, 1 - (elapsedSeconds - holdSeconds) / BOSS_DEATH_FADE_SECONDS); } diff --git a/src/game/bossHome.test.ts b/src/game/bossHome.test.ts index 7f94e8c..eb91c91 100644 --- a/src/game/bossHome.test.ts +++ b/src/game/bossHome.test.ts @@ -24,6 +24,7 @@ describe("boss home positioning", () => { ); const result = advanceBossMechanics({ + arenaLayout: "standard", boss: createBossState(bossId), motion, party: freshParty(), diff --git a/src/game/bossVisuals.ts b/src/game/bossVisuals.ts index c5bc5a8..10501dc 100644 --- a/src/game/bossVisuals.ts +++ b/src/game/bossVisuals.ts @@ -1,22 +1,27 @@ import type { BossId } from "./types"; +import type { BossAnimationClips } from "./bossAnimation"; export type AlternateBossKind = Exclude; -export interface AlternateBossConfig { +export interface AlternateBossConfig extends BossAnimationClips { url: string; optimizedUrl?: string; scale: number; - idle: string; - move: string; - attack: string; - special: string; - death: string; light: string; rotationOffset: number; floating?: boolean; } export const BULL_URL = new URL("../assets/game/models/claudecraft/creatures/bull.glb", import.meta.url).href; +export const BULL_BOSS_ANIMATION_CONFIG: BossAnimationClips = { + idle: "Idle", + move: "Gallop", + attack: "Attack_Headbutt", + special: "Gallop_Jump", + melee: "Attack_Kick", + hit: "Idle_HitReact_Left", + death: "Death", +}; const SANDGLASS_URL = new URL("../assets/game/models/original/bosses/sandglass-scorpion/sandglass-scorpion.glb", import.meta.url).href; const CRYSTAL_BAT_MATRIARCH_URL = new URL("../assets/game/models/original/bosses/crystal-bat-matriarch/crystal-bat-matriarch.glb", import.meta.url).href; @@ -61,34 +66,34 @@ const CLAUDE_BOSS_URLS: Record = { - "sandglass-scorpion": { url: SANDGLASS_URL, scale: 0.7, idle: "Idle", move: "Burrow", attack: "Eruption", special: "Hourglass", death: "Death", light: "#e9b94f", rotationOffset: 0 }, - "cragclaw-crab": { url: CRAGCLAW_URL, scale: 1.2, idle: "Idle", move: "Walk", attack: "Bite_Front", special: "Bite_InPlace", death: "Death", light: "#49d5df", rotationOffset: 0 }, - "mournveil-ghost": { url: MOURNVEIL_URL, scale: 1.1, idle: "Flying_Idle", move: "Fast_Flying", attack: "Punch", special: "Headbutt", death: "Death", light: "#9d72ff", rotationOffset: 0, floating: true }, - "crownshard-golem": { url: CROWNSHARD_URL, scale: 1.15, idle: "Flying_Idle", move: "Fast_Flying", attack: "Punch", special: "Headbutt", death: "Death", light: "#e0bd45", rotationOffset: 0, floating: true }, - "crystal-bat-matriarch": { url: CRYSTAL_BAT_MATRIARCH_URL, scale: 0.828, idle: "Idle", move: "Swoop", attack: "SonicPulse", special: "MirrorShatter", death: "Death", light: "#8eeaff", rotationOffset: 0, floating: true }, - "stormwool-alpaca": { url: CLAUDE_BOSS_URLS["stormwool-alpaca"], scale: 0.72, idle: "Idle", move: "Gallop", attack: "Attack_Headbutt", special: "Attack_Kick", death: "Death", light: "#8fc7ff", rotationOffset: 0 }, + "sandglass-scorpion": { url: SANDGLASS_URL, scale: 0.7, idle: "Idle", move: "Burrow", attack: "Eruption", special: "Hourglass", melee: "ClawAttack", hit: "Stagger", death: "Death", light: "#e9b94f", rotationOffset: 0 }, + "cragclaw-crab": { url: CRAGCLAW_URL, scale: 1.2, idle: "Idle", move: "Walk", attack: "Bite_Front", special: "Bite_InPlace", melee: "Bite_Front", hit: "HitRecieve", death: "Death", light: "#49d5df", rotationOffset: 0 }, + "mournveil-ghost": { url: MOURNVEIL_URL, scale: 1.1, idle: "Flying_Idle", move: "Fast_Flying", attack: "Punch", special: "Headbutt", melee: "Punch", hit: "HitReact", death: "Death", light: "#9d72ff", rotationOffset: 0, floating: true }, + "crownshard-golem": { url: CROWNSHARD_URL, scale: 1.15, idle: "Flying_Idle", move: "Fast_Flying", attack: "Punch", special: "Headbutt", melee: "Punch", hit: "HitReact", death: "Death", light: "#e0bd45", rotationOffset: 0, floating: true }, + "crystal-bat-matriarch": { url: CRYSTAL_BAT_MATRIARCH_URL, scale: 0.828, idle: "Idle", move: "Swoop", attack: "SonicPulse", special: "MirrorShatter", melee: "SonicPulse", hit: "Stagger", death: "Death", light: "#8eeaff", rotationOffset: 0, floating: true }, + "stormwool-alpaca": { url: CLAUDE_BOSS_URLS["stormwool-alpaca"], scale: 0.72, idle: "Idle", move: "Gallop", attack: "Attack_Headbutt", special: "Gallop_Jump", melee: "Attack_Kick", hit: "Idle_HitReact_Left", death: "Death", light: "#8fc7ff", rotationOffset: 0 }, // IDs remain stable so existing saves and trophies keep working after visual replacement. - "cluckhorn-colossus": { url: BRASSBEAK_BASILISK_URL, scale: 0.75, idle: "Idle", move: "Scuttle", attack: "BeakRend", special: "FurnaceBurst", death: "Death", light: "#5cebd7", rotationOffset: 0 }, - "ashwing-demon": { url: CLAUDE_BOSS_URLS["ashwing-demon"], scale: 1.2, idle: "Flying_Idle", move: "Fast_Flying", attack: "Punch", special: "Headbutt", death: "Death", light: "#df665d", rotationOffset: 0, floating: true }, - "riftclaw-demon": { url: CLAUDE_BOSS_URLS["riftclaw-demon"], scale: 1.35, idle: "Idle", move: "Run", attack: "Punch", special: "Weapon", death: "Death", light: "#d45cff", rotationOffset: 0 }, - "tempestscale-dragon": { url: CLAUDE_BOSS_URLS["tempestscale-dragon"], scale: 1.2, idle: "Flying_Idle", move: "Fast_Flying", attack: "Punch", special: "Headbutt", death: "Death", light: "#5fc8e8", rotationOffset: 0, floating: true }, - emberfox: { url: CLAUDE_BOSS_URLS.emberfox, scale: 1, idle: "Idle", move: "Gallop", attack: "Attack", special: "Gallop_Jump", death: "Death", light: "#ff7b45", rotationOffset: 0 }, - "mirelord-frog": { url: BOGBELL_MYCONID_URL, scale: 0.78, idle: "Idle", move: "BurrowRush", attack: "RootPummel", special: "SporeEruption", death: "Death", light: "#8df06b", rotationOffset: 0 }, - "stonebreaker-giant": { url: CLAUDE_BOSS_URLS["stonebreaker-giant"], scale: 1, idle: "Idle", move: "Run", attack: "Attack", special: "Jump", death: "Death", light: "#c89563", rotationOffset: 0 }, - "glub-sovereign": { url: CLAUDE_BOSS_URLS["glub-sovereign"], scale: 1.2, idle: "Flying_Idle", move: "Fast_Flying", attack: "Punch", special: "Headbutt", death: "Death", light: "#6ce0b8", rotationOffset: 0, floating: true }, - "scrapking-goblin": { url: CLAUDE_BOSS_URLS["scrapking-goblin"], scale: 1.5, idle: "Idle", move: "Run", attack: "Attack", special: "Jump", death: "Death", light: "#d7a34b", rotationOffset: 0 }, - "warcaller-orc": { url: CLAUDE_BOSS_URLS["warcaller-orc"], scale: 1.35, idle: "Idle", move: "Run", attack: "Punch", special: "Weapon", death: "Death", light: "#e4533f", rotationOffset: 0 }, - "tuskmaw-orc": { url: CLAUDE_BOSS_URLS["tuskmaw-orc"], scale: 1.45, idle: "Idle", move: "Walk", attack: "Bite_Front", special: "Jump", death: "Death", light: "#9eb25d", rotationOffset: 0 }, - "broodfang-spider": { url: CLAUDE_BOSS_URLS["broodfang-spider"], scale: 1.35, idle: "Spider_Idle", move: "Spider_Walk", attack: "Spider_Attack", special: "Spider_Jump", death: "Spider_Death", light: "#b56cff", rotationOffset: 0 }, - "silkfang-spider": { url: CLAUDE_BOSS_URLS["silkfang-spider"], scale: 1.35, idle: "Spider_Idle", move: "Spider_Walk", attack: "Spider_Attack", special: "Spider_Jump", death: "Spider_Death", light: "#9d68d8", rotationOffset: 0 }, - "thorncrown-stag": { url: CLAUDE_BOSS_URLS["thorncrown-stag"], scale: 0.85, idle: "Idle", move: "Gallop", attack: "Attack_Headbutt", special: "Attack_Kick", death: "Death", light: "#7fc46b", rotationOffset: 0 }, - "sky-totem": { url: CLAUDE_BOSS_URLS["sky-totem"], scale: 1.2, idle: "Flying_Idle", move: "Fast_Flying", attack: "Punch", special: "Headbutt", death: "Death", light: "#69d4d1", rotationOffset: 0, floating: true }, - "razorcrest-raptor": { url: CLAUDE_BOSS_URLS["razorcrest-raptor"], scale: 1.1, idle: "Velociraptor_Idle", move: "Velociraptor_Run", attack: "Velociraptor_Attack", special: "Velociraptor_Jump", death: "Velociraptor_Death", light: "#d9c45a", rotationOffset: 0 }, - "bristlequake-boar": { url: CLAUDE_BOSS_URLS["bristlequake-boar"], scale: 0.475, idle: "Idle_AnimalArmature", move: "Gallop_AnimalArmature", attack: "Attack_Headbutt_AnimalArmature", special: "Attack_Kick_AnimalArmature", death: "Dying", light: "#d47b45", rotationOffset: 0 }, - "moonfang-wolf": { url: CLAUDE_BOSS_URLS["moonfang-wolf"], scale: 1.05, idle: "Idle", move: "Gallop", attack: "Attack", special: "Gallop_Jump", death: "Death", light: "#9db9e5", rotationOffset: 0 }, - "frostmaw-yeti": { url: CLAUDE_BOSS_URLS["frostmaw-yeti"], scale: 1.35, idle: "Idle", move: "Walk", attack: "Bite_Front", special: "Jump", death: "Death", light: "#8ed8ef", rotationOffset: 0 }, - "rimeclaw-yeti": { url: CLAUDE_BOSS_URLS["rimeclaw-yeti"], scale: 1.35, idle: "Idle", move: "Run", attack: "Punch", special: "Weapon", death: "Death", light: "#75bfe8", rotationOffset: 0 }, - "gravehorn-triceratops": { url: GRAVEHORN_URL, optimizedUrl: GRAVEHORN_OPTIMIZED_URL, scale: 0.82, idle: "Gravehorn|Idle", move: "Armature|Walk", attack: "Armature|Roar", special: "Armature|RiseUp", death: "Armature|Fall", light: "#ffd9a0", rotationOffset: 0 }, + "cluckhorn-colossus": { url: BRASSBEAK_BASILISK_URL, scale: 0.75, idle: "Idle", move: "Scuttle", attack: "BeakRend", special: "FurnaceBurst", melee: "BeakRend", hit: "Stagger", death: "Death", light: "#5cebd7", rotationOffset: 0 }, + "ashwing-demon": { url: CLAUDE_BOSS_URLS["ashwing-demon"], scale: 1.2, idle: "Flying_Idle", move: "Fast_Flying", attack: "Punch", special: "Headbutt", melee: "Punch", hit: "HitReact", death: "Death", light: "#df665d", rotationOffset: 0, floating: true }, + "riftclaw-demon": { url: CLAUDE_BOSS_URLS["riftclaw-demon"], scale: 1.35, idle: "Idle", move: "Run", attack: "Weapon", special: "Punch", melee: "Weapon", hit: "HitReact", death: "Death", light: "#d45cff", rotationOffset: 0 }, + "tempestscale-dragon": { url: CLAUDE_BOSS_URLS["tempestscale-dragon"], scale: 1.2, idle: "Flying_Idle", move: "Fast_Flying", attack: "Punch", special: "Headbutt", melee: "Punch", hit: "HitReact", death: "Death", light: "#5fc8e8", rotationOffset: 0, floating: true }, + emberfox: { url: CLAUDE_BOSS_URLS.emberfox, scale: 1, idle: "Idle", move: "Gallop", attack: "Attack", special: "Gallop_Jump", melee: "Attack", hit: "Idle_HitReact_Left", death: "Death", light: "#ff7b45", rotationOffset: 0 }, + "mirelord-frog": { url: BOGBELL_MYCONID_URL, scale: 0.78, idle: "Idle", move: "BurrowRush", attack: "RootPummel", special: "SporeEruption", melee: "RootPummel", hit: "Stagger", death: "Death", light: "#8df06b", rotationOffset: 0 }, + "stonebreaker-giant": { url: CLAUDE_BOSS_URLS["stonebreaker-giant"], scale: 1, idle: "Idle", move: "Run", attack: "Attack", special: "Jump", melee: "Attack", hit: "HitRecieve", death: "Death", light: "#c89563", rotationOffset: 0 }, + "glub-sovereign": { url: CLAUDE_BOSS_URLS["glub-sovereign"], scale: 1.2, idle: "Flying_Idle", move: "Fast_Flying", attack: "Punch", special: "Headbutt", melee: "Punch", hit: "HitReact", death: "Death", light: "#6ce0b8", rotationOffset: 0, floating: true }, + "scrapking-goblin": { url: CLAUDE_BOSS_URLS["scrapking-goblin"], scale: 1.5, idle: "Idle", move: "Run", attack: "Attack", special: "Jump", melee: "Attack", hit: "HitRecieve", death: "Death", light: "#d7a34b", rotationOffset: 0 }, + "warcaller-orc": { url: CLAUDE_BOSS_URLS["warcaller-orc"], scale: 1.35, idle: "Idle", move: "Run", attack: "Weapon", special: "Punch", melee: "Weapon", hit: "HitReact", death: "Death", light: "#e4533f", rotationOffset: 0 }, + "tuskmaw-orc": { url: CLAUDE_BOSS_URLS["tuskmaw-orc"], scale: 1.45, idle: "Idle", move: "Walk", attack: "Bite_Front", special: "Jump", melee: "Bite_Front", hit: "HitRecieve", death: "Death", light: "#9eb25d", rotationOffset: 0 }, + "broodfang-spider": { url: CLAUDE_BOSS_URLS["broodfang-spider"], scale: 1.35, idle: "Spider_Idle", move: "Spider_Walk", attack: "Spider_Attack", special: "Spider_Jump", melee: "Spider_Attack", death: "Spider_Death", light: "#b56cff", rotationOffset: 0 }, + "silkfang-spider": { url: CLAUDE_BOSS_URLS["silkfang-spider"], scale: 1.35, idle: "Spider_Idle", move: "Spider_Walk", attack: "Spider_Attack", special: "Spider_Jump", melee: "Spider_Attack", death: "Spider_Death", light: "#9d68d8", rotationOffset: 0 }, + "thorncrown-stag": { url: CLAUDE_BOSS_URLS["thorncrown-stag"], scale: 0.85, idle: "Idle", move: "Gallop", attack: "Attack_Headbutt", special: "Gallop_Jump", melee: "Attack_Kick", hit: "Idle_HitReact_Left", death: "Death", light: "#7fc46b", rotationOffset: 0 }, + "sky-totem": { url: CLAUDE_BOSS_URLS["sky-totem"], scale: 1.2, idle: "Flying_Idle", move: "Fast_Flying", attack: "Punch", special: "Headbutt", melee: "Punch", hit: "HitReact", death: "Death", light: "#69d4d1", rotationOffset: 0, floating: true }, + "razorcrest-raptor": { url: CLAUDE_BOSS_URLS["razorcrest-raptor"], scale: 1.1, idle: "Velociraptor_Idle", move: "Velociraptor_Run", attack: "Velociraptor_Attack", special: "Velociraptor_Jump", melee: "Velociraptor_Attack", death: "Velociraptor_Death", light: "#d9c45a", rotationOffset: 0 }, + "bristlequake-boar": { url: CLAUDE_BOSS_URLS["bristlequake-boar"], scale: 0.475, idle: "Idle_AnimalArmature", move: "Gallop_AnimalArmature", attack: "Attack_Headbutt_AnimalArmature", special: "Attack2 (tusks)", melee: "Attack_Headbutt_AnimalArmature", hit: "Hurt", death: "Dying", light: "#d47b45", rotationOffset: 0 }, + "moonfang-wolf": { url: CLAUDE_BOSS_URLS["moonfang-wolf"], scale: 1.05, idle: "Idle", move: "Gallop", attack: "Attack", special: "Gallop_Jump", melee: "Attack", hit: "Idle_HitReact_Left", death: "Death", light: "#9db9e5", rotationOffset: 0 }, + "frostmaw-yeti": { url: CLAUDE_BOSS_URLS["frostmaw-yeti"], scale: 1.35, idle: "Idle", move: "Walk", attack: "Bite_Front", special: "Jump", melee: "Bite_Front", hit: "HitRecieve", death: "Death", light: "#8ed8ef", rotationOffset: 0 }, + "rimeclaw-yeti": { url: CLAUDE_BOSS_URLS["rimeclaw-yeti"], scale: 1.35, idle: "Idle", move: "Run", attack: "Weapon", special: "Punch", melee: "Weapon", hit: "HitReact", death: "Death", light: "#75bfe8", rotationOffset: 0 }, + "gravehorn-triceratops": { url: GRAVEHORN_URL, optimizedUrl: GRAVEHORN_OPTIMIZED_URL, scale: 0.82, idle: "Gravehorn|Idle", move: "Armature|Walk", attack: "Armature|Roar", special: "Armature|RiseUp", melee: "Armature|Roar", death: "Armature|Fall", light: "#ffd9a0", rotationOffset: 0 }, }; export function bossVisualUrl(bossId: BossId, optimized = false): string { diff --git a/src/game/bosses/mechanicPool.test.ts b/src/game/bosses/mechanicPool.test.ts index 290a6e2..a94b520 100644 --- a/src/game/bosses/mechanicPool.test.ts +++ b/src/game/bosses/mechanicPool.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from "vitest"; import { freshParty } from "../data"; -import { createBaseMotion } from "./shared"; -import { advanceMechanicLoadout, BOSS_MECHANIC_POOL, bossAnimationCue, SOUL_SIPHON } from "./mechanicPool"; +import { HOCKEY_MIDLINE_Z } from "../hockeyHealing"; +import { createBaseMotion, selectMeleeTargetIndex } from "./shared"; +import { advanceMechanicLoadout, BOSS_MECHANIC_POOL, bossAnimationCue, BULL_POUNCE, MEMORY_SEQUENCE, SOUL_SIPHON } from "./mechanicPool"; import type { BossMechanicContext } from "./types"; import type { BossState, PoolTelegraph, WorldPosition } from "../types"; @@ -23,8 +24,22 @@ function state(): BossState { }; } +describe("basic melee target fallback", () => { + it("prefers Brann, then another living tank, companion, and finally healer", () => { + let party = freshParty(); + expect(party[selectMeleeTargetIndex(party)].id).toBe("brann"); + party = party.map((member) => member.id === "brann" ? { ...member, hp: 0 } : member.id === "nia" ? { ...member, role: "Tank" as const } : member); + expect(party[selectMeleeTargetIndex(party)].id).toBe("nia"); + party = party.map((member) => member.id === "nia" ? { ...member, hp: 0 } : member); + expect(party[selectMeleeTargetIndex(party)].id).toBe("orin"); + party = party.map((member) => member.id === "aelia" ? member : { ...member, hp: 0 }); + expect(party[selectMeleeTargetIndex(party)].id).toBe("aelia"); + }); +}); + function context(time: number, positions = POSITIONS): BossMechanicContext { return { + arenaLayout: "standard", boss: state(), motion: createBaseMotion("bulldrome"), party: freshParty(), @@ -121,6 +136,16 @@ function advanceSoulSiphon( } describe("shared boss mechanic pool", () => { + it("records each basic melee impact for renderer animation cues", () => { + const source = context(2); + source.boss.nextMeleeAt = 2; + const tankHp = source.party.find((member) => member.id === "brann")!.hp; + const advanced = advanceMechanicLoadout(source, ["basic-melee", "bull-charge"]); + + expect(advanced.party.find((member) => member.id === "brann")!.hp).toBeLessThan(tankHp); + expect(advanced.motion.lastMeleeAt).toBe(2); + }); + it.each(BOSS_MECHANIC_POOL.filter(({ id }) => id !== "basic-melee"))("starts $name directly from its canonical ID", ({ id }) => { const source = context(0); source.motion.nextMechanicAt = 0; @@ -141,6 +166,44 @@ describe("shared boss mechanic pool", () => { expect(bossAnimationCue(started.motion)).toBe("attack"); }); + it("uses NPC-only Crushing Pounce targets in Hockey Healing", () => { + const source = context(0); + source.arenaLayout = "hockey-healing"; + source.motion.mechanicCount = 4; + source.motion.nextMechanicAt = 0; + const started = advanceMechanicLoadout(source, ["crushing-pounce", "bull-charge"]); + + expect(started.motion.pounceTargetId).not.toBe("aelia"); + expect(["brann", "nia", "orin", "vale"]).toContain(started.motion.pounceTargetId); + }); + + it("limits Hockey Healing Crushing Pounce to 150 shared damage", () => { + const source = context(0.1, { + aelia: [0, 4.5], + brann: [0, 0], + nia: [0, 0], + orin: [0, 0], + vale: [0, 0], + }); + source.arenaLayout = "hockey-healing"; + source.motion = { + ...source.motion, + activeMechanicId: "crushing-pounce", + mode: "pouncing", + position: [0, 0], + chargeEnd: [0, 0], + pounceCenter: [0, 0], + pounceTargetId: "brann", + phaseEndsAt: 0, + }; + const startingHp = source.party.reduce((total, member) => total + member.hp, 0); + const resolved = advanceMechanicLoadout(source, ["crushing-pounce", "bull-charge"]); + const remainingHp = resolved.party.reduce((total, member) => total + member.hp, 0); + + expect(startingHp - remainingHp).toBe(BULL_POUNCE.hockeySharedDamage); + expect(resolved.party.find((member) => member.id === "aelia")!.hp).toBe(source.party[0].hp); + }); + it("moves a web caster sideways during Binding Web and animates its return home", () => { const source = context(0); source.motion.nextMechanicAt = 0; @@ -241,6 +304,23 @@ describe("shared boss mechanic pool", () => { expect(current.party).toEqual(freshParty()); }); + it.each(["hockey-healing", "hockey-healing-pvp"] as const)( + "keeps every Memory Sequence tile inside the healer half in %s", + (arenaLayout) => { + const source = context(0); + source.arenaLayout = arenaLayout; + source.motion.nextMechanicAt = 0; + const started = advanceMechanicLoadout(source, ["memory-sequence", "bull-charge"]); + const tiles = started.motion.poolTelegraphs[0].tiles ?? []; + const halfSize = MEMORY_SEQUENCE.tileSize * 0.5; + + expect(tiles).toHaveLength(4); + for (const tile of tiles) { + expect(tile.center[1] - halfSize).toBeGreaterThan(HOCKEY_MIDLINE_Z); + } + }, + ); + it("ignores NPC positions but deals 15 raidwide damage when healer selects a wrong tile", () => { const waiting = advanceMemory(1, memoryTelegraph(), freshParty(), [0, 4.5]); expect(waiting.events).toEqual([]); @@ -267,6 +347,17 @@ describe("shared boss mechanic pool", () => { expect(cleansed.events[0].message).toContain("cleansing ward"); }); + it("keeps Soul Siphon fully inside the healer half in Hockey Healing", () => { + const source = context(0); + source.arenaLayout = "hockey-healing"; + source.motion.nextMechanicAt = 0; + const started = advanceMechanicLoadout(source, ["soul-siphon", "bull-charge"]); + const siphon = started.motion.poolTelegraphs[0].soulSiphon!; + + expect(siphon.wardPosition[1] - siphon.wardRadius).toBeGreaterThan(HOCKEY_MIDLINE_Z); + expect(siphon.ghostPosition[1]).toBeGreaterThan(HOCKEY_MIDLINE_Z); + }); + it("deals half-strength ramping Soul Siphon damage and caps the ramp", () => { const result = advanceSoulSiphon(SOUL_SIPHON.tickInterval * 6 + 0.01, soulSiphonTelegraph()); const aelia = result.party.find((member) => member.id === "aelia")!; diff --git a/src/game/bosses/mechanicPool.ts b/src/game/bosses/mechanicPool.ts index 3e4ef3d..c70fc62 100644 --- a/src/game/bosses/mechanicPool.ts +++ b/src/game/bosses/mechanicPool.ts @@ -1,4 +1,4 @@ -import { ARENA_CENTER, ARENA_RADIUS, clampToArena } from "../arena"; +import { ARENA_CENTER, ARENA_RADIUS, clampToArena, clampToHockeyArena, clampToHockeyHealerHalf } from "../arena"; import { angleTo, distance, moveToward, pointToSegmentDistance } from "../geometry"; import type { BossAnimationCue, BossMechanicId, BossMotionState, CircleHazard, Debuff, MemberId, MemorySymbolId, PartyMember, PoolTelegraph, SlashLane, WorldPosition } from "../types"; import { applyMelee, chooseLivingTarget, cloneMotion, createCircleHazard, memberName, resolveCircleHazards, returnBossToArenaCenter } from "./shared"; @@ -90,6 +90,7 @@ export const BULL_POUNCE = { stackDuration: 5, stackRadius: 3.2, sharedDamage: 200, + hockeySharedDamage: 150, leapDuration: 0.55, cooldown: 4, } as const; @@ -148,6 +149,13 @@ const MEMORY_TILE_LAYOUT: readonly { symbol: MemorySymbolId; center: WorldPositi { symbol: "square", center: [2.35, -3.15] }, ]; +const HOCKEY_MEMORY_TILE_LAYOUT: readonly { symbol: MemorySymbolId; center: WorldPosition }[] = [ + { symbol: "triangle", center: [-2.35, 1.35] }, + { symbol: "cross", center: [2.35, 1.35] }, + { symbol: "circle", center: [-2.35, 5.85] }, + { symbol: "square", center: [2.35, 5.85] }, +]; + const MEMORY_SEQUENCES: readonly (readonly MemorySymbolId[])[] = [ ["triangle", "cross", "circle", "square"], ["circle", "triangle", "square", "cross"], @@ -221,8 +229,17 @@ function beamTelegraph(id: string, center: WorldPosition, target: WorldPosition, }; } -function memorySequenceTelegraph(id: string, bossPosition: WorldPosition, count: number, inputStartsAt: number): PoolTelegraph { +function memorySequenceTelegraph( + id: string, + bossPosition: WorldPosition, + count: number, + inputStartsAt: number, + arenaLayout: BossMechanicContext["arenaLayout"], +): PoolTelegraph { const sequence = MEMORY_SEQUENCES[count % MEMORY_SEQUENCES.length]; + const tileLayout = arenaLayout === "standard" || arenaLayout === "aether-assault" + ? MEMORY_TILE_LAYOUT + : HOCKEY_MEMORY_TILE_LAYOUT; return { id, kind: "memory", @@ -235,28 +252,47 @@ function memorySequenceTelegraph(id: string, bossPosition: WorldPosition, count: damage: MEMORY_SEQUENCE.raidwideDamage, targetId: "aelia", sequence: [...sequence], - tiles: MEMORY_TILE_LAYOUT.map((tile) => ({ symbol: tile.symbol, center: [tile.center[0], tile.center[1]] })), + tiles: tileLayout.map((tile) => ({ symbol: tile.symbol, center: [tile.center[0], tile.center[1]] })), inputIndex: 0, resolved: false, hitIds: [], }; } -function oppositeWardPosition(healerPosition: WorldPosition, count: number): WorldPosition { +function clampSoulSiphonPosition( + position: WorldPosition, + padding: number, + arenaLayout: BossMechanicContext["arenaLayout"], +): WorldPosition { + if (arenaLayout === "aether-assault") return clampToHockeyArena(position, padding); + return arenaLayout !== "standard" ? clampToHockeyHealerHalf(position, padding) : clampToArena(position, padding); +} + +function oppositeWardPosition( + healerPosition: WorldPosition, + count: number, + arenaLayout: BossMechanicContext["arenaLayout"], +): WorldPosition { const offsetX = healerPosition[0] - ARENA_CENTER[0]; const offsetZ = healerPosition[1] - ARENA_CENTER[1]; const offsetLength = Math.hypot(offsetX, offsetZ); const fallbackAngle = count % 2 === 0 ? 0 : Math.PI / 2; const directionX = offsetLength > 0.05 ? -offsetX / offsetLength : Math.sin(fallbackAngle); const directionZ = offsetLength > 0.05 ? -offsetZ / offsetLength : Math.cos(fallbackAngle); - return clampToArena([ + return clampSoulSiphonPosition([ ARENA_CENTER[0] + directionX * SOUL_SIPHON.wardDistance, ARENA_CENTER[1] + directionZ * SOUL_SIPHON.wardDistance, - ], 0.25); + ], arenaLayout !== "standard" ? SOUL_SIPHON.wardRadius + 0.25 : 0.25, arenaLayout); } -function soulSiphonTelegraph(id: string, healerPosition: WorldPosition, count: number, time: number): PoolTelegraph { - const wardPosition = oppositeWardPosition(healerPosition, count); +function soulSiphonTelegraph( + id: string, + healerPosition: WorldPosition, + count: number, + time: number, + arenaLayout: BossMechanicContext["arenaLayout"], +): PoolTelegraph { + const wardPosition = oppositeWardPosition(healerPosition, count, arenaLayout); return { id, kind: "soul-siphon", @@ -269,7 +305,11 @@ function soulSiphonTelegraph(id: string, healerPosition: WorldPosition, count: n targetId: "aelia", soulSiphon: { targetId: "aelia", - ghostPosition: clampToArena([healerPosition[0] - 0.85, healerPosition[1] + 0.85], 0.2), + ghostPosition: clampSoulSiphonPosition( + [healerPosition[0] - 0.85, healerPosition[1] + 0.85], + 0.2, + arenaLayout, + ), wardPosition, wardRadius: SOUL_SIPHON.wardRadius, nextDamageAt: time + SOUL_SIPHON.tickInterval, @@ -286,6 +326,7 @@ function beginPoolMechanic( positions: BossMechanicContext["partyPositions"], time: number, requestedId: BossMechanicId, + arenaLayout: BossMechanicContext["arenaLayout"], ) { const count = motion.poolMechanicCount + 1; const entry = MECHANIC_COPY_BY_ID[requestedId]; @@ -334,10 +375,16 @@ function beginPoolMechanic( telegraphs = [soak]; } else if (entry.id === "memory-sequence") { targetId = "aelia"; - telegraphs = [memorySequenceTelegraph(`pool-memory-${count}`, motion.position, count, time)]; + telegraphs = [memorySequenceTelegraph(`pool-memory-${count}`, motion.position, count, time, arenaLayout)]; } else if (entry.id === "soul-siphon") { targetId = "aelia"; - telegraphs = [soulSiphonTelegraph(`pool-soul-siphon-${count}`, positions.aelia, count, time)]; + telegraphs = [soulSiphonTelegraph( + `pool-soul-siphon-${count}`, + positions.aelia, + count, + time, + arenaLayout, + )]; } else { targetId = liveTarget(party, TARGET_ORDER[count % TARGET_ORDER.length]); telegraphs = [beamTelegraph(`pool-beam-${count}`, motion.position, positions[targetId], activatesAt)]; @@ -447,7 +494,7 @@ function resolveSoulSiphon( at: context.time, message: "Aelia reaches the cleansing ward. Soul Siphon collapses.", tone: "neutral", - pulseKind: "purify", + pulseKind: "cleanse", targetId: "aelia", }); return party; @@ -855,6 +902,8 @@ function laneAttackDefinition(config: LaneAttackConfig): BossMechanicDefinition } const BULL_TARGETS: readonly MemberId[] = ["nia", "orin", "vale", "aelia", "brann"]; +const POUNCE_TARGETS: readonly MemberId[] = ["aelia", "nia", "orin", "vale", "brann"]; +const HOCKEY_POUNCE_TARGETS: readonly MemberId[] = ["nia", "orin", "vale", "brann"]; const bullCharge = laneChargeDefinition({ id: "bull-charge", warning: BULL_CHARGE.warning, speed: BULL_CHARGE.speed, distance: BULL_CHARGE.distance, width: BULL_CHARGE.hitRadius * 2, damage: BULL_CHARGE.damage, knockdown: BULL_CHARGE.knockdown, cooldown: BULL_CHARGE.cooldown, targetOrder: BULL_TARGETS }); const destructionRush = laneChargeDefinition({ id: "destruction-rush", warning: 1.45, speed: 11.5, distance: 14, width: 2.5, damage: 27, knockdown: 0.55, cooldown: 3.8, targetOrder: BULL_TARGETS }); @@ -867,7 +916,10 @@ const crushingPounce: BossMechanicDefinition = { instruction: mechanicCopy("crushing-pounce").instruction, cooldown: 4, start(runtime) { - const targetId = chooseLivingTarget(runtime.party, ["aelia", "nia", "orin", "vale", "brann"], runtime.motion.mechanicCount); + const targetOrder = runtime.context.arenaLayout !== "standard" + ? HOCKEY_POUNCE_TARGETS + : POUNCE_TARGETS; + const targetId = chooseLivingTarget(runtime.party, targetOrder, runtime.motion.mechanicCount); runtime.motion.mode = "stacking"; runtime.motion.pounceTargetId = targetId; runtime.motion.pounceCenter = [...runtime.context.partyPositions[targetId]]; @@ -891,7 +943,10 @@ const crushingPounce: BossMechanicDefinition = { motion.position = moveToward(motion.position, motion.chargeEnd, 16 * context.delta); if (context.time < motion.phaseEndsAt && distance(motion.position, motion.chargeEnd) >= 0.08) return; const stackedIds = runtime.party.filter((member) => member.hp > 0 && distance(context.partyPositions[member.id], motion.pounceCenter) <= BULL_POUNCE.stackRadius).map((member) => member.id); - const damage = BULL_POUNCE.sharedDamage / Math.max(1, stackedIds.length); + const sharedDamage = context.arenaLayout !== "standard" + ? BULL_POUNCE.hockeySharedDamage + : BULL_POUNCE.sharedDamage; + const damage = sharedDamage / Math.max(1, stackedIds.length); runtime.party = runtime.party.map((member) => stackedIds.includes(member.id) ? context.damageMember(member, damage, context.partyPositions[member.id], context.time) : member); @@ -1142,7 +1197,14 @@ function telegraphDefinition(id: BossMechanicId): BossMechanicDefinition { const definition: BossMechanicDefinition = { id, name: copy.name, instruction: copy.instruction, cooldown: 5, start(runtime) { - const started = beginPoolMechanic(runtime.motion, runtime.party, runtime.context.partyPositions, runtime.context.time, id); + const started = beginPoolMechanic( + runtime.motion, + runtime.party, + runtime.context.partyPositions, + runtime.context.time, + id, + runtime.context.arenaLayout, + ); runtime.motion = started.motion; runtime.motion.mode = "golem_crownfall"; runtime.events.push(started.event); @@ -1180,7 +1242,7 @@ const basicMelee: BossMechanicDefinition = { advance(runtime) { applyMelee(runtime.boss, runtime.motion, runtime.party, runtime.context.partyPositions, runtime.context.time, 2.5, 15, runtime.context.damageMember); }, - animationCue: () => "idle", + animationCue: () => "attack", }; export const BOSS_MECHANIC_REGISTRY: Record = { diff --git a/src/game/bosses/shared.ts b/src/game/bosses/shared.ts index 2a19e09..edc4d61 100644 --- a/src/game/bosses/shared.ts +++ b/src/game/bosses/shared.ts @@ -94,6 +94,7 @@ export function createBaseMotion(bossId: BossId): BossMotionState { nextMechanicAt: Number.POSITIVE_INFINITY, mechanicCount: 0, phaseStartedAt: 0, + lastMeleeAt: -1, mechanicHitIds: [], mechanicNextDamageAt: {}, tetherIds: [], @@ -170,15 +171,28 @@ export function applyMelee( ) { while (boss.nextMeleeAt <= time) { if (motion.mode === "holding") { - const tankIndex = party.findIndex((member) => member.id === "brann"); - if (tankIndex >= 0 && party[tankIndex].hp > 0) { - party[tankIndex] = damageMember(party[tankIndex], amount, positions.brann, boss.nextMeleeAt); + const targetIndex = selectMeleeTargetIndex(party); + if (targetIndex >= 0) { + const target = party[targetIndex]; + party[targetIndex] = damageMember(target, amount, positions[target.id], boss.nextMeleeAt); + motion.lastMeleeAt = boss.nextMeleeAt; } } boss.nextMeleeAt += interval; } } +/** Keeps legacy Brann aggro, then fails over to another living tank or ally. */ +export function selectMeleeTargetIndex(party: readonly PartyMember[]): number { + const preferredTank = party.findIndex((member) => member.id === "brann" && member.hp > 0); + if (preferredTank >= 0) return preferredTank; + const otherTank = party.findIndex((member) => member.role === "Tank" && member.hp > 0); + if (otherTank >= 0) return otherTank; + const companion = party.findIndex((member) => member.role !== "Healer" && member.hp > 0); + if (companion >= 0) return companion; + return party.findIndex((member) => member.hp > 0); +} + export function resolveCircleHazards( motion: BossMotionState, party: PartyMember[], diff --git a/src/game/bosses/types.ts b/src/game/bosses/types.ts index 86fce65..4c886b9 100644 --- a/src/game/bosses/types.ts +++ b/src/game/bosses/types.ts @@ -16,6 +16,7 @@ export interface BossMechanicResult { } export interface BossMechanicContext { + arenaLayout: "standard" | "hockey-healing" | "hockey-healing-pvp" | "aether-assault"; boss: BossState; motion: BossMotionState; party: PartyMember[]; diff --git a/src/game/bottomTabs.test.ts b/src/game/bottomTabs.test.ts new file mode 100644 index 0000000..49c82ef --- /dev/null +++ b/src/game/bottomTabs.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { bottomTabsFor, cycleBottomTab } from "./bottomTabs"; + +describe("bottom display tabs", () => { + it("replaces Pack with PVP only during a PVP run", () => { + expect(bottomTabsFor("hockey-healing-pvp")).toEqual(["combat", "map", "pvp"]); + expect(bottomTabsFor("hockey-healing")).toEqual(["combat", "map", "pack"]); + expect(bottomTabsFor("encounter")).toEqual(["combat", "map", "pack"]); + }); + + it("cycles every controller-accessible PVP tab", () => { + expect(cycleBottomTab("combat", "hockey-healing-pvp")).toBe("map"); + expect(cycleBottomTab("map", "hockey-healing-pvp")).toBe("pvp"); + expect(cycleBottomTab("pvp", "hockey-healing-pvp")).toBe("combat"); + }); + + it("recovers to the first valid tab after a mode changes", () => { + expect(cycleBottomTab("pack", "hockey-healing-pvp")).toBe("combat"); + expect(cycleBottomTab("pvp", "encounter")).toBe("combat"); + }); +}); diff --git a/src/game/bottomTabs.ts b/src/game/bottomTabs.ts new file mode 100644 index 0000000..92d40dd --- /dev/null +++ b/src/game/bottomTabs.ts @@ -0,0 +1,15 @@ +import type { BottomTab, RunMode } from "./types"; +import { isPvpRunMode } from "./runModes"; + +const STANDARD_BOTTOM_TABS = ["combat", "map", "pack"] as const satisfies readonly BottomTab[]; +const PVP_BOTTOM_TABS = ["combat", "map", "pvp"] as const satisfies readonly BottomTab[]; + +export function bottomTabsFor(runMode: RunMode): readonly BottomTab[] { + return isPvpRunMode(runMode) ? PVP_BOTTOM_TABS : STANDARD_BOTTOM_TABS; +} + +export function cycleBottomTab(activeTab: BottomTab, runMode: RunMode): BottomTab { + const tabs = bottomTabsFor(runMode); + const currentIndex = tabs.indexOf(activeTab); + return tabs[(currentIndex + 1) % tabs.length]; +} diff --git a/src/game/characterAppearance.test.ts b/src/game/characterAppearance.test.ts new file mode 100644 index 0000000..bf9bd5d --- /dev/null +++ b/src/game/characterAppearance.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, it } from "vitest"; +import { + CHARACTER_PART_CATALOG, + CHARACTER_WEAPON_GRIP_BY_MODEL, + characterAppearancePartIds, + normalizeCharacterAppearance, + resolveCharacterModelMode, + type CharacterAppearanceV1, + type CharacterPartSlot, +} from "./characterAppearance"; +import { createDefaultHealerAppearance, HEALER_VISUAL_PROFILES } from "./healerVisuals"; + +const EXPECTED_SLOTS: readonly CharacterPartSlot[] = ["head", "upper-body", "lower-body"]; + +describe("modular character appearance", () => { + it("defaults to modular rendering and supports query or build rollback", () => { + expect(resolveCharacterModelMode({})).toBe("modular"); + expect(resolveCharacterModelMode({ search: "?characterModels=legacy" })).toBe("legacy"); + expect(resolveCharacterModelMode({ search: "?legacyCharacterModels" })).toBe("legacy"); + expect(resolveCharacterModelMode({ envMode: "legacy", search: "?characterModels=modular" })).toBe("legacy"); + }); + + it("gives every healer a valid versioned appearance", () => { + for (const [classId, profile] of Object.entries(HEALER_VISUAL_PROFILES)) { + const appearance: CharacterAppearanceV1 = profile.appearance; + expect(appearance.version, classId).toBe(1); + expect(appearance.rigId, classId).toBe("medium"); + const slots = characterAppearancePartIds(appearance) + .map((partId) => CHARACTER_PART_CATALOG[partId].slot); + for (const requiredSlot of EXPECTED_SLOTS) expect(slots, `${classId}:${requiredSlot}`).toContain(requiredSlot); + expect(new Set(slots).size, classId).toBe(slots.length); + } + }); + + it("keeps every catalog part inside one declared slot", () => { + for (const [partId, definition] of Object.entries(CHARACTER_PART_CATALOG)) { + expect(definition.nodeNames.length, partId).toBeGreaterThan(0); + expect(new Set(definition.nodeNames).size, partId).toBe(definition.nodeNames.length); + } + }); + + it("normalizes mixed appearances and canonicalizes every weapon grip", () => { + const fallback = createDefaultHealerAppearance("priest"); + const mixed = normalizeCharacterAppearance({ + version: 1, + rigId: "medium", + scaleSourceMemberId: "brann", + headPartId: "rogue-head", + upperBodyPartId: "knight-upper", + lowerBodyPartId: "ranger-lower", + headwearPartId: "mage-hat", + backPartId: "druid-backpack", + mainHand: { modelId: "sword", grip: "staff" }, + offHand: { modelId: "spellbook", grip: "sword" }, + }, fallback); + + expect(mixed).toEqual({ + version: 1, + rigId: "medium", + scaleSourceMemberId: "brann", + headPartId: "rogue-head", + upperBodyPartId: "knight-upper", + lowerBodyPartId: "ranger-lower", + headwearPartId: "mage-hat", + backPartId: "druid-backpack", + mainHand: { modelId: "cc/adv_sword_1handed", grip: "upright" }, + offHand: { modelId: "cc/spellbook_open", grip: "prop" }, + }); + expect(mixed).not.toBe(fallback); + expect(mixed.mainHand).not.toBe(fallback.mainHand); + expect(Object.keys(CHARACTER_WEAPON_GRIP_BY_MODEL)).toHaveLength(54); + expect(CHARACTER_WEAPON_GRIP_BY_MODEL).toMatchObject({ + "cc/adv_druid_staff": "staff", + "cc/adv_sword_1handed": "upright", + "cc/shield_badge": "prop", + "cc/adv_wand": "wand", + "cc/adv_dagger": "dagger", + "cc/crossbow_2handed": "crossbow", + }); + }); + + it("repairs invalid fields without discarding safe choices", () => { + const fallback = createDefaultHealerAppearance("paladin"); + const normalized = normalizeCharacterAppearance({ + version: 1, + rigId: "medium", + scaleSourceMemberId: "intruder", + headPartId: "mage-upper", + upperBodyPartId: "rogue-upper", + lowerBodyPartId: "toString", + headwearPartId: "druid-backpack", + backPartId: null, + mainHand: { modelId: "wand", grip: "invalid-grip" }, + offHand: { modelId: "toString", grip: "prop" }, + }, fallback); + + expect(normalized).toMatchObject({ + scaleSourceMemberId: fallback.scaleSourceMemberId, + headPartId: fallback.headPartId, + upperBodyPartId: "rogue-upper", + lowerBodyPartId: fallback.lowerBodyPartId, + headwearPartId: fallback.headwearPartId, + backPartId: null, + mainHand: { modelId: "cc/adv_wand", grip: "wand" }, + }); + expect(normalized.offHand).toEqual(fallback.offHand); + }); + + it("falls back as one unit for unknown appearance versions or rigs", () => { + const fallback = createDefaultHealerAppearance("druid"); + expect(normalizeCharacterAppearance({ ...fallback, version: 2 }, fallback)).toEqual(fallback); + expect(normalizeCharacterAppearance({ ...fallback, rigId: "large" }, fallback)).toEqual(fallback); + expect(normalizeCharacterAppearance(null, fallback)).toEqual(fallback); + expect(normalizeCharacterAppearance(null, fallback)).not.toBe(fallback); + }); +}); diff --git a/src/game/characterAppearance.ts b/src/game/characterAppearance.ts new file mode 100644 index 0000000..9ad0a39 --- /dev/null +++ b/src/game/characterAppearance.ts @@ -0,0 +1,210 @@ +import type { MemberId } from "./types"; +import { + CHARACTER_WEAPON_MODEL_IDS, + heldItemForModel, + resolveCharacterWeaponModelId, + weaponDefinition, + weaponSupportsSlot, + type CharacterHeldItemVisual, + type CharacterWeaponGrip, + type CharacterWeaponModelId, +} from "./weaponCatalog"; + +export type { + CharacterHeldItemVisual, + CharacterWeaponGrip, + CharacterWeaponModelId, +} from "./weaponCatalog"; + +/** Compatibility map for callers that still need direct model-to-grip lookup. */ +export const CHARACTER_WEAPON_GRIP_BY_MODEL = Object.fromEntries( + CHARACTER_WEAPON_MODEL_IDS.map((modelId) => [modelId, weaponDefinition(modelId).grip]), +) as Record; + +export type CharacterPartSlot = "head" | "upper-body" | "lower-body" | "headwear" | "back"; + +export interface CharacterPartDefinition { + slot: CharacterPartSlot; + sourceMemberId: MemberId; + nodeNames: readonly string[]; +} + +export const CHARACTER_PART_CATALOG = { + "druid-head": { slot: "head", sourceMemberId: "aelia", nodeNames: ["Druid_Head"] }, + "mage-head": { slot: "head", sourceMemberId: "orin", nodeNames: ["Mage_Head"] }, + "ranger-head": { slot: "head", sourceMemberId: "nia", nodeNames: ["Ranger_Head"] }, + "knight-head": { slot: "head", sourceMemberId: "brann", nodeNames: ["Knight_Head"] }, + "rogue-head": { slot: "head", sourceMemberId: "vale", nodeNames: ["Rogue_Head"] }, + + "druid-upper": { slot: "upper-body", sourceMemberId: "aelia", nodeNames: ["Druid_ArmLeft", "Druid_ArmRight", "Druid_Body"] }, + "mage-upper": { slot: "upper-body", sourceMemberId: "orin", nodeNames: ["Mage_ArmLeft", "Mage_ArmRight", "Mage_Body"] }, + "ranger-upper": { slot: "upper-body", sourceMemberId: "nia", nodeNames: ["Ranger_ArmLeft", "Ranger_ArmRight", "Ranger_Body"] }, + "knight-upper": { slot: "upper-body", sourceMemberId: "brann", nodeNames: ["Knight_ArmLeft", "Knight_ArmRight", "Knight_Body"] }, + "rogue-upper": { slot: "upper-body", sourceMemberId: "vale", nodeNames: ["Rogue_ArmLeft", "Rogue_ArmRight", "Rogue_Body"] }, + + "druid-lower": { slot: "lower-body", sourceMemberId: "aelia", nodeNames: ["Druid_LegLeft", "Druid_LegRight"] }, + "mage-lower": { slot: "lower-body", sourceMemberId: "orin", nodeNames: ["Mage_LegLeft", "Mage_LegRight"] }, + "ranger-lower": { slot: "lower-body", sourceMemberId: "nia", nodeNames: ["Ranger_LegLeft", "Ranger_LegRight"] }, + "knight-lower": { slot: "lower-body", sourceMemberId: "brann", nodeNames: ["Knight_LegLeft", "Knight_LegRight"] }, + "rogue-lower": { slot: "lower-body", sourceMemberId: "vale", nodeNames: ["Rogue_LegLeft", "Rogue_LegRight"] }, + + "mage-hat": { slot: "headwear", sourceMemberId: "orin", nodeNames: ["Mage_Hat"] }, + "knight-helmet": { slot: "headwear", sourceMemberId: "brann", nodeNames: ["Knight_Helmet", "Knight_HelmetVisor"] }, + + "druid-backpack": { slot: "back", sourceMemberId: "aelia", nodeNames: ["Druid_Backpack"] }, + "mage-cape": { slot: "back", sourceMemberId: "orin", nodeNames: ["Mage_Cape"] }, + "ranger-cape": { slot: "back", sourceMemberId: "nia", nodeNames: ["Ranger_Cape"] }, + "ranger-quiver": { slot: "back", sourceMemberId: "nia", nodeNames: ["Ranger_Quiver"] }, + "knight-cape": { slot: "back", sourceMemberId: "brann", nodeNames: ["Knight_Cape"] }, + "rogue-cape": { slot: "back", sourceMemberId: "vale", nodeNames: ["Rogue_Cape"] }, +} as const satisfies Record; + +export type CharacterPartId = keyof typeof CHARACTER_PART_CATALOG; +export type CharacterPartIdFor = { + [PartId in CharacterPartId]: typeof CHARACTER_PART_CATALOG[PartId]["slot"] extends Slot ? PartId : never; +}[CharacterPartId]; + +export interface CharacterAppearanceV1 { + version: 1; + rigId: "medium"; + scaleSourceMemberId: MemberId; + headPartId: CharacterPartIdFor<"head">; + upperBodyPartId: CharacterPartIdFor<"upper-body">; + lowerBodyPartId: CharacterPartIdFor<"lower-body">; + headwearPartId: CharacterPartIdFor<"headwear"> | null; + backPartId: CharacterPartIdFor<"back"> | null; + mainHand: CharacterHeldItemVisual; + offHand?: CharacterHeldItemVisual; +} + +const MEMBER_IDS: readonly MemberId[] = ["aelia", "brann", "nia", "orin", "vale"]; + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function isMemberId(value: unknown): value is MemberId { + return typeof value === "string" && MEMBER_IDS.includes(value as MemberId); +} + +function canonicalHeldItem( + value: unknown, + fallback: CharacterHeldItemVisual, + slot: "main" | "off", +): CharacterHeldItemVisual { + const requestedModelId = isRecord(value) + ? resolveCharacterWeaponModelId(value.modelId) + : null; + const modelId = requestedModelId && weaponSupportsSlot(requestedModelId, slot) + ? requestedModelId + : fallback.modelId; + return heldItemForModel(modelId); +} + +function canonicalOptionalOffHand( + value: unknown, + fallback?: CharacterHeldItemVisual, +): CharacterHeldItemVisual | undefined { + if (value === undefined || value === null) return undefined; + const requestedModelId = isRecord(value) + ? resolveCharacterWeaponModelId(value.modelId) + : null; + if (requestedModelId && weaponSupportsSlot(requestedModelId, "off")) { + return heldItemForModel(requestedModelId); + } + return fallback ? heldItemForModel(fallback.modelId) : undefined; +} + +function partForSlot( + value: unknown, + slot: Slot, +): CharacterPartIdFor | null { + if (typeof value !== "string" + || !Object.prototype.hasOwnProperty.call(CHARACTER_PART_CATALOG, value)) return null; + const partId = value as CharacterPartId; + return CHARACTER_PART_CATALOG[partId].slot === slot + ? partId as CharacterPartIdFor + : null; +} + +function optionalPartForSlot( + value: unknown, + slot: Slot, + fallback: CharacterPartIdFor | null, +): CharacterPartIdFor | null { + if (value === null) return null; + return partForSlot(value, slot) ?? fallback; +} + +export function cloneCharacterAppearance(appearance: CharacterAppearanceV1): CharacterAppearanceV1 { + return { + ...appearance, + mainHand: heldItemForModel(appearance.mainHand.modelId), + ...(appearance.offHand + ? { offHand: heldItemForModel(appearance.offHand.modelId) } + : {}), + }; +} + +/** + * Repairs untrusted save/API data before it can select runtime model assets. + * Appearance has its own version so this backward-compatible save field does not + * require a hunter-save schema bump. + */ +export function normalizeCharacterAppearance( + value: unknown, + fallback: CharacterAppearanceV1, +): CharacterAppearanceV1 { + if (!isRecord(value) || value.version !== 1 || value.rigId !== "medium") { + return cloneCharacterAppearance(fallback); + } + + const mainHand = canonicalHeldItem(value.mainHand, fallback.mainHand, "main"); + const offHand = canonicalOptionalOffHand(value.offHand, fallback.offHand); + + return { + version: 1, + rigId: "medium", + scaleSourceMemberId: isMemberId(value.scaleSourceMemberId) + ? value.scaleSourceMemberId + : fallback.scaleSourceMemberId, + headPartId: partForSlot(value.headPartId, "head") ?? fallback.headPartId, + upperBodyPartId: partForSlot(value.upperBodyPartId, "upper-body") ?? fallback.upperBodyPartId, + lowerBodyPartId: partForSlot(value.lowerBodyPartId, "lower-body") ?? fallback.lowerBodyPartId, + headwearPartId: optionalPartForSlot(value.headwearPartId, "headwear", fallback.headwearPartId), + backPartId: optionalPartForSlot(value.backPartId, "back", fallback.backPartId), + mainHand, + ...(offHand ? { offHand } : {}), + }; +} + +export type CharacterModelMode = "modular" | "legacy"; + +export function resolveCharacterModelMode({ + envMode, + search, +}: { + envMode?: string; + search?: string; +}): CharacterModelMode { + if (envMode?.toLowerCase() === "legacy") return "legacy"; + const query = new URLSearchParams(search ?? ""); + return query.get("characterModels") === "legacy" || query.has("legacyCharacterModels") + ? "legacy" + : "modular"; +} + +export const CHARACTER_MODEL_MODE = resolveCharacterModelMode({ + envMode: import.meta.env.VITE_CHARACTER_MODEL_MODE, + search: typeof window === "undefined" ? "" : window.location.search, +}); + +export function characterAppearancePartIds(appearance: CharacterAppearanceV1): CharacterPartId[] { + return [ + appearance.headPartId, + appearance.upperBodyPartId, + appearance.lowerBodyPartId, + ...(appearance.headwearPartId ? [appearance.headwearPartId] : []), + ...(appearance.backPartId ? [appearance.backPartId] : []), + ]; +} diff --git a/src/game/characterEquipment.test.ts b/src/game/characterEquipment.test.ts new file mode 100644 index 0000000..4505f8d --- /dev/null +++ b/src/game/characterEquipment.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { createDefaultHealerAppearance } from "./healerVisuals"; +import { heldItemForModel, weaponDefinitionsForSlot } from "./weaponCatalog"; +import { resolveCharacterEquipment } from "./characterEquipment"; + +describe("character equipment resolution", () => { + it("hides but never deletes a saved offhand behind a two-handed weapon", () => { + const appearance = { + ...createDefaultHealerAppearance("paladin"), + mainHand: heldItemForModel("cc/staff_d"), + }; + const resolved = resolveCharacterEquipment(appearance); + expect(resolved.offHand).toBeUndefined(); + expect(appearance.offHand).toEqual(heldItemForModel("cc/shield_badge")); + + const restored = resolveCharacterEquipment({ + ...appearance, + mainHand: heldItemForModel("cc/adv_sword_1handed"), + }); + expect(restored.offHand).toEqual(appearance.offHand); + }); + + it("pairs every crossbow with the loose Claudecraft quiver only at render time", () => { + const base = createDefaultHealerAppearance("priest"); + const crossbows = weaponDefinitionsForSlot("main").filter((entry) => entry.category === "crossbow"); + expect(crossbows).toHaveLength(3); + for (const crossbow of crossbows) { + const appearance = { ...base, mainHand: heldItemForModel(crossbow.id) }; + expect(resolveCharacterEquipment(appearance)).toMatchObject({ + backPropModelId: "cc/quiver", + suppressSkinnedBack: true, + }); + expect(appearance.backPartId).toBe(base.backPartId); + } + expect(resolveCharacterEquipment(base).backPropModelId).toBeUndefined(); + }); +}); diff --git a/src/game/characterEquipment.ts b/src/game/characterEquipment.ts new file mode 100644 index 0000000..ae6db1e --- /dev/null +++ b/src/game/characterEquipment.ts @@ -0,0 +1,24 @@ +import type { CharacterAppearanceV1, CharacterHeldItemVisual } from "./characterAppearance"; +import { + weaponDefinition, + weaponUsesBothHands, + type CharacterBackPropModelId, +} from "./weaponCatalog"; + +export interface ResolvedCharacterEquipment { + mainHand: CharacterHeldItemVisual; + /** Effective render choice. The saved offhand remains untouched while hidden. */ + offHand?: CharacterHeldItemVisual; + backPropModelId?: CharacterBackPropModelId; + suppressSkinnedBack: boolean; +} + +export function resolveCharacterEquipment(appearance: CharacterAppearanceV1): ResolvedCharacterEquipment { + const crossbowEquipped = weaponDefinition(appearance.mainHand.modelId).category === "crossbow"; + return { + mainHand: appearance.mainHand, + ...(weaponUsesBothHands(appearance.mainHand.modelId) ? {} : appearance.offHand ? { offHand: appearance.offHand } : {}), + ...(crossbowEquipped ? { backPropModelId: "cc/quiver" as const } : {}), + suppressSkinnedBack: crossbowEquipped, + }; +} diff --git a/src/game/controllerBindings.test.ts b/src/game/controllerBindings.test.ts index c84ba1e..d67d2e1 100644 --- a/src/game/controllerBindings.test.ts +++ b/src/game/controllerBindings.test.ts @@ -4,12 +4,12 @@ import { ABILITY_BY_CONTROLLER_BUTTON, ABILITY_CONTROLLER_BINDINGS } from "./con describe("PlayStation controller ability bindings", () => { it("keeps prompts aligned with standard gamepad button indices", () => { expect(ABILITY_BY_CONTROLLER_BUTTON).toEqual({ - 0: "purify", - 1: "shield", - 2: "mend", - 3: "renew", - 4: "radiance", - 5: "barrier", + 0: "ability4", + 1: "ability3", + 2: "ability1", + 3: "ability2", + 4: "ability5", + 5: "ability6", }); expect(Object.values(ABILITY_CONTROLLER_BINDINGS).map(({ glyph }) => glyph)).toEqual([ "□", diff --git a/src/game/controllerBindings.ts b/src/game/controllerBindings.ts index 22d21aa..535b470 100644 --- a/src/game/controllerBindings.ts +++ b/src/game/controllerBindings.ts @@ -1,20 +1,20 @@ import { DEFAULT_CONTROLLER_GLYPHS } from "../input/controllerGlyphs"; -import type { AbilityId } from "./types"; +import type { AbilitySlotId } from "./types"; interface AbilityControllerBinding { buttonIndex: number; glyph: string; } -export const ABILITY_CONTROLLER_BINDINGS: Record = { - mend: { buttonIndex: 2, glyph: DEFAULT_CONTROLLER_GLYPHS.faceLeft }, - renew: { buttonIndex: 3, glyph: DEFAULT_CONTROLLER_GLYPHS.faceTop }, - shield: { buttonIndex: 1, glyph: DEFAULT_CONTROLLER_GLYPHS.faceRight }, - purify: { buttonIndex: 0, glyph: DEFAULT_CONTROLLER_GLYPHS.faceBottom }, - radiance: { buttonIndex: 4, glyph: DEFAULT_CONTROLLER_GLYPHS.leftShoulder }, - barrier: { buttonIndex: 5, glyph: DEFAULT_CONTROLLER_GLYPHS.rightShoulder }, +export const ABILITY_CONTROLLER_BINDINGS: Record = { + ability1: { buttonIndex: 2, glyph: DEFAULT_CONTROLLER_GLYPHS.faceLeft }, + ability2: { buttonIndex: 3, glyph: DEFAULT_CONTROLLER_GLYPHS.faceTop }, + ability3: { buttonIndex: 1, glyph: DEFAULT_CONTROLLER_GLYPHS.faceRight }, + ability4: { buttonIndex: 0, glyph: DEFAULT_CONTROLLER_GLYPHS.faceBottom }, + ability5: { buttonIndex: 4, glyph: DEFAULT_CONTROLLER_GLYPHS.leftShoulder }, + ability6: { buttonIndex: 5, glyph: DEFAULT_CONTROLLER_GLYPHS.rightShoulder }, }; export const ABILITY_BY_CONTROLLER_BUTTON = Object.fromEntries( Object.entries(ABILITY_CONTROLLER_BINDINGS).map(([abilityId, binding]) => [binding.buttonIndex, abilityId]), -) as Partial>; +) as Partial>; diff --git a/src/game/data.ts b/src/game/data.ts index cf6af4e..cf40c18 100644 --- a/src/game/data.ts +++ b/src/game/data.ts @@ -1,17 +1,17 @@ import { HEALER_CLASSES } from "./healers"; -import type { AbilityId, HealerClassId, PartyMember } from "./types"; +import type { AbilitySlotId, HealerClassId, PartyMember } from "./types"; export const ABILITIES = HEALER_CLASSES.priest.abilities; -export const ABILITY_ORDER: AbilityId[] = ["mend", "renew", "shield", "purify", "radiance", "barrier"]; +export const ABILITY_ORDER: AbilitySlotId[] = ["ability1", "ability2", "ability3", "ability4", "ability5", "ability6"]; export function freshParty(classId: HealerClassId = "priest", playerName = "Aelia"): PartyMember[] { const healer = HEALER_CLASSES[classId]; return [ - { id: "aelia", name: playerName, className: healer.specialization, role: "Healer", color: healer.color, maxHp: 100, hp: 100, absorb: 0, renewExpiresAt: 0, renewNextTickAt: 0, knockedUntil: 0, debuffs: [] }, - { id: "brann", name: "Brann", className: "Knight", role: "Tank", color: "#69a8dd", maxHp: 150, hp: 150, absorb: 0, renewExpiresAt: 0, renewNextTickAt: 0, knockedUntil: 0, debuffs: [] }, - { id: "nia", name: "Nia", className: "Ranger", role: "Damage", color: "#74c987", maxHp: 94, hp: 94, absorb: 0, renewExpiresAt: 0, renewNextTickAt: 0, knockedUntil: 0, debuffs: [] }, - { id: "orin", name: "Orin", className: "Mage", role: "Damage", color: "#b17ee6", maxHp: 86, hp: 86, absorb: 0, renewExpiresAt: 0, renewNextTickAt: 0, knockedUntil: 0, debuffs: [] }, - { id: "vale", name: "Vale", className: "Rogue", role: "Damage", color: "#d97171", maxHp: 92, hp: 92, absorb: 0, renewExpiresAt: 0, renewNextTickAt: 0, knockedUntil: 0, debuffs: [] }, + { id: "aelia", name: playerName, className: healer.specialization, role: "Healer", color: healer.color, maxHp: 100, hp: 100, absorb: 0, healingEffects: [], reactiveHeal: null, knockedUntil: 0, debuffs: [] }, + { id: "brann", name: "Brann", className: "Knight", role: "Tank", color: "#69a8dd", maxHp: 150, hp: 150, absorb: 0, healingEffects: [], reactiveHeal: null, knockedUntil: 0, debuffs: [] }, + { id: "nia", name: "Nia", className: "Ranger", role: "Damage", color: "#74c987", maxHp: 94, hp: 94, absorb: 0, healingEffects: [], reactiveHeal: null, knockedUntil: 0, debuffs: [] }, + { id: "orin", name: "Orin", className: "Mage", role: "Damage", color: "#b17ee6", maxHp: 86, hp: 86, absorb: 0, healingEffects: [], reactiveHeal: null, knockedUntil: 0, debuffs: [] }, + { id: "vale", name: "Vale", className: "Rogue", role: "Damage", color: "#d97171", maxHp: 92, hp: 92, absorb: 0, healingEffects: [], reactiveHeal: null, knockedUntil: 0, debuffs: [] }, ]; } diff --git a/src/game/healerClasses.test.ts b/src/game/healerClasses.test.ts new file mode 100644 index 0000000..0a40f75 --- /dev/null +++ b/src/game/healerClasses.test.ts @@ -0,0 +1,291 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { healingEffect } from "./healerEffects"; +import { createClassInventory } from "./healers"; +import { barrierProtects, useGameStore } from "./store"; +import type { HealerClassId, MemberId, WorldPosition } from "./types"; + +function startQuietEncounter(classId: HealerClassId) { + useGameStore.getState().configureHealer(classId, "Aelia", createClassInventory(classId)); + useGameStore.getState().startEncounter(); + useGameStore.setState((state) => ({ + boss: { ...state.boss, nextMeleeAt: 999 }, + bossMotion: { ...state.bossMotion, nextMechanicAt: 999 }, + })); +} + +function member(id: MemberId) { + return useGameStore.getState().party.find((candidate) => candidate.id === id)!; +} + +function setHealth(values: Partial>) { + useGameStore.setState((state) => ({ + party: state.party.map((candidate) => values[candidate.id] === undefined + ? candidate + : { ...candidate, hp: values[candidate.id]! }), + })); +} + +describe("Restoration Druid combat kit", () => { + beforeEach(() => startQuietEncounter("druid")); + + it("generates Verdancy from healing ticks and spends it on Regrowth", () => { + setHealth({ brann: 50 }); + expect(useGameStore.getState().castAbility("ability2")).toBe(true); + useGameStore.getState().tick(1.01); + expect(useGameStore.getState().healerMechanic.resource).toBe(1); + + expect(useGameStore.getState().castAbility("ability1")).toBe(true); + expect(useGameStore.getState().activeCast?.resourceSpent).toBe(1); + expect(useGameStore.getState().healerMechanic.resource).toBe(0); + useGameStore.getState().tick(0.51); + + expect(member("brann").hp).toBeCloseTo(87); + expect(healingEffect(member("brann"), "regrowth")).toBeDefined(); + }); + + it("lets Nature's Cure trigger every active healing effect immediately", () => { + setHealth({ brann: 50 }); + useGameStore.getState().castAbility("ability2"); + useGameStore.getState().tick(0.51); + useGameStore.setState((state) => ({ + party: state.party.map((candidate) => candidate.id === "brann" + ? { ...candidate, debuffs: [{ id: "test", name: "Test Hex", expiresAt: 10, nextTickAt: 9, tickDamage: 1 }] } + : candidate), + })); + + expect(useGameStore.getState().castAbility("ability4")).toBe(true); + expect(member("brann").hp).toBe(56); + expect(member("brann").debuffs).toEqual([]); + }); + + it("stacks Lifebloom, then spends Verdancy to force its bloom", () => { + setHealth({ brann: 40 }); + for (let stack = 1; stack <= 3; stack += 1) { + expect(useGameStore.getState().castAbility("ability3")).toBe(true); + expect(healingEffect(member("brann"), "lifebloom")?.stacks).toBe(stack); + useGameStore.getState().tick(0.51); + } + useGameStore.setState((state) => ({ + party: state.party.map((candidate) => candidate.id === "brann" ? { ...candidate, hp: 40 } : candidate), + healerMechanic: { ...state.healerMechanic, resource: 2 }, + })); + + expect(useGameStore.getState().castAbility("ability3")).toBe(true); + expect(healingEffect(member("brann"), "lifebloom")).toBeUndefined(); + expect(member("brann").hp).toBe(70); + expect(useGameStore.getState().healerMechanic.resource).toBe(0); + }); + + it("targets only three most injured allies with Wild Growth", () => { + setHealth({ aelia: 90, brann: 40, nia: 20, orin: 30, vale: 80 }); + expect(useGameStore.getState().castAbility("ability5")).toBe(true); + const grown = useGameStore.getState().party + .filter((candidate) => healingEffect(candidate, "wild-growth")) + .map((candidate) => candidate.id); + expect(grown).toEqual(["brann", "nia", "orin"]); + }); + + it("extends active growth and accelerates its next tick with Flourish", () => { + setHealth({ brann: 50 }); + useGameStore.getState().castAbility("ability2"); + useGameStore.getState().tick(0.51); + expect(useGameStore.getState().castAbility("ability6")).toBe(true); + + const effect = healingEffect(member("brann"), "rejuvenation")!; + expect(effect.expiresAt).toBe(13); + expect(effect.nextTickAt).toBeCloseTo(1); + expect(useGameStore.getState().healerMechanic.flourishExpiresAt).toBeCloseTo(6.51); + useGameStore.getState().tick(0.5); + expect(healingEffect(member("brann"), "rejuvenation")?.nextTickAt).toBeCloseTo(1.5); + }); +}); + +describe("Restoration Shaman combat kit", () => { + beforeEach(() => startQuietEncounter("shaman")); + + it("builds Tidal Surge with Riptide and spends it on Healing Wave", () => { + setHealth({ brann: 40 }); + expect(useGameStore.getState().castAbility("ability2")).toBe(true); + expect(useGameStore.getState().healerMechanic.resource).toBe(1); + useGameStore.getState().tick(0.51); + + expect(useGameStore.getState().castAbility("ability1")).toBe(true); + expect(useGameStore.getState().activeCast?.resourceSpent).toBe(1); + expect(useGameStore.getState().activeCast?.completesAt).toBeCloseTo(0.51 + 0.325); + useGameStore.getState().tick(0.34); + + expect(member("brann").hp).toBeCloseTo(107.9); + expect(useGameStore.getState().healerMechanic.resource).toBe(0); + }); + + it("consumes Earth Shield after damage and generates Tidal Surge", () => { + expect(useGameStore.getState().castAbility("ability3")).toBe(true); + useGameStore.getState().tick(0.51); + useGameStore.setState((state) => ({ + party: state.party.map((candidate) => candidate.id === "brann" + ? { ...candidate, debuffs: [{ id: "shock", name: "Shock", expiresAt: 2, nextTickAt: 0.6, tickDamage: 20 }] } + : candidate), + })); + + useGameStore.getState().tick(0.1); + expect(member("brann").reactiveHeal?.charges).toBe(5); + expect(member("brann").hp).toBeLessThan(member("brann").maxHp); + expect(useGameStore.getState().healerMechanic.resource).toBe(1); + }); + + it("uses Tidal Surge to add positional Chain Heal jumps", () => { + const partyPositions: Record = { + brann: [0, 0], nia: [0, 1], orin: [0, 2], aelia: [0, 3], vale: [20, 20], + }; + setHealth({ aelia: 40, brann: 40, nia: 20, orin: 20, vale: 20 }); + useGameStore.setState({ partyPositions }); + useGameStore.getState().castAbility("ability2"); + useGameStore.getState().tick(0.51); + useGameStore.setState({ partyPositions }); + const valeBefore = member("vale").hp; + + expect(useGameStore.getState().castAbility("ability5")).toBe(true); + expect(useGameStore.getState().healerMechanic.resource).toBe(0); + expect(member("brann").hp).toBeGreaterThan(52); + expect(member("nia").hp).toBeGreaterThan(20); + expect(member("orin").hp).toBeGreaterThan(20); + expect(member("aelia").hp).toBeGreaterThan(40); + expect(member("vale").hp).toBe(valeBefore); + }); + + it("equalizes nearby health percentages without granting Barrier reduction", () => { + const partyPositions: Record = { + aelia: [0, 0], brann: [0, 1], nia: [0, 2], orin: [20, 20], vale: [20, 21], + }; + useGameStore.setState((state) => ({ + partyPositions, + party: state.party.map((candidate) => candidate.id === "aelia" + ? { ...candidate, hp: 20 } + : candidate.id === "brann" + ? { ...candidate, hp: 120 } + : candidate.id === "nia" + ? { ...candidate, hp: 47 } + : candidate), + })); + + expect(useGameStore.getState().castAbility("ability6")).toBe(true); + expect(barrierProtects([0, 0], useGameStore.getState().barrier, 1)).toBe(false); + useGameStore.getState().tick(1.01); + + expect(member("aelia").hp / member("aelia").maxHp).toBeCloseTo(0.5); + expect(member("brann").hp / member("brann").maxHp).toBeCloseTo(0.5); + expect(member("nia").hp / member("nia").maxHp).toBeCloseTo(0.5); + }); +}); + +describe("Dawnforged Paladin combat kit", () => { + beforeEach(() => startQuietEncounter("paladin")); + + it("echoes single-target healing through Beacon of Light", () => { + setHealth({ brann: 50, nia: 30 }); + expect(useGameStore.getState().castAbility("ability3")).toBe(true); + useGameStore.getState().tick(0.51); + useGameStore.getState().selectMember("nia"); + + expect(useGameStore.getState().castAbility("ability1")).toBe(true); + useGameStore.getState().tick(0.56); + + expect(member("nia").hp).toBeCloseTo(64); + expect(member("brann").hp).toBeCloseTo(63.6); + }); + + it("turns Crusader Strike into boss damage, triage healing, and Conviction", () => { + setHealth({ brann: 40 }); + const bossHp = useGameStore.getState().boss.hp; + + expect(useGameStore.getState().castAbility("ability2")).toBe(true); + + expect(useGameStore.getState().boss.hp).toBe(bossHp - 18); + expect(member("brann").hp).toBe(58); + expect(useGameStore.getState().healerMechanic.resource).toBe(1); + }); + + it("spends full Conviction on Word of Glory and splashes party healing", () => { + setHealth({ brann: 30, nia: 40 }); + useGameStore.setState((state) => ({ + healerMechanic: { ...state.healerMechanic, resource: 3 }, + })); + + expect(useGameStore.getState().castAbility("ability5")).toBe(true); + + expect(useGameStore.getState().healerMechanic.resource).toBe(0); + expect(member("brann").hp).toBe(98); + expect(member("nia").hp).toBe(48); + }); + + it("converts party attack damage into triage healing during Avenging Crusader", () => { + setHealth({ brann: 20 }); + expect(useGameStore.getState().castAbility("ability6")).toBe(true); + useGameStore.getState().tick(2); + + expect(useGameStore.getState().partyDamageEvents.length).toBeGreaterThan(0); + expect(member("brann").hp).toBeGreaterThan(20); + }); +}); + +describe("Continuum Chronomancer combat kit", () => { + beforeEach(() => startQuietEncounter("chronomancer")); + + it("anchors health, rewinds later damage, and gains a Chronoshard", () => { + setHealth({ brann: 80 }); + expect(useGameStore.getState().castAbility("ability2")).toBe(true); + useGameStore.getState().tick(0.51); + setHealth({ brann: 35 }); + + expect(useGameStore.getState().castAbility("ability2")).toBe(true); + + expect(member("brann").hp).toBe(80); + expect(useGameStore.getState().healerMechanic.resource).toBe(1); + expect(useGameStore.getState().healerMechanic.temporalAnchor).toBeNull(); + }); + + it("schedules Echo of Tomorrow without front-loading its delayed heal", () => { + setHealth({ brann: 30 }); + expect(useGameStore.getState().castAbility("ability3")).toBe(true); + expect(member("brann").hp).toBe(40); + + useGameStore.getState().tick(2); + useGameStore.getState().tick(0.9); + expect(member("brann").hp).toBe(40); + useGameStore.getState().tick(0.11); + + expect(member("brann").hp).toBe(68); + expect(healingEffect(member("brann"), "temporal-echo")).toBeUndefined(); + }); + + it("spends Chronoshards to heal the party and advance cooldowns", () => { + setHealth({ aelia: 70, brann: 50, nia: 40 }); + useGameStore.setState((state) => ({ + healerMechanic: { ...state.healerMechanic, resource: 3 }, + cooldowns: { ...state.cooldowns, ability3: 10 }, + })); + + expect(useGameStore.getState().castAbility("ability5")).toBe(true); + + expect(useGameStore.getState().healerMechanic.resource).toBe(0); + expect(useGameStore.getState().cooldowns.ability3).toBe(7); + expect(member("aelia").hp).toBe(100); + expect(member("brann").hp).toBe(84); + expect(member("nia").hp).toBe(74); + }); + + it("restores only health lost during Time Loop and never resurrects", () => { + setHealth({ aelia: 90, brann: 100, nia: 80 }); + expect(useGameStore.getState().castAbility("ability6")).toBe(true); + setHealth({ aelia: 30, brann: 50, nia: 0 }); + + useGameStore.getState().tick(2); + useGameStore.getState().tick(2); + useGameStore.getState().tick(2.01); + + expect(member("aelia").hp).toBe(90); + expect(member("brann").hp).toBe(100); + expect(member("nia").hp).toBe(0); + expect(useGameStore.getState().healerMechanic.timeLoop).toBeNull(); + }); +}); diff --git a/src/game/healerEffects.test.ts b/src/game/healerEffects.test.ts new file mode 100644 index 0000000..eaa306b --- /dev/null +++ b/src/game/healerEffects.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { freshParty } from "./data"; +import { advanceHealingEffects } from "./healerEffects"; + +describe("healing effect advancement", () => { + it("does not count resource-generating healing ticks on defeated members", () => { + const fallen = { + ...freshParty("druid", "Aelia")[0], + hp: 0, + healingEffects: [{ + id: "rejuvenation" as const, + expiresAt: 5, + nextTickAt: 1, + tickInterval: 1, + healingPerTick: 8, + stacks: 1, + }], + }; + + const advanced = advanceHealingEffects(fallen, 0, 3, 1); + + expect(advanced.member.hp).toBe(0); + expect(advanced.tickCount).toBe(0); + expect(advanced.tickCounts.rejuvenation).toBeUndefined(); + }); +}); diff --git a/src/game/healerEffects.ts b/src/game/healerEffects.ts new file mode 100644 index 0000000..dbf5b06 --- /dev/null +++ b/src/game/healerEffects.ts @@ -0,0 +1,195 @@ +import { distance } from "./geometry"; +import type { + HealingEffectId, + HealingOverTimeEffect, + MemberId, + PartyMember, + WorldPosition, +} from "./types"; + +const DRUID_EFFECTS = new Set(["regrowth", "rejuvenation", "lifebloom", "wild-growth"]); + +export interface HealingEffectAdvanceResult { + member: PartyMember; + tickCount: number; + tickCounts: Partial>; + bloomHealing: number; +} + +export function healingEffect(member: PartyMember, id: HealingEffectId): HealingOverTimeEffect | undefined { + return member.healingEffects.find((effect) => effect.id === id); +} + +export function setHealingEffect(member: PartyMember, effect: HealingOverTimeEffect): PartyMember { + const existingIndex = member.healingEffects.findIndex((candidate) => candidate.id === effect.id); + if (existingIndex < 0) return { ...member, healingEffects: [...member.healingEffects, effect] }; + const healingEffects = [...member.healingEffects]; + healingEffects[existingIndex] = effect; + return { ...member, healingEffects }; +} + +export function removeHealingEffect(member: PartyMember, id: HealingEffectId): PartyMember { + const healingEffects = member.healingEffects.filter((effect) => effect.id !== id); + return healingEffects.length === member.healingEffects.length ? member : { ...member, healingEffects }; +} + +export function triggerHealingEffects( + member: PartyMember, + healingMultiplier: number, + predicate: (effect: HealingOverTimeEffect) => boolean = () => true, +): { member: PartyMember; healing: number; triggered: number } { + if (member.hp <= 0) return { member, healing: 0, triggered: 0 }; + let healing = 0; + let triggered = 0; + for (const effect of member.healingEffects) { + if (!predicate(effect)) continue; + healing += effect.healingPerTick * effect.stacks * healingMultiplier; + triggered += 1; + } + return { + member: healing > 0 ? { ...member, hp: Math.min(member.maxHp, member.hp + healing) } : member, + healing, + triggered, + }; +} + +export function advanceHealingEffects( + member: PartyMember, + oldTime: number, + time: number, + healingMultiplier: number, + flourishExpiresAt = 0, +): HealingEffectAdvanceResult { + if (!member.healingEffects.length) return { member, tickCount: 0, tickCounts: {}, bloomHealing: 0 }; + let hp = member.hp; + let tickCount = 0; + const tickCounts: Partial> = {}; + let bloomHealing = 0; + const healingEffects: HealingOverTimeEffect[] = []; + + for (const current of member.healingEffects) { + if (current.expiresAt <= oldTime) continue; + let nextTickAt = current.nextTickAt; + const lastTickAt = Math.min(time, current.expiresAt); + while (nextTickAt <= lastTickAt + 0.001) { + if (hp > 0) { + hp = Math.min(member.maxHp, hp + current.healingPerTick * current.stacks * healingMultiplier); + tickCount += 1; + tickCounts[current.id] = (tickCounts[current.id] ?? 0) + 1; + } + const accelerated = DRUID_EFFECTS.has(current.id) && flourishExpiresAt > nextTickAt; + nextTickAt += accelerated ? Math.min(0.5, current.tickInterval) : current.tickInterval; + } + + if (time >= current.expiresAt) { + if (current.id === "lifebloom" && hp > 0) { + const bloom = (current.expirationHealingPerStack ?? 10) * current.stacks * healingMultiplier; + bloomHealing += bloom; + hp = Math.min(member.maxHp, hp + bloom); + } + continue; + } + + healingEffects.push({ ...current, nextTickAt }); + } + + return { + member: { ...member, hp, healingEffects }, + tickCount, + tickCounts, + bloomHealing, + }; +} + +export function flourishParty(party: readonly PartyMember[], time: number, extension = 5): PartyMember[] { + return party.map((member) => { + let changed = false; + const healingEffects = member.healingEffects.map((effect) => { + if (!DRUID_EFFECTS.has(effect.id) || effect.expiresAt <= time) return effect; + changed = true; + return { + ...effect, + expiresAt: effect.expiresAt + extension, + nextTickAt: Math.min(effect.nextTickAt, time + 0.5), + }; + }); + return changed ? { ...member, healingEffects } : member; + }); +} + +export function chainHealIndexes( + party: readonly PartyMember[], + positions: Readonly>, + startIndex: number, + maxTargets: number, + maxJumpDistance = 7, +): number[] { + if (startIndex < 0 || party[startIndex]?.hp <= 0 || maxTargets <= 0) return []; + const result = [startIndex]; + const visited = new Set(result); + while (result.length < maxTargets) { + const previousIndex = result[result.length - 1]; + const previousPosition = positions[party[previousIndex].id]; + const candidate = party + .map((member, index) => ({ + index, + member, + jumpDistance: distance(previousPosition, positions[member.id]), + })) + .filter(({ index, member, jumpDistance }) => !visited.has(index) + && member.hp > 0 + && member.hp < member.maxHp + && jumpDistance <= maxJumpDistance) + .sort((left, right) => left.jumpDistance - right.jumpDistance + || (left.member.hp / left.member.maxHp) - (right.member.hp / right.member.maxHp) + || left.index - right.index)[0]; + if (!candidate) break; + visited.add(candidate.index); + result.push(candidate.index); + } + return result; +} + +export function equalizeHealthPercentages( + party: readonly PartyMember[], + positions: Readonly>, + center: WorldPosition, + radius: number, +): PartyMember[] { + const linkedIndexes = party + .map((member, index) => ({ member, index })) + .filter(({ member }) => member.hp > 0 && distance(positions[member.id], center) <= radius) + .map(({ index }) => index); + if (linkedIndexes.length < 2) return [...party]; + const averagePercent = linkedIndexes.reduce((sum, index) => sum + party[index].hp / party[index].maxHp, 0) / linkedIndexes.length; + const linked = new Set(linkedIndexes); + return party.map((member, index) => linked.has(index) + ? { ...member, hp: Math.min(member.maxHp, member.maxHp * averagePercent) } + : member); +} + +export function resolveReactiveHealAfterDamage( + before: PartyMember, + after: PartyMember, + time: number, + healingMultiplier: number, +): { member: PartyMember; triggered: boolean } { + const effect = before.reactiveHeal; + const tookDamage = after.hp < before.hp || after.absorb < before.absorb; + if (!effect || effect.id !== "earth-shield" || effect.charges <= 0 || effect.expiresAt <= time + || effect.nextTriggerAt > time || after.hp <= 0 || !tookDamage) { + return { + member: effect && effect.expiresAt <= time ? { ...after, reactiveHeal: null } : after, + triggered: false, + }; + } + const charges = effect.charges - 1; + return { + member: { + ...after, + hp: Math.min(after.maxHp, after.hp + effect.healingPerTrigger * healingMultiplier), + reactiveHeal: charges > 0 ? { ...effect, charges, nextTriggerAt: time + 0.75 } : null, + }, + triggered: true, + }; +} diff --git a/src/game/healerMechanics.test.ts b/src/game/healerMechanics.test.ts new file mode 100644 index 0000000..4b61708 --- /dev/null +++ b/src/game/healerMechanics.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; +import { freshParty } from "./data"; +import { + createHealerMechanicState, + healTargetAndBeacon, + placeOrRewindTimeAnchor, + resolveTimeLoop, + startTimeLoop, +} from "./healerMechanics"; + +describe("shared healer mechanics", () => { + it("echoes a direct heal without healing dead or duplicate beacon targets", () => { + const party = freshParty("paladin").map((member) => member.id === "brann" + ? { ...member, hp: 50 } + : member.id === "nia" + ? { ...member, hp: 20 } + : member); + const mechanic = { + ...createHealerMechanicState("paladin"), + beaconTargetId: "brann" as const, + beaconExpiresAt: 10, + }; + + const result = healTargetAndBeacon(party, 2, 30, mechanic, 1); + expect(result.party[2].hp).toBe(50); + expect(result.party[1].hp).toBe(62); + expect(result.directHealing).toBe(30); + expect(result.beaconHealing).toBe(12); + expect(party[1].hp).toBe(50); + }); + + it("replaces an anchor on a new target and rewinds only its recorded target", () => { + const party = freshParty("chronomancer").map((member) => member.id === "brann" ? { ...member, hp: 90 } : member); + const mechanic = createHealerMechanicState("chronomancer"); + const anchored = placeOrRewindTimeAnchor(party, 1, mechanic, 1); + const damaged = anchored.party.map((member) => member.id === "brann" ? { ...member, hp: 35 } : member); + const rewound = placeOrRewindTimeAnchor(damaged, 1, anchored.mechanic, 2); + + expect(rewound.party[1].hp).toBe(90); + expect(rewound.restored).toBe(55); + expect(rewound.mechanic.temporalAnchor).toBeNull(); + }); + + it("resolves a party snapshot once without resurrecting defeated allies", () => { + const party = freshParty("chronomancer"); + const mechanic = startTimeLoop(party, createHealerMechanicState("chronomancer"), 5); + const damaged = party.map((member) => member.id === "brann" + ? { ...member, hp: 40 } + : member.id === "nia" + ? { ...member, hp: 0 } + : member); + const early = resolveTimeLoop(damaged, mechanic, 4.9); + const resolved = resolveTimeLoop(early.party, early.mechanic, 5); + + expect(early.party[1].hp).toBe(40); + expect(resolved.party[1].hp).toBe(150); + expect(resolved.party[2].hp).toBe(0); + expect(resolved.mechanic.timeLoop).toBeNull(); + }); +}); diff --git a/src/game/healerMechanics.ts b/src/game/healerMechanics.ts new file mode 100644 index 0000000..60c1e56 --- /dev/null +++ b/src/game/healerMechanics.ts @@ -0,0 +1,163 @@ +import type { HealerClassId, HealerMechanicState, MemberId, PartyMember } from "./types"; + +export interface BeaconHealResult { + party: PartyMember[]; + directHealing: number; + beaconHealing: number; +} + +export interface TimeAnchorResult { + party: PartyMember[]; + mechanic: HealerMechanicState; + restored: number; + anchored: boolean; +} + +export interface TimeLoopResult { + party: PartyMember[]; + mechanic: HealerMechanicState; + restored: number; +} + +export function healerMaxResource(classId: HealerClassId): number { + if (classId === "druid") return 5; + if (classId === "shaman") return 2; + if (classId === "paladin" || classId === "chronomancer") return 3; + return 0; +} + +export function createHealerMechanicState(classId: HealerClassId): HealerMechanicState { + return { + resource: 0, + maxResource: healerMaxResource(classId), + flourishExpiresAt: 0, + nextPulseAt: 0, + beaconTargetId: null, + beaconExpiresAt: 0, + avengingCrusaderExpiresAt: 0, + temporalAnchor: null, + timeLoop: null, + }; +} + +function healLiving(member: PartyMember, amount: number): PartyMember { + if (member.hp <= 0 || amount <= 0) return member; + return { ...member, hp: Math.min(member.maxHp, member.hp + amount) }; +} + +export function healTargetAndBeacon( + party: readonly PartyMember[], + targetIndex: number, + amount: number, + mechanic: HealerMechanicState, + time: number, + echoFraction = 0.4, +): BeaconHealResult { + const target = party[targetIndex]; + if (!target || target.hp <= 0) return { party: [...party], directHealing: 0, beaconHealing: 0 }; + const next = [...party]; + const healedTarget = healLiving(target, amount); + const directHealing = healedTarget.hp - target.hp; + next[targetIndex] = healedTarget; + + let beaconHealing = 0; + if (mechanic.beaconTargetId && mechanic.beaconExpiresAt > time && mechanic.beaconTargetId !== target.id) { + const beaconIndex = next.findIndex((member) => member.id === mechanic.beaconTargetId); + const beacon = next[beaconIndex]; + if (beacon?.hp > 0) { + const healedBeacon = healLiving(beacon, directHealing * echoFraction); + beaconHealing = healedBeacon.hp - beacon.hp; + next[beaconIndex] = healedBeacon; + } + } + return { party: next, directHealing, beaconHealing }; +} + +export function placeOrRewindTimeAnchor( + party: readonly PartyMember[], + targetIndex: number, + mechanic: HealerMechanicState, + time: number, + duration = 6, +): TimeAnchorResult { + const target = party[targetIndex]; + if (!target || target.hp <= 0) return { party: [...party], mechanic, restored: 0, anchored: false }; + const anchor = mechanic.temporalAnchor; + if (anchor?.targetId === target.id && anchor.expiresAt > time) { + const restoredHp = Math.max(target.hp, Math.min(target.maxHp, anchor.hp)); + const next = [...party]; + next[targetIndex] = { ...target, hp: restoredHp }; + return { + party: next, + mechanic: { ...mechanic, temporalAnchor: null }, + restored: restoredHp - target.hp, + anchored: false, + }; + } + return { + party: [...party], + mechanic: { + ...mechanic, + temporalAnchor: { targetId: target.id, hp: target.hp, expiresAt: time + duration }, + }, + restored: 0, + anchored: true, + }; +} + +export function startTimeLoop(party: readonly PartyMember[], mechanic: HealerMechanicState, restoresAt: number): HealerMechanicState { + return { + ...mechanic, + timeLoop: { + restoresAt, + partyHp: Object.fromEntries(party.map((member) => [member.id, member.hp])) as Record, + }, + }; +} + +export function resolveTimeLoop( + party: PartyMember[], + mechanic: HealerMechanicState, + time: number, +): TimeLoopResult { + const loop = mechanic.timeLoop; + if (!loop || loop.restoresAt > time) return { party, mechanic, restored: 0 }; + let restored = 0; + const next = party.map((member) => { + if (member.hp <= 0) return member; + const hp = Math.max(member.hp, Math.min(member.maxHp, loop.partyHp[member.id])); + restored += hp - member.hp; + return hp === member.hp ? member : { ...member, hp }; + }); + return { party: next, mechanic: { ...mechanic, timeLoop: null }, restored }; +} + +export function healMostInjured(party: readonly PartyMember[], amount: number): { party: PartyMember[]; targetId: MemberId | null; healing: number } { + let targetIndex = -1; + let lowestRatio = Number.POSITIVE_INFINITY; + for (let index = 0; index < party.length; index += 1) { + const member = party[index]; + if (member.hp <= 0 || member.hp >= member.maxHp) continue; + const ratio = member.hp / member.maxHp; + if (ratio < lowestRatio) { + lowestRatio = ratio; + targetIndex = index; + } + } + if (targetIndex < 0) return { party: [...party], targetId: null, healing: 0 }; + const next = [...party]; + const before = next[targetIndex]; + next[targetIndex] = healLiving(before, amount); + return { party: next, targetId: before.id, healing: next[targetIndex].hp - before.hp }; +} + +export function expireHealerMechanics(mechanic: HealerMechanicState, time: number): HealerMechanicState { + let next = mechanic; + if (next.beaconTargetId && next.beaconExpiresAt <= time) { + next = { ...next, beaconTargetId: null, beaconExpiresAt: 0 }; + } + if (next.temporalAnchor && next.temporalAnchor.expiresAt <= time) { + next = { ...next, temporalAnchor: null }; + } + return next; +} diff --git a/src/game/healerRigAssets.test.ts b/src/game/healerRigAssets.test.ts new file mode 100644 index 0000000..e1ddade --- /dev/null +++ b/src/game/healerRigAssets.test.ts @@ -0,0 +1,93 @@ +import { fileURLToPath } from "node:url"; +import { NodeIO, type Document } from "@gltf-transform/core"; +import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; +import { MeshoptDecoder } from "meshoptimizer"; +import { beforeAll, describe, expect, it } from "vitest"; +import { CHARACTER_PART_CATALOG } from "./characterAppearance"; +import type { MemberId } from "./types"; + +const BODY_ASSETS = ["druid", "mage", "ranger", "knight", "rogue"] as const; +const REQUIRED_SHARED_CLIPS = ["Death_A", "Hit_A", "Idle", "Running_A", "Walking_A", "Spellcasting"]; +const REQUIRED_RIG_NODES = ["root", "hips", "spine", "chest", "head", "handslot.r", "handslot.l"]; +const ASSET_BY_MEMBER: Record = { + aelia: "druid", + brann: "knight", + nia: "ranger", + orin: "mage", + vale: "rogue", +}; + +const io = new NodeIO() + .registerExtensions(ALL_EXTENSIONS) + .registerDependencies({ "meshopt.decoder": MeshoptDecoder }); + +function assetPath(name: typeof BODY_ASSETS[number]) { + return fileURLToPath(new URL(`../assets/game/models/claudecraft/chars/players/${name}-uastc.glb`, import.meta.url)); +} + +describe("shared healer animation rig assets", () => { + const documents = new Map(); + + beforeAll(async () => { + await MeshoptDecoder.ready; + await Promise.all(BODY_ASSETS.map(async (name) => { + documents.set(name, await io.read(assetPath(name))); + })); + }); + + it("keeps the same named joint and socket contract on every healer body", () => { + const druidSkin = documents.get("druid")!.getRoot().listSkins()[0]; + const druidJoints = druidSkin.listJoints().map((joint) => joint.getName()).sort(); + const druidRestPose = new Map(druidSkin.listJoints().map((joint) => [joint.getName(), { + parent: joint.getParentNode()?.getName() ?? null, + translation: joint.getTranslation(), + rotation: joint.getRotation(), + scale: joint.getScale(), + }])); + + for (const name of BODY_ASSETS) { + const root = documents.get(name)!.getRoot(); + const nodeNames = new Set(root.listNodes().map((node) => node.getName())); + for (const requiredNode of REQUIRED_RIG_NODES) expect(nodeNames.has(requiredNode), `${name}:${requiredNode}`).toBe(true); + for (const skin of root.listSkins()) { + expect(skin.listJoints().map((joint) => joint.getName()).sort(), name).toEqual(druidJoints); + for (const joint of skin.listJoints()) { + expect({ + parent: joint.getParentNode()?.getName() ?? null, + translation: joint.getTranslation(), + rotation: joint.getRotation(), + scale: joint.getScale(), + }, `${name}:${joint.getName()}`).toEqual(druidRestPose.get(joint.getName())); + } + } + } + }); + + it("lets every body resolve all targets from Aelia's shared animation set", () => { + const druidRoot = documents.get("druid")!.getRoot(); + const clipNames = new Set(druidRoot.listAnimations().map((animation) => animation.getName())); + for (const clipName of REQUIRED_SHARED_CLIPS) expect(clipNames.has(clipName), clipName).toBe(true); + + const animationTargets = new Set(druidRoot.listAnimations().flatMap((animation) => + animation.listChannels().map((channel) => channel.getTargetNode()?.getName()).filter((name): name is string => Boolean(name)), + )); + for (const name of BODY_ASSETS) { + const nodeNames = new Set(documents.get(name)!.getRoot().listNodes().map((node) => node.getName())); + const missingTargets = [...animationTargets].filter((target) => !nodeNames.has(target)); + expect(missingTargets, name).toEqual([]); + } + }); + + it("keeps every modular catalog node present in its source GLB", () => { + for (const [partId, definition] of Object.entries(CHARACTER_PART_CATALOG)) { + const assetName = ASSET_BY_MEMBER[definition.sourceMemberId]; + const nodes = documents.get(assetName)!.getRoot().listNodes(); + for (const nodeName of definition.nodeNames) { + const node = nodes.find((candidate) => candidate.getName() === nodeName); + expect(node, `${partId}:${nodeName}`).toBeDefined(); + expect(node?.getMesh(), `${partId}:${nodeName}:mesh`).not.toBeNull(); + expect(node?.getSkin(), `${partId}:${nodeName}:skin`).not.toBeNull(); + } + } + }); +}); diff --git a/src/game/healerVisuals.test.ts b/src/game/healerVisuals.test.ts new file mode 100644 index 0000000..46cad56 --- /dev/null +++ b/src/game/healerVisuals.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { HEALER_CLASS_ORDER } from "./healers"; +import { + createDefaultHealerAppearance, + HEALER_VISUAL_PROFILES, + healerVisualSignature, + normalizeHealerAppearance, +} from "./healerVisuals"; + +describe("healer visual profiles", () => { + it("covers every healer class", () => { + expect(Object.keys(HEALER_VISUAL_PROFILES)).toEqual(HEALER_CLASS_ORDER); + }); + + it("reuses Aelia's animation rig for every healer body", () => { + for (const profile of Object.values(HEALER_VISUAL_PROFILES)) { + expect(profile.animationMemberId).toBe("aelia"); + } + }); + + it("gives every healer a distinct silhouette and loadout signature", () => { + const signatures = Object.values(HEALER_VISUAL_PROFILES).map(healerVisualSignature); + expect(new Set(signatures).size).toBe(HEALER_CLASS_ORDER.length); + }); + + it("returns owned defaults and repairs unknown saved appearances per class", () => { + const first = createDefaultHealerAppearance("priest"); + const second = createDefaultHealerAppearance("priest"); + expect(first).toEqual(HEALER_VISUAL_PROFILES.priest.appearance); + expect(first).not.toBe(second); + expect(first.mainHand).not.toBe(second.mainHand); + expect(normalizeHealerAppearance("druid", { version: 99 })).toEqual( + HEALER_VISUAL_PROFILES.druid.appearance, + ); + }); +}); diff --git a/src/game/healerVisuals.ts b/src/game/healerVisuals.ts new file mode 100644 index 0000000..6f553e8 --- /dev/null +++ b/src/game/healerVisuals.ts @@ -0,0 +1,148 @@ +import type { HealerClassId, MemberId } from "./types"; +import { + cloneCharacterAppearance, + normalizeCharacterAppearance, + type CharacterAppearanceV1, + type CharacterHeldItemVisual, + type CharacterWeaponGrip, + type CharacterWeaponModelId, +} from "./characterAppearance"; + +export type HealerWeaponModelId = CharacterWeaponModelId; +export type HealerWeaponGrip = CharacterWeaponGrip; +export type HealerAccessoryId = "sun-halo" | "grove-antlers" | "storm-totem" | "sun-crest" | "clockwork-rings"; + +export type HealerHeldItemVisual = CharacterHeldItemVisual; + +export interface HealerVisualProfile { + /** Geometry source. Every source uses the shared Rig_Medium bone contract. */ + bodyMemberId: MemberId; + /** Aelia's Druid file remains the single animation source for every healer. */ + animationMemberId: "aelia"; + appearance: CharacterAppearanceV1; + hiddenNodes: readonly string[]; + accessory: HealerAccessoryId; + accentColor: string; + secondaryColor: string; +} + +export const HEALER_VISUAL_PROFILES: Record = { + priest: { + bodyMemberId: "orin", + animationMemberId: "aelia", + appearance: { + version: 1, + rigId: "medium", + scaleSourceMemberId: "orin", + headPartId: "mage-head", + upperBodyPartId: "mage-upper", + lowerBodyPartId: "mage-lower", + headwearPartId: null, + backPartId: "mage-cape", + mainHand: { modelId: "cc/adv_druid_staff", grip: "staff" }, + }, + hiddenNodes: ["Mage_Hat"], + accessory: "sun-halo", + accentColor: "#ffe7a3", + secondaryColor: "#9a76ff", + }, + druid: { + bodyMemberId: "aelia", + animationMemberId: "aelia", + appearance: { + version: 1, + rigId: "medium", + scaleSourceMemberId: "aelia", + headPartId: "druid-head", + upperBodyPartId: "druid-upper", + lowerBodyPartId: "druid-lower", + headwearPartId: null, + backPartId: "druid-backpack", + mainHand: { modelId: "cc/adv_druid_staff", grip: "staff" }, + }, + hiddenNodes: [], + accessory: "grove-antlers", + accentColor: "#79d66f", + secondaryColor: "#e8b86a", + }, + shaman: { + bodyMemberId: "nia", + animationMemberId: "aelia", + appearance: { + version: 1, + rigId: "medium", + scaleSourceMemberId: "nia", + headPartId: "ranger-head", + upperBodyPartId: "ranger-upper", + lowerBodyPartId: "ranger-lower", + headwearPartId: null, + backPartId: "ranger-cape", + mainHand: { modelId: "cc/adv_druid_staff", grip: "staff" }, + }, + hiddenNodes: ["Ranger_Quiver"], + accessory: "storm-totem", + accentColor: "#62bdff", + secondaryColor: "#7de0bf", + }, + paladin: { + bodyMemberId: "brann", + animationMemberId: "aelia", + appearance: { + version: 1, + rigId: "medium", + scaleSourceMemberId: "brann", + headPartId: "knight-head", + upperBodyPartId: "knight-upper", + lowerBodyPartId: "knight-lower", + headwearPartId: "knight-helmet", + backPartId: "knight-cape", + mainHand: { modelId: "cc/adv_sword_1handed", grip: "upright" }, + offHand: { modelId: "cc/shield_badge", grip: "prop" }, + }, + hiddenNodes: [], + accessory: "sun-crest", + accentColor: "#ffc45c", + secondaryColor: "#fff3bd", + }, + chronomancer: { + bodyMemberId: "vale", + animationMemberId: "aelia", + appearance: { + version: 1, + rigId: "medium", + scaleSourceMemberId: "vale", + headPartId: "rogue-head", + upperBodyPartId: "rogue-upper", + lowerBodyPartId: "rogue-lower", + headwearPartId: null, + backPartId: "rogue-cape", + mainHand: { modelId: "cc/adv_wand", grip: "wand" }, + offHand: { modelId: "cc/spellbook_open", grip: "prop" }, + }, + hiddenNodes: [], + accessory: "clockwork-rings", + accentColor: "#62d4d9", + secondaryColor: "#8877ff", + }, +}; + +export function createDefaultHealerAppearance(classId: HealerClassId): CharacterAppearanceV1 { + return cloneCharacterAppearance(HEALER_VISUAL_PROFILES[classId].appearance); +} + +export function normalizeHealerAppearance(classId: HealerClassId, value: unknown): CharacterAppearanceV1 { + return normalizeCharacterAppearance(value, HEALER_VISUAL_PROFILES[classId].appearance); +} + +export function healerVisualSignature(profile: HealerVisualProfile) { + return [ + profile.appearance.headPartId, + profile.appearance.upperBodyPartId, + profile.appearance.lowerBodyPartId, + profile.appearance.headwearPartId ?? "bare", + profile.appearance.backPartId ?? "no-back", + profile.accessory, + profile.appearance.mainHand.modelId, + profile.appearance.offHand?.modelId ?? "empty", + ].join(":"); +} diff --git a/src/game/healers.ts b/src/game/healers.ts index 4a4a3cb..188fdd7 100644 --- a/src/game/healers.ts +++ b/src/game/healers.ts @@ -1,20 +1,39 @@ -import type { AbilityDefinition, AbilityId, HealerClassDefinition, HealerClassId, InventoryItem } from "./types"; +import type { + AbilityDefinition, + AbilityLoadout, + AbilitySlotId, + HealerAbilityId, + HealerClassDefinition, + HealerClassId, + HealerPulseKind, + InventoryItem, +} from "./types"; import { ABILITY_CONTROLLER_BINDINGS } from "./controllerBindings"; -const bindings: Record> = { - mend: { id: "mend", key: "1", gamepad: ABILITY_CONTROLLER_BINDINGS.mend.glyph, targeting: "ally" }, - renew: { id: "renew", key: "2", gamepad: ABILITY_CONTROLLER_BINDINGS.renew.glyph, targeting: "ally" }, - shield: { id: "shield", key: "3", gamepad: ABILITY_CONTROLLER_BINDINGS.shield.glyph, targeting: "ally" }, - purify: { id: "purify", key: "4", gamepad: ABILITY_CONTROLLER_BINDINGS.purify.glyph, targeting: "ally" }, - radiance: { id: "radiance", key: "5", gamepad: ABILITY_CONTROLLER_BINDINGS.radiance.glyph, targeting: "party" }, - barrier: { id: "barrier", key: "6", gamepad: ABILITY_CONTROLLER_BINDINGS.barrier.glyph, targeting: "party" }, +const slotBindings: Record> = { + ability1: { slot: "ability1", key: "1", gamepad: ABILITY_CONTROLLER_BINDINGS.ability1.glyph }, + ability2: { slot: "ability2", key: "2", gamepad: ABILITY_CONTROLLER_BINDINGS.ability2.glyph }, + ability3: { slot: "ability3", key: "3", gamepad: ABILITY_CONTROLLER_BINDINGS.ability3.glyph }, + ability4: { slot: "ability4", key: "4", gamepad: ABILITY_CONTROLLER_BINDINGS.ability4.glyph }, + ability5: { slot: "ability5", key: "5", gamepad: ABILITY_CONTROLLER_BINDINGS.ability5.glyph }, + ability6: { slot: "ability6", key: "6", gamepad: ABILITY_CONTROLLER_BINDINGS.ability6.glyph }, }; -function ability(id: AbilityId, definition: Omit): AbilityDefinition { - return { ...bindings[id], ...definition }; +function ability( + slot: AbilitySlotId, + id: HealerAbilityId, + pulseKind: HealerPulseKind, + targeting: AbilityDefinition["targeting"], + definition: Omit, +): AbilityDefinition { + return { ...slotBindings[slot], id, pulseKind, targeting, ...definition }; } -export const HEALER_CLASS_ORDER: HealerClassId[] = ["priest", "druid", "shaman"]; +export const HEALER_CLASS_ORDER: HealerClassId[] = ["priest", "druid", "shaman", "paladin", "chronomancer"]; + +export function isHealerClassId(value: string): value is HealerClassId { + return HEALER_CLASS_ORDER.includes(value as HealerClassId); +} export const HEALER_CLASSES: Record = { priest: { @@ -26,12 +45,12 @@ export const HEALER_CLASSES: Record = { resourceName: "Grace", description: "Direct healing, protective shields, cleansing, and a damage-reducing sanctuary.", abilities: { - mend: ability("mend", { name: "Mend", shortName: "Mend", cooldown: 0, castTime: 0.5, mana: 5, icon: "+", description: "Cast for 0.5 seconds to heal the selected ally for 38 health. No cooldown.", color: "#fff0bd" }), - renew: ability("renew", { name: "Renew", shortName: "Renew", cooldown: 0, mana: 7, icon: "✣", description: "Heal selected ally for 7 health every second for 8 seconds. No cooldown.", color: "#71df9c" }), - shield: ability("shield", { name: "Aegis Shield", shortName: "Shield", cooldown: 10, mana: 8, icon: "◇", description: "Give selected ally a 36-point damage shield.", color: "#6fc6ff" }), - purify: ability("purify", { name: "Purify", shortName: "Purify", cooldown: 3, mana: 5, icon: "✧", description: "Dispel all harmful magic from the selected ally.", color: "#b58cff" }), - radiance: ability("radiance", { name: "Radiance", shortName: "Radiance", cooldown: 14, mana: 12, icon: "☀", description: "Heal every party member for 22 health.", color: "#ffd66b" }), - barrier: ability("barrier", { name: "Barrier", shortName: "Barrier", cooldown: 60, mana: 10, icon: "◉", description: "Place a 3m field at your feet for 8 seconds. Allies inside take 30% less damage.", color: "#f2cf55" }), + ability1: ability("ability1", "priest-mend", "direct-heal", "ally", { name: "Mend", shortName: "Mend", cooldown: 0, castTime: 0.5, mana: 5, icon: "+", description: "Cast for 0.5 seconds to heal the selected ally for 38 health. No cooldown.", color: "#fff0bd" }), + ability2: ability("ability2", "priest-renew", "periodic-heal", "ally", { name: "Renew", shortName: "Renew", cooldown: 0, mana: 7, icon: "✣", description: "Heal selected ally for 7 health every second for 8 seconds. No cooldown.", color: "#71df9c" }), + ability3: ability("ability3", "priest-aegis-shield", "protective", "ally", { name: "Aegis Shield", shortName: "Shield", cooldown: 10, mana: 8, icon: "◇", description: "Give selected ally a 36-point damage shield.", color: "#6fc6ff" }), + ability4: ability("ability4", "priest-purify", "cleanse", "ally", { name: "Purify", shortName: "Purify", cooldown: 3, mana: 5, icon: "✧", description: "Dispel all harmful magic from the selected ally.", color: "#b58cff" }), + ability5: ability("ability5", "priest-radiance", "group-heal", "party", { name: "Radiance", shortName: "Radiance", cooldown: 14, mana: 12, icon: "☀", description: "Heal every party member for 22 health.", color: "#ffd66b" }), + ability6: ability("ability6", "priest-barrier", "field", "party", { name: "Barrier", shortName: "Barrier", cooldown: 60, mana: 10, icon: "◉", description: "Place a 3m field at your feet for 8 seconds. Allies inside take 30% less damage.", color: "#f2cf55" }), }, }, druid: { @@ -41,14 +60,15 @@ export const HEALER_CLASSES: Record = { icon: "❧", color: "#79d36f", resourceName: "Mana", - description: "Placeholder nature kit built around regeneration, bark wards, and restorative growth.", + secondaryResourceName: "Verdancy", + description: "Proactive regeneration, layered growth effects, and well-timed blooms.", abilities: { - mend: ability("mend", { name: "Healing Touch", shortName: "Heal Touch", cooldown: 0, castTime: 0.5, mana: 5, icon: "❦", description: "Placeholder: cast a focused nature heal for 38 health.", color: "#b8ef8b" }), - renew: ability("renew", { name: "Rejuvenation", shortName: "Rejuvenate", cooldown: 0, mana: 7, icon: "☘", description: "Placeholder: restore 7 health each second for 8 seconds.", color: "#63d77e" }), - shield: ability("shield", { name: "Ironbark", shortName: "Ironbark", cooldown: 10, mana: 8, icon: "♧", description: "Placeholder: grant the selected ally 36 absorption.", color: "#a6c76b" }), - purify: ability("purify", { name: "Nature's Cure", shortName: "Nature Cure", cooldown: 3, mana: 5, icon: "✤", description: "Placeholder: dispel all harmful magic from the selected ally.", color: "#8de2b2" }), - radiance: ability("radiance", { name: "Wild Growth", shortName: "Wild Growth", cooldown: 14, mana: 12, icon: "✾", description: "Placeholder: heal every party member for 22 health.", color: "#d1ed73" }), - barrier: ability("barrier", { name: "Grove Ward", shortName: "Grove Ward", cooldown: 60, mana: 10, icon: "◌", description: "Placeholder: grow an 8-second protective grove that reduces damage by 30%.", color: "#70bc72" }), + ability1: ability("ability1", "druid-regrowth", "direct-heal", "ally", { name: "Regrowth", shortName: "Regrowth", cooldown: 0, castTime: 0.5, mana: 5, icon: "❦", description: "Heal for 24, then 4 each second for 6 seconds. Consumes up to 3 Verdancy for 7 more healing each.", color: "#b8ef8b" }), + ability2: ability("ability2", "druid-rejuvenation", "periodic-heal", "ally", { name: "Rejuvenation", shortName: "Rejuvenate", cooldown: 0, mana: 6, icon: "☘", description: "Restore 6 health each second for 8 seconds. Each tick generates Verdancy.", color: "#63d77e" }), + ability3: ability("ability3", "druid-lifebloom", "periodic-heal", "ally", { name: "Lifebloom", shortName: "Lifebloom", cooldown: 0, mana: 5, icon: "♧", description: "Stack up to 3 times for 7 seconds. Heals over time, then blooms for 10 per stack. At 3 stacks, spend 2 Verdancy to bloom now.", color: "#a6c76b" }), + ability4: ability("ability4", "druid-natures-cure", "cleanse", "ally", { name: "Nature's Cure", shortName: "Nature Cure", cooldown: 3, mana: 5, icon: "✤", description: "Dispel all harmful magic and immediately trigger every healing-over-time effect on the target.", color: "#8de2b2" }), + ability5: ability("ability5", "druid-wild-growth", "group-heal", "party", { name: "Wild Growth", shortName: "Wild Growth", cooldown: 14, mana: 12, icon: "✾", description: "Heal the three most injured allies for 5 health each second for 6 seconds.", color: "#d1ed73" }), + ability6: ability("ability6", "druid-flourish", "field", "party", { name: "Flourish", shortName: "Flourish", cooldown: 60, mana: 10, icon: "◌", description: "Extend active Druid healing effects by 5 seconds and make them tick twice as fast for 6 seconds.", color: "#70bc72" }), }, }, shaman: { @@ -58,18 +78,86 @@ export const HEALER_CLASSES: Record = { icon: "ϟ", color: "#65b9ed", resourceName: "Mana", - description: "Placeholder elemental kit using tides, earth wards, cleansing, and spirit protection.", + secondaryResourceName: "Tidal Surge", + description: "Reactive burst healing through chain positioning, earth wards, and spirit links.", abilities: { - mend: ability("mend", { name: "Healing Wave", shortName: "Heal Wave", cooldown: 0, castTime: 0.5, mana: 5, icon: "≈", description: "Placeholder: cast a focused water heal for 38 health.", color: "#8fdcf2" }), - renew: ability("renew", { name: "Riptide", shortName: "Riptide", cooldown: 0, mana: 7, icon: "≋", description: "Placeholder: restore 7 health each second for 8 seconds.", color: "#54c9c5" }), - shield: ability("shield", { name: "Earth Shield", shortName: "Earth Shield", cooldown: 10, mana: 8, icon: "⬡", description: "Placeholder: grant the selected ally 36 absorption.", color: "#d2b66c" }), - purify: ability("purify", { name: "Cleanse Spirit", shortName: "Cleanse", cooldown: 3, mana: 5, icon: "✧", description: "Placeholder: dispel all harmful magic from the selected ally.", color: "#9aaef5" }), - radiance: ability("radiance", { name: "Chain Heal", shortName: "Chain Heal", cooldown: 14, mana: 12, icon: "⌁", description: "Placeholder: heal every party member for 22 health.", color: "#6ee2db" }), - barrier: ability("barrier", { name: "Spirit Link", shortName: "Spirit Link", cooldown: 60, mana: 10, icon: "◎", description: "Placeholder: place an 8-second spirit field that reduces damage by 30%.", color: "#9d8cf2" }), + ability1: ability("ability1", "shaman-healing-wave", "direct-heal", "ally", { name: "Healing Wave", shortName: "Heal Wave", cooldown: 0, castTime: 0.65, mana: 5, icon: "≈", description: "Heal for 34, increased by 35% below half health. Tidal Surge makes the cast faster and adds 10 healing.", color: "#8fdcf2" }), + ability2: ability("ability2", "shaman-riptide", "periodic-heal", "ally", { name: "Riptide", shortName: "Riptide", cooldown: 6, mana: 6, icon: "≋", description: "Heal for 12, then 5 each second for 6 seconds. Marks a Chain Heal anchor and grants Tidal Surge.", color: "#54c9c5" }), + ability3: ability("ability3", "shaman-earth-shield", "protective", "ally", { name: "Earth Shield", shortName: "Earth Shield", cooldown: 10, mana: 8, icon: "⬡", description: "Give an ally 6 charges for 30 seconds. Taking damage consumes a charge to heal for 9 and grants Tidal Surge.", color: "#d2b66c" }), + ability4: ability("ability4", "shaman-cleanse-spirit", "cleanse", "ally", { name: "Cleanse Spirit", shortName: "Cleanse", cooldown: 3, mana: 5, icon: "✧", description: "Dispel all harmful magic from an ally. A successful cleanse grants Tidal Surge.", color: "#9aaef5" }), + ability5: ability("ability5", "shaman-chain-heal", "group-heal", "ally", { name: "Chain Heal", shortName: "Chain Heal", cooldown: 12, mana: 12, icon: "⌁", description: "Heal the target, then jump through nearby injured allies with diminishing power. Tidal Surge adds jumps; Riptide strengthens the first heal.", color: "#6ee2db" }), + ability6: ability("ability6", "shaman-spirit-link", "field", "party", { name: "Spirit Link Totem", shortName: "Spirit Link", cooldown: 60, mana: 10, icon: "◎", description: "Place an 8-second, 3m spirit field that equalizes nearby allies' health percentages each second.", color: "#9d8cf2" }), + }, + }, + paladin: { + id: "paladin", + name: "Paladin", + specialization: "Dawnforged Paladin", + icon: "☼", + color: "#f2a65a", + resourceName: "Mana", + secondaryResourceName: "Conviction", + description: "Fight beside the party, turn offense into healing, and funnel light through a chosen beacon.", + abilities: { + ability1: ability("ability1", "paladin-holy-light", "direct-heal", "ally", { name: "Holy Light", shortName: "Holy Light", cooldown: 0, castTime: 0.55, mana: 5, icon: "✚", description: "Cast for 0.55 seconds to heal an ally for 34. Beacon of Light echoes 40% to its marked ally.", color: "#ffe29a" }), + ability2: ability("ability2", "paladin-crusader-strike", "direct-heal", "enemy", { name: "Crusader Strike", shortName: "Crusader", cooldown: 4, mana: 3, icon: "⚔", description: "Strike the boss for 18 damage, heal the most injured ally for 18, and gain 1 Conviction.", color: "#f6b35f" }), + ability3: ability("ability3", "paladin-beacon-of-light", "protective", "ally", { name: "Beacon of Light", shortName: "Beacon", cooldown: 8, mana: 7, icon: "♢", description: "Mark an ally for 30 seconds. Your single-target Paladin heals on others echo to the beacon for 40%.", color: "#ffd86b" }), + ability4: ability("ability4", "paladin-cleanse-light", "cleanse", "ally", { name: "Cleanse Light", shortName: "Cleanse", cooldown: 3, mana: 5, icon: "✧", description: "Dispel all harmful magic from the selected ally and heal them for 10.", color: "#fff0bd" }), + ability5: ability("ability5", "paladin-word-of-glory", "group-heal", "ally", { name: "Word of Glory", shortName: "Word Glory", cooldown: 0, mana: 6, icon: "✹", description: "Spend all Conviction to heal an ally for 18 plus 14 per point. At 3 Conviction, also heal the party for 8.", color: "#ffc857" }), + ability6: ability("ability6", "paladin-avenging-crusader", "field", "party", { name: "Avenging Crusader", shortName: "Avenging", cooldown: 60, mana: 10, icon: "⚜", description: "For 10 seconds, 20% of party attack damage heals the most injured living ally.", color: "#ff9f43" }), + }, + }, + chronomancer: { + id: "chronomancer", + name: "Chronomancer", + specialization: "Continuum Chronomancer", + icon: "◷", + color: "#62d4d9", + resourceName: "Mana", + secondaryResourceName: "Chronoshards", + description: "Prepare health snapshots, schedule healing echoes, and reverse damage after it happens.", + abilities: { + ability1: ability("ability1", "chronomancer-mend-timeline", "direct-heal", "ally", { name: "Mend Timeline", shortName: "Mend Time", cooldown: 0, castTime: 0.5, mana: 5, icon: "⌛", description: "Cast for 0.5 seconds to heal the selected ally for 30.", color: "#b5f4ef" }), + ability2: ability("ability2", "chronomancer-time-anchor", "protective", "ally", { name: "Time Anchor", shortName: "Anchor", cooldown: 0, mana: 4, icon: "⌖", description: "Record an ally's health for 6 seconds. Recast on that ally to restore damage taken since and gain 1 Chronoshard.", color: "#6ce5df" }), + ability3: ability("ability3", "chronomancer-echo-of-tomorrow", "periodic-heal", "ally", { name: "Echo of Tomorrow", shortName: "Tomorrow", cooldown: 6, mana: 7, icon: "◌", description: "Heal an ally for 10 now, then echo 28 healing after 3 seconds.", color: "#84cfff" }), + ability4: ability("ability4", "chronomancer-erase-affliction", "cleanse", "ally", { name: "Erase Affliction", shortName: "Erase", cooldown: 3, mana: 5, icon: "⌫", description: "Dispel all harmful magic and gain 1 Chronoshard.", color: "#a99cff" }), + ability5: ability("ability5", "chronomancer-accelerate", "group-heal", "party", { name: "Accelerate", shortName: "Accelerate", cooldown: 14, mana: 11, icon: "≫", description: "Spend all Chronoshards. Heal the party for 10 plus 8 per shard and reduce active cooldowns by 1 second per shard.", color: "#52c7e8" }), + ability6: ability("ability6", "chronomancer-time-loop", "field", "party", { name: "Time Loop", shortName: "Time Loop", cooldown: 60, mana: 10, icon: "∞", description: "Record every living ally's health. After 6 seconds, restore any health lost since the loop began.", color: "#7d8cff" }), }, }, }; +/** + * Flat spell registry used by run modes that compose abilities across healer + * classes. Class screens remain projections of the same canonical definitions. + */ +const HEALER_ABILITY_ENTRIES = HEALER_CLASS_ORDER.flatMap((classId) => + Object.values(HEALER_CLASSES[classId].abilities).map((entry) => [entry.id, entry] as const), +); + +/** Canonical deterministic order for every registered healer ability. */ +export const HEALER_ABILITY_IDS: readonly HealerAbilityId[] = Object.freeze( + HEALER_ABILITY_ENTRIES.map(([abilityId]) => abilityId), +); + +export const HEALER_ABILITIES = Object.fromEntries(HEALER_ABILITY_ENTRIES) as Record; + +export const DEFAULT_ABILITY_LOADOUTS = Object.fromEntries( + HEALER_CLASS_ORDER.map((classId) => [ + classId, + Object.fromEntries(Object.entries(HEALER_CLASSES[classId].abilities).map(([slotId, entry]) => [slotId, entry.id])), + ]), +) as Record>; + +export function createClassAbilityLoadout(classId: HealerClassId): Record { + return { ...DEFAULT_ABILITY_LOADOUTS[classId] }; +} + +export function resolveSlottedAbility(loadout: AbilityLoadout, slotId: AbilitySlotId): AbilityDefinition | undefined { + const abilityId = loadout[slotId]; + return abilityId ? HEALER_ABILITIES[abilityId] : undefined; +} + const CLASS_INVENTORIES: Record = { priest: [ { id: "priest-censer", name: "Censer of First Light", slot: "Main Hand", rarity: "Rare", icon: "♰", stats: ["+12 Grace", "+8% Mend healing"], effect: "Mend restores 2 mana when it lands on an ally below 50% health.", equipped: true }, @@ -78,14 +166,24 @@ const CLASS_INVENTORIES: Record = { { id: "priest-sigil", name: "Sigil of Quiet Resolve", slot: "Trinket", rarity: "Rare", icon: "◈", stats: ["+10% Purify range", "+5 Haste"], effect: "Purify grants its target 8 absorption when it removes Ember Brand.", equipped: false }, ], druid: [ - { id: "druid-branch", name: "Verdant Branch", slot: "Main Hand", rarity: "Common", icon: "❧", stats: ["+5 Spirit"], effect: "Placeholder Druid starter weapon.", equipped: true }, - { id: "druid-hide", name: "Mossbound Hide", slot: "Chest", rarity: "Common", icon: "♧", stats: ["+10 Armor"], effect: "Placeholder Druid starter armor.", equipped: true }, - { id: "druid-seed", name: "Dreamseed", slot: "Trinket", rarity: "Uncommon", icon: "•", stats: ["+3 Haste"], effect: "Placeholder Druid trinket.", equipped: false }, + { id: "druid-branch", name: "Verdant Branch", slot: "Main Hand", rarity: "Common", icon: "❧", stats: ["+5 Spirit"], effect: "A living focus shaped for layered restoration magic.", equipped: true }, + { id: "druid-hide", name: "Mossbound Hide", slot: "Chest", rarity: "Common", icon: "♧", stats: ["+10 Armor"], effect: "Flexible living armor that protects without slowing spellwork.", equipped: true }, + { id: "druid-seed", name: "Dreamseed", slot: "Trinket", rarity: "Uncommon", icon: "•", stats: ["+3 Haste"], effect: "A dormant seed that stirs near active growth magic.", equipped: false }, ], shaman: [ - { id: "shaman-totem", name: "Raincall Totem", slot: "Main Hand", rarity: "Common", icon: "ϟ", stats: ["+5 Spirit"], effect: "Placeholder Shaman starter focus.", equipped: true }, - { id: "shaman-mail", name: "Tideworn Mail", slot: "Chest", rarity: "Common", icon: "▧", stats: ["+12 Armor"], effect: "Placeholder Shaman starter armor.", equipped: true }, - { id: "shaman-stone", name: "Whispering Stone", slot: "Trinket", rarity: "Uncommon", icon: "◇", stats: ["+3 Haste"], effect: "Placeholder Shaman trinket.", equipped: false }, + { id: "shaman-totem", name: "Raincall Totem", slot: "Main Hand", rarity: "Common", icon: "ϟ", stats: ["+5 Spirit"], effect: "A compact focus carrying water and earth sigils.", equipped: true }, + { id: "shaman-mail", name: "Tideworn Mail", slot: "Chest", rarity: "Common", icon: "▧", stats: ["+12 Armor"], effect: "Salt-tempered mail built for close formation healing.", equipped: true }, + { id: "shaman-stone", name: "Whispering Stone", slot: "Trinket", rarity: "Uncommon", icon: "◇", stats: ["+3 Haste"], effect: "An ancestral stone that hums beside active totems.", equipped: false }, + ], + paladin: [ + { id: "paladin-mace", name: "Sunward Mace", slot: "Main Hand", rarity: "Common", icon: "⚒", stats: ["+5 Spirit"], effect: "A balanced war-mace that channels each impact into restorative light.", equipped: true }, + { id: "paladin-plate", name: "Dawnforged Plate", slot: "Chest", rarity: "Common", icon: "▣", stats: ["+14 Armor"], effect: "Warm plate built for fighting close to the party.", equipped: true }, + { id: "paladin-reliquary", name: "Reliquary of Oaths", slot: "Trinket", rarity: "Uncommon", icon: "♢", stats: ["+3 Haste"], effect: "A tiny reliquary that brightens as Conviction grows.", equipped: false }, + ], + chronomancer: [ + { id: "chronomancer-focus", name: "Continuum Focus", slot: "Main Hand", rarity: "Common", icon: "◷", stats: ["+5 Spirit"], effect: "A clockwork focus calibrated to preserve living timelines.", equipped: true }, + { id: "chronomancer-mantle", name: "Secondhand Mantle", slot: "Chest", rarity: "Common", icon: "⌛", stats: ["+10 Armor"], effect: "Threaded with moments recovered from failed futures.", equipped: true }, + { id: "chronomancer-hourglass", name: "Unspent Hourglass", slot: "Trinket", rarity: "Uncommon", icon: "∞", stats: ["+3 Haste"], effect: "Its sand falls upward near a damaged timeline.", equipped: false }, ], }; diff --git a/src/game/hockeyHealing.test.ts b/src/game/hockeyHealing.test.ts new file mode 100644 index 0000000..b283ed0 --- /dev/null +++ b/src/game/hockeyHealing.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from "vitest"; +import { + HOCKEY_GOAL_HALF_WIDTH, + HOCKEY_HEALER_GOAL_Z, + HOCKEY_NPC_PADDLE_Z, + advanceHockeyHealing, + createHockeyHealingState, + hockeyAimPreviewVisible, + hockeyPaddleReturnDirection, + hockeyPuckSpeed, + hockeyReturnDirection, +} from "./hockeyHealing"; + +describe("Hockey Healing", () => { + it("starts rallies at the faster default puck speed", () => { + expect(hockeyPuckSpeed(0)).toBe(6.2); + expect(Math.hypot(...createHockeyHealingState(true).puckVelocity)).toBeCloseTo(6.2); + }); + + it("uses a wide goal and keeps return aim facing the NPC half", () => { + expect(HOCKEY_GOAL_HALF_WIDTH).toBe(8); + expect(hockeyReturnDirection([0, 0])).toEqual([0, -1]); + expect(hockeyReturnDirection([-1, 0])[0]).toBeLessThan(0); + expect(hockeyReturnDirection([1, 0])[0]).toBeGreaterThan(0); + expect(hockeyReturnDirection([0, 1])[1]).toBeLessThan(0); + }); + + it("moves the enemy Pong paddle toward the puck with bounded speed", () => { + const state = createHockeyHealingState(true); + state.puckPosition = [7, 0]; + state.puckVelocity = [0, -5.2]; + + const next = advanceHockeyHealing(state, { delta: 0.1, time: 1, playerPosition: [0, 8.5] }); + + expect(next.paddleX).toBeGreaterThan(0); + expect(next.paddleX).toBeCloseTo(1.2, 8); + }); + + it("returns an incoming puck using current left-stick aim", () => { + const state = createHockeyHealingState(true); + state.puckPosition = [0, 7.8]; + state.puckVelocity = [0, 5.2]; + state.aimDirection = [1, -0.5]; + + const next = advanceHockeyHealing(state, { delta: 0.2, time: 3, playerPosition: [0, 8.5] }); + + expect(next.returns).toBe(1); + expect(next.puckVelocity[0]).toBeGreaterThan(0); + expect(next.puckVelocity[1]).toBeLessThan(0); + expect(next.lastReturnAt).toBe(3); + }); + + it("ends run when puck enters healer goal", () => { + const state = createHockeyHealingState(true); + state.puckPosition = [0, HOCKEY_HEALER_GOAL_Z - 0.2]; + state.puckVelocity = [0, 5.2]; + + const next = advanceHockeyHealing(state, { delta: 0.1, time: 9, playerPosition: [7, 8] }); + + expect(next.status).toBe("lost"); + expect(next.lostAt).toBe(9); + expect(next.puckVelocity).toEqual([0, 0]); + }); + + it("lets the moving enemy paddle visibly return the puck", () => { + const state = createHockeyHealingState(true); + state.puckPosition = [0, HOCKEY_NPC_PADDLE_Z + 1]; + state.puckVelocity = [0, -5.2]; + + const next = advanceHockeyHealing(state, { delta: 0.2, time: 4, playerPosition: [0, 8.5] }); + + expect(next.status).toBe("live"); + expect(next.npcGoals).toBe(0); + expect(next.paddleReturns).toBe(1); + expect(next.paddleHitAt).toBe(4); + expect(next.serveIndex).toBe(1); + expect(next.puckVelocity[1]).toBeGreaterThan(0); + }); + + it("uses Pong contact position to set rebound angle", () => { + expect(hockeyPaddleReturnDirection(-2)[0]).toBeLessThan(0); + expect(hockeyPaddleReturnDirection(0)).toEqual([0, 1]); + expect(hockeyPaddleReturnDirection(2)[0]).toBeGreaterThan(0); + }); + + it("shows aim preview only for nearby incoming puck", () => { + const state = createHockeyHealingState(true); + state.puckPosition = [0, 2]; + state.puckVelocity = [0, 5.2]; + expect(hockeyAimPreviewVisible(state, [0, 8.5])).toBe(true); + state.puckPosition = [0, -10]; + expect(hockeyAimPreviewVisible(state, [0, 8.5])).toBe(false); + state.puckVelocity = [0, -5.2]; + expect(hockeyAimPreviewVisible(state, [0, 8.5])).toBe(false); + }); +}); diff --git a/src/game/hockeyHealing.ts b/src/game/hockeyHealing.ts new file mode 100644 index 0000000..6d5f0ed --- /dev/null +++ b/src/game/hockeyHealing.ts @@ -0,0 +1,252 @@ +import type { WorldPosition } from "./types"; + +export type HockeyHealingStatus = "inactive" | "live" | "lost"; + +export interface HockeyHealingState { + status: HockeyHealingStatus; + puckPosition: WorldPosition; + puckVelocity: WorldPosition; + aimDirection: WorldPosition; + returns: number; + paddleReturns: number; + paddleX: number; + paddleHitAt: number; + npcGoals: number; + serveIndex: number; + lastReturnAt: number; + lostAt: number | null; +} + +export interface HockeyHealingStep { + delta: number; + time: number; + playerPosition: WorldPosition; +} + +export const HOCKEY_ARENA_MIN_X = -10; +export const HOCKEY_ARENA_MAX_X = 10; +export const HOCKEY_ARENA_MIN_Z = -15; +export const HOCKEY_ARENA_MAX_Z = 13; +export const HOCKEY_ARENA_CENTER_Z = (HOCKEY_ARENA_MIN_Z + HOCKEY_ARENA_MAX_Z) * 0.5; +export const HOCKEY_ARENA_WIDTH = HOCKEY_ARENA_MAX_X - HOCKEY_ARENA_MIN_X; +export const HOCKEY_ARENA_LENGTH = HOCKEY_ARENA_MAX_Z - HOCKEY_ARENA_MIN_Z; +export const HOCKEY_MIDLINE_Z = -1; +export const HOCKEY_GOAL_HALF_WIDTH = 8; +export const HOCKEY_HEALER_GOAL_Z = 12.5; +export const HOCKEY_NPC_GOAL_Z = -14.5; +export const HOCKEY_NPC_PADDLE_Z = -13.35; +export const HOCKEY_NPC_PADDLE_WIDTH = 4.5; +export const HOCKEY_NPC_PADDLE_HALF_WIDTH = HOCKEY_NPC_PADDLE_WIDTH * 0.5; +export const HOCKEY_PUCK_RADIUS = 0.42; +export const HOCKEY_PLAYER_INTERCEPT_RADIUS = 1.05; +export const HOCKEY_AIM_PREVIEW_SECONDS = 1.5; + +const STARTING_SPEED = 6.2; +const MAX_SPEED = 10.7; +const MAX_SUBSTEPS = 8; +const MAX_SUBSTEP_DISTANCE = 0.34; +const AIM_DEAD_ZONE = 0.12; +const MIN_FORWARD_COMPONENT = 0.3; +const NPC_PADDLE_SPEED = 12; +const NPC_PADDLE_MAX_REBOUND_X = 0.76; +const INCOMING_LANES = [0, -0.62, 0.7, -0.28, 0.38, -0.82, 0.86, -0.48, 0.18] as const; +const PADDLE_CONTACT_OFFSETS = [0, -0.48, 0.56, -0.26, 0.34, -0.62, 0.64, -0.38, 0.2] as const; + +const HOCKEY_NPC_PADDLE_HIT_Z = HOCKEY_NPC_PADDLE_Z + HOCKEY_PUCK_RADIUS + 0.28; + +export const HOCKEY_DEFAULT_PLAYER_POSITION: WorldPosition = [0, 8.5]; + +export function hockeyPuckSpeed(returns: number) { + return Math.min(MAX_SPEED, STARTING_SPEED + Math.max(0, returns) * 0.18); +} + +function velocityToward(start: WorldPosition, target: WorldPosition, speed: number): WorldPosition { + const dx = target[0] - start[0]; + const dz = target[1] - start[1]; + const length = Math.max(0.0001, Math.hypot(dx, dz)); + return [dx / length * speed, dz / length * speed]; +} + +function incomingVelocity(position: WorldPosition, serveIndex: number, returns: number) { + const lane = INCOMING_LANES[serveIndex % INCOMING_LANES.length]; + return velocityToward( + position, + [lane * HOCKEY_GOAL_HALF_WIDTH, HOCKEY_HEALER_GOAL_Z], + hockeyPuckSpeed(returns), + ); +} + +export function createHockeyHealingState(active = false): HockeyHealingState { + const puckPosition: WorldPosition = [0, HOCKEY_NPC_PADDLE_HIT_Z + 0.08]; + return { + status: active ? "live" : "inactive", + puckPosition, + puckVelocity: active ? incomingVelocity(puckPosition, 0, 0) : [0, 0], + aimDirection: [0, -1], + returns: 0, + paddleReturns: 0, + paddleX: 0, + paddleHitAt: Number.NEGATIVE_INFINITY, + npcGoals: 0, + serveIndex: 0, + lastReturnAt: 0, + lostAt: null, + }; +} + +export function setHockeyAim(state: HockeyHealingState, aim: WorldPosition): HockeyHealingState { + const x = Number.isFinite(aim[0]) ? aim[0] : 0; + const z = Number.isFinite(aim[1]) ? aim[1] : 0; + if (state.aimDirection[0] === x && state.aimDirection[1] === z) return state; + return { ...state, aimDirection: [x, z] }; +} + +/** Maps world-space left-stick movement into opponent-facing puck travel. */ +export function hockeyReturnDirection(aim: WorldPosition): WorldPosition { + const magnitude = Math.hypot(aim[0], aim[1]); + if (magnitude < AIM_DEAD_ZONE) return [0, -1]; + const x = aim[0] / magnitude; + const forward = Math.max(MIN_FORWARD_COMPONENT, -aim[1] / magnitude); + const length = Math.hypot(x, forward); + return [x / length, -forward / length]; +} + +/** Pong-style rebound: edge hits travel wider while every return faces the healer goal. */ +export function hockeyPaddleReturnDirection(contactOffset: number): WorldPosition { + const normalizedOffset = Math.max(-1, Math.min(1, contactOffset / HOCKEY_NPC_PADDLE_HALF_WIDTH)); + const x = normalizedOffset * NPC_PADDLE_MAX_REBOUND_X; + return [x, Math.sqrt(Math.max(0.001, 1 - x * x))]; +} + +function moveToward(current: number, target: number, distance: number) { + if (Math.abs(target - current) <= distance) return target; + return current + Math.sign(target - current) * distance; +} + +function paddleTargetX(puckX: number, serveIndex: number, returningToPaddle: boolean) { + const contactOffset = returningToPaddle + ? PADDLE_CONTACT_OFFSETS[serveIndex % PADDLE_CONTACT_OFFSETS.length] * HOCKEY_NPC_PADDLE_HALF_WIDTH + : 0; + return Math.max( + HOCKEY_ARENA_MIN_X + HOCKEY_NPC_PADDLE_HALF_WIDTH, + Math.min(HOCKEY_ARENA_MAX_X - HOCKEY_NPC_PADDLE_HALF_WIDTH, puckX - contactOffset), + ); +} + +function segmentDistanceSquared( + startX: number, + startZ: number, + endX: number, + endZ: number, + point: WorldPosition, +) { + const dx = endX - startX; + const dz = endZ - startZ; + const lengthSquared = dx * dx + dz * dz; + const projection = lengthSquared < 0.000001 + ? 0 + : Math.max(0, Math.min(1, ((point[0] - startX) * dx + (point[1] - startZ) * dz) / lengthSquared)); + const nearestX = startX + dx * projection; + const nearestZ = startZ + dz * projection; + return (point[0] - nearestX) ** 2 + (point[1] - nearestZ) ** 2; +} + +export function hockeyAimPreviewVisible(state: HockeyHealingState, playerPosition: WorldPosition) { + if (state.status !== "live" || state.puckVelocity[1] <= 0) return false; + const distance = Math.hypot( + playerPosition[0] - state.puckPosition[0], + playerPosition[1] - state.puckPosition[1], + ); + return distance / Math.max(0.001, Math.hypot(state.puckVelocity[0], state.puckVelocity[1])) <= HOCKEY_AIM_PREVIEW_SECONDS; +} + +export function advanceHockeyHealing(source: HockeyHealingState, step: HockeyHealingStep): HockeyHealingState { + if (source.status !== "live" || step.delta <= 0) return source; + + const state: HockeyHealingState = { + ...source, + puckPosition: [...source.puckPosition], + puckVelocity: [...source.puckVelocity], + aimDirection: [...source.aimDirection], + }; + const speed = Math.hypot(state.puckVelocity[0], state.puckVelocity[1]); + const substeps = Math.max(1, Math.min(MAX_SUBSTEPS, Math.ceil(speed * step.delta / MAX_SUBSTEP_DISTANCE))); + const subDelta = step.delta / substeps; + const interceptRadiusSquared = (HOCKEY_PLAYER_INTERCEPT_RADIUS + HOCKEY_PUCK_RADIUS) ** 2; + + for (let substep = 0; substep < substeps && state.status === "live"; substep += 1) { + const startX = state.puckPosition[0]; + const startZ = state.puckPosition[1]; + let endX = startX + state.puckVelocity[0] * subDelta; + let endZ = startZ + state.puckVelocity[1] * subDelta; + + const minPuckX = HOCKEY_ARENA_MIN_X + HOCKEY_PUCK_RADIUS; + const maxPuckX = HOCKEY_ARENA_MAX_X - HOCKEY_PUCK_RADIUS; + if (endX < minPuckX || endX > maxPuckX) { + endX = Math.max(minPuckX, Math.min(maxPuckX, endX)); + state.puckVelocity[0] *= -1; + } + + const returningToPaddle = state.puckVelocity[1] < 0; + const paddleTarget = paddleTargetX(endX, state.serveIndex, returningToPaddle); + state.paddleX = moveToward(state.paddleX, paddleTarget, NPC_PADDLE_SPEED * subDelta); + + if (state.puckVelocity[1] > 0 && segmentDistanceSquared(startX, startZ, endX, endZ, step.playerPosition) <= interceptRadiusSquared) { + const direction = hockeyReturnDirection(state.aimDirection); + const nextReturns = state.returns + 1; + const returnSpeed = hockeyPuckSpeed(nextReturns); + state.puckPosition[0] = endX; + state.puckPosition[1] = Math.min(endZ, step.playerPosition[1]); + state.puckVelocity[0] = direction[0] * returnSpeed; + state.puckVelocity[1] = direction[1] * returnSpeed; + state.returns = nextReturns; + state.lastReturnAt = step.time; + continue; + } + + if (state.puckVelocity[1] > 0 && endZ >= HOCKEY_HEALER_GOAL_Z) { + state.puckPosition[0] = endX; + state.puckPosition[1] = HOCKEY_HEALER_GOAL_Z; + if (Math.abs(endX) <= HOCKEY_GOAL_HALF_WIDTH) { + state.status = "lost"; + state.puckVelocity = [0, 0]; + state.lostAt = step.time; + } else { + state.puckVelocity[1] = -Math.abs(state.puckVelocity[1]); + } + continue; + } + + if (state.puckVelocity[1] < 0 && startZ >= HOCKEY_NPC_PADDLE_HIT_Z && endZ <= HOCKEY_NPC_PADDLE_HIT_Z) { + const travelRatio = Math.max(0, Math.min(1, (HOCKEY_NPC_PADDLE_HIT_Z - startZ) / Math.min(-0.0001, endZ - startZ))); + const hitX = startX + (endX - startX) * travelRatio; + const contactOffset = hitX - state.paddleX; + if (Math.abs(contactOffset) <= HOCKEY_NPC_PADDLE_HALF_WIDTH + HOCKEY_PUCK_RADIUS) { + const direction = hockeyPaddleReturnDirection(contactOffset); + const returnSpeed = hockeyPuckSpeed(state.returns); + state.puckPosition = [hitX, HOCKEY_NPC_PADDLE_HIT_Z]; + state.puckVelocity = [direction[0] * returnSpeed, direction[1] * returnSpeed]; + state.paddleReturns += 1; + state.paddleHitAt = step.time; + state.serveIndex += 1; + continue; + } + } + + if (state.puckVelocity[1] < 0 && endZ <= HOCKEY_NPC_GOAL_Z) { + state.npcGoals += 1; + state.serveIndex += 1; + state.paddleX = paddleTargetX(endX, state.serveIndex, false); + state.puckPosition = [state.paddleX, HOCKEY_NPC_PADDLE_HIT_Z]; + state.puckVelocity = incomingVelocity(state.puckPosition, state.serveIndex, state.returns); + state.paddleReturns += 1; + state.paddleHitAt = step.time; + continue; + } + + state.puckPosition[0] = endX; + state.puckPosition[1] = endZ; + } + + return state; +} diff --git a/src/game/hockeyHealingPvp.test.ts b/src/game/hockeyHealingPvp.test.ts new file mode 100644 index 0000000..0823116 --- /dev/null +++ b/src/game/hockeyHealingPvp.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from "vitest"; +import { + HOCKEY_PVP_GOAL_DAMAGE, + HOCKEY_PVP_GOAL_Z, + advanceHockeyPvpPuck, + createHockeyPvpState, + hockeyPvpBossAt, + hockeyPvpDampeningPercent, + hockeyPvpHealingEffectiveness, + hockeyPvpPuckSpeed, + mirrorHockeyPvpPuck, +} from "./hockeyHealingPvp"; + +describe("Healing Hockey PVP", () => { + it("starts rallies at the faster default puck speed", () => { + const state = createHockeyPvpState({ matchId: null, seed: 7, opponentName: "CPU", role: "cpu" }); + + expect(hockeyPvpPuckSpeed(0)).toBe(7.2); + expect(Math.hypot(...state.puckVelocity)).toBeCloseTo(7.2); + }); + + it("adds five percent global dampening for every boss killed by either party", () => { + expect(hockeyPvpDampeningPercent(0, 0)).toBe(0); + expect(hockeyPvpDampeningPercent(1, 0)).toBe(5); + expect(hockeyPvpDampeningPercent(2, 3)).toBe(25); + expect(hockeyPvpHealingEffectiveness(2, 3)).toBe(0.75); + expect(hockeyPvpDampeningPercent(14, 9)).toBe(100); + expect(hockeyPvpHealingEffectiveness(14, 9)).toBe(0); + }); + + it("uses same deterministic boss order for both parties", () => { + const first = Array.from({ length: 20 }, (_, index) => hockeyPvpBossAt(4242, index)); + const second = Array.from({ length: 20 }, (_, index) => hockeyPvpBossAt(4242, index)); + expect(first).toEqual(second); + expect(first.every(Boolean)).toBe(true); + expect(new Set(first).size).toBeGreaterThan(1); + }); + + it("records a 45-damage goal signal and continues rally", () => { + const source = createHockeyPvpState({ matchId: null, seed: 7, opponentName: "CPU", role: "cpu" }); + source.puckPosition = [0, HOCKEY_PVP_GOAL_Z - 0.1]; + source.puckVelocity = [0, 8]; + const result = advanceHockeyPvpPuck(source, { + delta: 0.1, + localPlayerPosition: [8.8, 8.5], + localAimDirection: [0, -1], + opponentPlayerPosition: [0, 8.5], + opponentAimDirection: [0, -1], + }); + + expect(HOCKEY_PVP_GOAL_DAMAGE).toBe(45); + expect(result.localGoalsConceded).toBe(1); + expect(result.goalSequence).toBe(1); + expect(result.status).toBe("live"); + expect(result.puckVelocity[1]).toBeGreaterThan(0); + }); + + it("mirrors authoritative puck state for guest perspective", () => { + const source = createHockeyPvpState({ matchId: "match", seed: 9, opponentName: "Rival", role: "host" }); + source.puckPosition = [3, -8]; + source.puckVelocity = [-2, -6]; + source.localReturns = 4; + source.opponentReturns = 7; + source.localGoalsConceded = 2; + source.opponentGoalsConceded = 1; + source.goalSequence = 3; + source.lastGoalSide = "opponent"; + + expect(mirrorHockeyPvpPuck(source)).toMatchObject({ + puckPosition: [-3, 8], + puckVelocity: [2, 6], + localReturns: 7, + opponentReturns: 4, + localGoalsConceded: 1, + opponentGoalsConceded: 2, + lastGoalSide: "local", + }); + }); +}); diff --git a/src/game/hockeyHealingPvp.ts b/src/game/hockeyHealingPvp.ts new file mode 100644 index 0000000..7dab627 --- /dev/null +++ b/src/game/hockeyHealingPvp.ts @@ -0,0 +1,281 @@ +import { AVAILABLE_BOSS_IDS } from "./bossCatalog"; +import { hockeyReturnDirection } from "./hockeyHealing"; +import type { BossId, BossMotionMode, PartyMember, WorldPosition } from "./types"; + +export type HockeyPvpRole = "cpu" | "host" | "guest"; +export type HockeyPvpGoalSide = "local" | "opponent"; + +export interface HockeyPvpMatchConfig { + matchId: string | null; + seed: number; + opponentName: string; + role: HockeyPvpRole; +} + +export interface HockeyPvpPuckState { + puckPosition: WorldPosition; + puckVelocity: WorldPosition; + localReturns: number; + opponentReturns: number; + localGoalsConceded: number; + opponentGoalsConceded: number; + goalSequence: number; + lastGoalSide: HockeyPvpGoalSide | null; + serveIndex: number; +} + +export interface HockeyPvpState extends HockeyPvpMatchConfig, HockeyPvpPuckState { + status: "inactive" | "live" | "won" | "lost"; + aimDirection: WorldPosition; + opponentBossKills: number; + opponentPlayerPosition: WorldPosition; + opponentAimDirection: WorldPosition; + nextCpuHealAt: number; + networkSequence: number; + appliedGoalSequence: number; +} + +export interface HockeyPvpRemoteSnapshot { + sequence: number; + time: number; + party: PartyMember[]; + partyPositions: Record; + boss: { id: BossId; name: string; hp: number; maxHp: number }; + bossPosition: WorldPosition; + bossMode: BossMotionMode; + bossKills: number; + playerPosition: WorldPosition; + aimDirection: WorldPosition; + puck?: HockeyPvpPuckState; +} + +export const HOCKEY_PVP_SIDE_OFFSET_Z = 11; +export const HOCKEY_PVP_ARENA_MIN_X = -10; +export const HOCKEY_PVP_ARENA_MAX_X = 10; +export const HOCKEY_PVP_ARENA_MIN_Z = -25; +export const HOCKEY_PVP_ARENA_MAX_Z = 25; +export const HOCKEY_PVP_GOAL_Z = 23.5; +export const HOCKEY_PVP_GOAL_HALF_WIDTH = 8; +export const HOCKEY_PVP_PUCK_RADIUS = 0.42; +export const HOCKEY_PVP_INTERCEPT_RADIUS = 1.05; +export const HOCKEY_PVP_GOAL_DAMAGE = 45; +export const HOCKEY_PVP_DAMPENING_PER_BOSS_PERCENT = 5; +export const HOCKEY_PVP_QUEUE_TIMEOUT_MS = 5_000; + +const STARTING_SPEED = 7.2; +const MAX_SPEED = 11.5; +const MAX_SUBSTEPS = 10; +const MAX_SUBSTEP_DISTANCE = 0.32; +const SERVE_LANES = [0, -0.46, 0.58, -0.25, 0.34, -0.7, 0.74] as const; + +export function hockeyPvpDampeningPercent(localBossKills: number, opponentBossKills: number): number { + const totalBossKills = Math.max(0, Math.floor(localBossKills)) + Math.max(0, Math.floor(opponentBossKills)); + return Math.min(100, totalBossKills * HOCKEY_PVP_DAMPENING_PER_BOSS_PERCENT); +} + +export function hockeyPvpHealingEffectiveness(localBossKills: number, opponentBossKills: number): number { + return 1 - hockeyPvpDampeningPercent(localBossKills, opponentBossKills) / 100; +} + +function normalizedSeed(seed: number) { + return Math.max(1, Math.floor(Math.abs(Number(seed) || 1))) >>> 0; +} + +export function hockeyPvpBossAt(seed: number, index: number): BossId { + let value = (normalizedSeed(seed) + Math.max(0, Math.floor(index)) * 0x9e3779b9) >>> 0; + value ^= value >>> 16; + value = Math.imul(value, 0x21f0aaad) >>> 0; + value ^= value >>> 15; + value = Math.imul(value, 0x735a2d97) >>> 0; + value ^= value >>> 15; + return AVAILABLE_BOSS_IDS[(value >>> 0) % AVAILABLE_BOSS_IDS.length]; +} + +export function hockeyPvpPuckSpeed(totalReturns: number) { + return Math.min(MAX_SPEED, STARTING_SPEED + Math.max(0, totalReturns) * 0.16); +} + +function serveVelocity(side: HockeyPvpGoalSide, serveIndex: number, totalReturns: number): WorldPosition { + const x = SERVE_LANES[serveIndex % SERVE_LANES.length] * HOCKEY_PVP_GOAL_HALF_WIDTH; + const z = side === "local" ? HOCKEY_PVP_GOAL_Z : -HOCKEY_PVP_GOAL_Z; + const length = Math.max(0.001, Math.hypot(x, z)); + const speed = hockeyPvpPuckSpeed(totalReturns); + return [x / length * speed, z / length * speed]; +} + +export function createHockeyPvpState(config?: HockeyPvpMatchConfig): HockeyPvpState { + const match = config ?? { matchId: null, seed: 1, opponentName: "CPU Willow", role: "cpu" as const }; + return { + ...match, + status: config ? "live" : "inactive", + puckPosition: [0, 0], + puckVelocity: config ? serveVelocity("local", 0, 0) : [0, 0], + localReturns: 0, + opponentReturns: 0, + localGoalsConceded: 0, + opponentGoalsConceded: 0, + goalSequence: 0, + lastGoalSide: null, + serveIndex: 0, + aimDirection: [0, -1], + opponentBossKills: 0, + opponentPlayerPosition: [0, 8.5], + opponentAimDirection: [0, -1], + nextCpuHealAt: 1, + networkSequence: 0, + appliedGoalSequence: 0, + }; +} + +export function hockeyPvpLocalToWorld(position: WorldPosition): WorldPosition { + return [position[0], position[1] + HOCKEY_PVP_SIDE_OFFSET_Z]; +} + +export function hockeyPvpOpponentToWorld(position: WorldPosition): WorldPosition { + return [-position[0], -position[1] - HOCKEY_PVP_SIDE_OFFSET_Z]; +} + +function segmentDistanceSquared(start: WorldPosition, end: WorldPosition, point: WorldPosition) { + const dx = end[0] - start[0]; + const dz = end[1] - start[1]; + const lengthSquared = dx * dx + dz * dz; + const projection = lengthSquared < 0.000001 + ? 0 + : Math.max(0, Math.min(1, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dz) / lengthSquared)); + const nearestX = start[0] + dx * projection; + const nearestZ = start[1] + dz * projection; + return (point[0] - nearestX) ** 2 + (point[1] - nearestZ) ** 2; +} + +function mirrorDirection(direction: WorldPosition): WorldPosition { + return [-direction[0], -direction[1]]; +} + +function resetAfterGoal(state: HockeyPvpPuckState, side: HockeyPvpGoalSide) { + state.goalSequence += 1; + state.lastGoalSide = side; + if (side === "local") state.localGoalsConceded += 1; + else state.opponentGoalsConceded += 1; + state.serveIndex += 1; + state.puckPosition = [0, 0]; + state.puckVelocity = serveVelocity(side, state.serveIndex, state.localReturns + state.opponentReturns); +} + +export function advanceHockeyPvpPuck( + source: HockeyPvpState, + step: { + delta: number; + localPlayerPosition: WorldPosition; + localAimDirection: WorldPosition; + opponentPlayerPosition: WorldPosition; + opponentAimDirection: WorldPosition; + }, +): HockeyPvpState { + if (source.status !== "live" || source.role === "guest" || step.delta <= 0) return source; + const state: HockeyPvpState = { + ...source, + puckPosition: [...source.puckPosition], + puckVelocity: [...source.puckVelocity], + }; + const localPlayer = hockeyPvpLocalToWorld(step.localPlayerPosition); + const opponentPlayer = hockeyPvpOpponentToWorld(step.opponentPlayerPosition); + const speed = Math.hypot(state.puckVelocity[0], state.puckVelocity[1]); + const substeps = Math.max(1, Math.min(MAX_SUBSTEPS, Math.ceil(speed * step.delta / MAX_SUBSTEP_DISTANCE))); + const subDelta = step.delta / substeps; + const interceptRadiusSquared = (HOCKEY_PVP_INTERCEPT_RADIUS + HOCKEY_PVP_PUCK_RADIUS) ** 2; + + for (let index = 0; index < substeps; index += 1) { + const start: WorldPosition = [...state.puckPosition]; + const end: WorldPosition = [ + start[0] + state.puckVelocity[0] * subDelta, + start[1] + state.puckVelocity[1] * subDelta, + ]; + const minX = HOCKEY_PVP_ARENA_MIN_X + HOCKEY_PVP_PUCK_RADIUS; + const maxX = HOCKEY_PVP_ARENA_MAX_X - HOCKEY_PVP_PUCK_RADIUS; + if (end[0] < minX || end[0] > maxX) { + end[0] = Math.max(minX, Math.min(maxX, end[0])); + state.puckVelocity[0] *= -1; + } + + if (state.puckVelocity[1] > 0 && segmentDistanceSquared(start, end, localPlayer) <= interceptRadiusSquared) { + const direction = hockeyReturnDirection(step.localAimDirection); + state.localReturns += 1; + const returnSpeed = hockeyPvpPuckSpeed(state.localReturns + state.opponentReturns); + state.puckPosition = [end[0], Math.min(end[1], localPlayer[1])]; + state.puckVelocity = [direction[0] * returnSpeed, direction[1] * returnSpeed]; + continue; + } + + if (state.puckVelocity[1] < 0 && segmentDistanceSquared(start, end, opponentPlayer) <= interceptRadiusSquared) { + const direction = mirrorDirection(hockeyReturnDirection(step.opponentAimDirection)); + state.opponentReturns += 1; + const returnSpeed = hockeyPvpPuckSpeed(state.localReturns + state.opponentReturns); + state.puckPosition = [end[0], Math.max(end[1], opponentPlayer[1])]; + state.puckVelocity = [direction[0] * returnSpeed, direction[1] * returnSpeed]; + continue; + } + + if (state.puckVelocity[1] > 0 && end[1] >= HOCKEY_PVP_GOAL_Z) { + if (Math.abs(end[0]) <= HOCKEY_PVP_GOAL_HALF_WIDTH) resetAfterGoal(state, "local"); + else { + state.puckPosition = [end[0], HOCKEY_PVP_GOAL_Z]; + state.puckVelocity[1] = -Math.abs(state.puckVelocity[1]); + } + continue; + } + + if (state.puckVelocity[1] < 0 && end[1] <= -HOCKEY_PVP_GOAL_Z) { + if (Math.abs(end[0]) <= HOCKEY_PVP_GOAL_HALF_WIDTH) resetAfterGoal(state, "opponent"); + else { + state.puckPosition = [end[0], -HOCKEY_PVP_GOAL_Z]; + state.puckVelocity[1] = Math.abs(state.puckVelocity[1]); + } + continue; + } + + state.puckPosition = end; + } + return state; +} + +export function advanceHockeyPvpCpuGoalie( + position: WorldPosition, + puckPosition: WorldPosition, + delta: number, +): { position: WorldPosition; aimDirection: WorldPosition } { + const opponentLocalPuckX = -puckPosition[0]; + const targetX = Math.max(-8.6, Math.min(8.6, opponentLocalPuckX)); + const maxMovement = 7.2 * Math.max(0, delta); + const nextX = Math.abs(targetX - position[0]) <= maxMovement + ? targetX + : position[0] + Math.sign(targetX - position[0]) * maxMovement; + const aimX = Math.max(-0.85, Math.min(0.85, -opponentLocalPuckX / HOCKEY_PVP_GOAL_HALF_WIDTH)); + return { position: [nextX, 8.5], aimDirection: [aimX, -1] }; +} + +export function mirrorHockeyPvpPuck(source: HockeyPvpPuckState): HockeyPvpPuckState { + return { + puckPosition: [-source.puckPosition[0], -source.puckPosition[1]], + puckVelocity: [-source.puckVelocity[0], -source.puckVelocity[1]], + localReturns: source.opponentReturns, + opponentReturns: source.localReturns, + localGoalsConceded: source.opponentGoalsConceded, + opponentGoalsConceded: source.localGoalsConceded, + goalSequence: source.goalSequence, + lastGoalSide: source.lastGoalSide === "local" ? "opponent" : source.lastGoalSide === "opponent" ? "local" : null, + serveIndex: source.serveIndex, + }; +} + +export const HOCKEY_PVP_CPU_NAMES = [ + "Willow Warden", + "Mender Nova", + "Pulsekeeper", + "Sage Rook", + "Mercy Vale", + "Aster Ward", +] as const; + +export function randomHockeyPvpCpuName(random: () => number = Math.random) { + return HOCKEY_PVP_CPU_NAMES[Math.floor(random() * HOCKEY_PVP_CPU_NAMES.length)] ?? HOCKEY_PVP_CPU_NAMES[0]; +} diff --git a/src/game/hockeyHealingPvpStore.test.ts b/src/game/hockeyHealingPvpStore.test.ts new file mode 100644 index 0000000..bc99c9c --- /dev/null +++ b/src/game/hockeyHealingPvpStore.test.ts @@ -0,0 +1,197 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { createClassInventory } from "./healers"; +import { HOCKEY_PVP_GOAL_DAMAGE, HOCKEY_PVP_GOAL_Z, hockeyPvpBossAt } from "./hockeyHealingPvp"; +import { upcomingEncounterMechanic, useGameStore } from "./store"; +import { freshParty } from "./data"; +import { createDefaultGearProgress, GEAR_OWNER_ORDER, GEAR_SLOT_ORDER, MAX_GEAR_LEVEL } from "./progression/gear"; +import { EMPTY_MEMBER_GEAR_MODIFIERS } from "./progression/gearEffects"; + +const MATCH = { + matchId: null, + seed: 4242, + opponentName: "CPU Aster", + role: "cpu" as const, +}; + +function createMaxedGear() { + const gear = createDefaultGearProgress(); + for (const ownerId of GEAR_OWNER_ORDER) { + for (const slotId of GEAR_SLOT_ORDER) gear[ownerId].slots[slotId].level = MAX_GEAR_LEVEL; + } + gear.priest.passiveInfusionId = "mend-echo"; + return gear; +} + +describe("Healing Hockey PVP encounter integration", () => { + beforeEach(() => { + useGameStore.getState().configureHealer( + "priest", + "Aelia", + createClassInventory("priest"), + [hockeyPvpBossAt(MATCH.seed, 0)], + "hockey-healing-pvp", + undefined, + "initiate", + MATCH, + ); + }); + + it("starts exactly two copies of same boss across mirrored parties", () => { + const state = useGameStore.getState(); + expect(state.additionalBosses).toHaveLength(0); + expect(state.boss.id).toBe(hockeyPvpBossAt(MATCH.seed, 0)); + expect(state.hockeyPvpOpponent.boss.id).toBe(state.boss.id); + expect(state.hockeyPvp.opponentName).toBe("CPU Aster"); + }); + + it("normalizes both parties to default base gear without changing saved upgrades", () => { + const maxedGear = createMaxedGear(); + const baseHealth = freshParty("priest", "Aelia").map((member) => member.maxHp); + useGameStore.getState().configureHealer( + "priest", + "Aelia", + createClassInventory("priest"), + [hockeyPvpBossAt(MATCH.seed, 0)], + "hockey-healing-pvp", + maxedGear, + "initiate", + MATCH, + ); + + const briefing = useGameStore.getState(); + expect(briefing.party.map((member) => member.maxHp)).toEqual(baseHealth); + expect(briefing.hockeyPvpOpponent.party.map((member) => member.maxHp)).toEqual(baseHealth); + expect(briefing.healingMultiplier).toBe(1); + expect(briefing.passiveRunBuffId).toBeNull(); + expect(briefing.gearProgress).toBe(maxedGear); + for (const modifiers of Object.values(briefing.gearModifiers)) { + expect(modifiers).toEqual(EMPTY_MEMBER_GEAR_MODIFIERS); + } + + briefing.startEncounter(); + const combat = useGameStore.getState(); + expect(combat.party.map((member) => member.maxHp)).toEqual(baseHealth); + expect(combat.hockeyPvpOpponent.party.map((member) => member.maxHp)).toEqual(baseHealth); + expect(combat.healingMultiplier).toBe(1); + expect(combat.passiveRunBuffId).toBeNull(); + expect(combat.gearProgress).toBe(maxedGear); + }); + + it("continues applying saved gear outside PVP", () => { + const maxedGear = createMaxedGear(); + const baseHealth = freshParty("priest", "Aelia").map((member) => member.maxHp); + useGameStore.getState().configureHealer( + "priest", + "Aelia", + createClassInventory("priest"), + "bulldrome", + "encounter", + maxedGear, + ); + + const state = useGameStore.getState(); + expect(state.party.map((member) => member.maxHp)).not.toEqual(baseHealth); + expect(state.healingMultiplier).toBeGreaterThan(1); + expect(state.passiveRunBuffId).toBe("mend-echo"); + }); + + it("deals exactly 45 partywide damage when local net is breached", () => { + useGameStore.getState().startEncounter(); + const startingHp = useGameStore.getState().party.map((member) => member.hp); + useGameStore.setState((state) => ({ + hockeyPvp: { + ...state.hockeyPvp, + puckPosition: [0, HOCKEY_PVP_GOAL_Z - 0.1], + puckVelocity: [0, 8], + }, + playerPosition: [8.8, 8.5], + partyPositions: { ...state.partyPositions, aelia: [8.8, 8.5] }, + boss: { ...state.boss, nextMeleeAt: 999 }, + bossMotion: { ...state.bossMotion, nextMechanicAt: 999 }, + hockeyPvpOpponent: { + ...state.hockeyPvpOpponent, + boss: { ...state.hockeyPvpOpponent.boss, nextMeleeAt: 999 }, + bossMotion: { ...state.hockeyPvpOpponent.bossMotion, nextMechanicAt: 999 }, + }, + })); + + useGameStore.getState().tick(0.1); + + useGameStore.getState().party.forEach((member, index) => { + expect(member.hp).toBe(startingHp[index] - HOCKEY_PVP_GOAL_DAMAGE); + }); + expect(useGameStore.getState().phase).toBe("combat"); + }); + + it("reduces both local and CPU healing by combined boss-kill dampening", () => { + useGameStore.getState().startEncounter(); + useGameStore.setState((state) => ({ + endlessBossKills: 1, + party: state.party.map((member) => ({ ...member, hp: Math.max(1, member.hp - 40) })), + boss: { ...state.boss, nextMeleeAt: 999 }, + bossMotion: { ...state.bossMotion, nextMechanicAt: 999 }, + hockeyPvp: { ...state.hockeyPvp, opponentBossKills: 1, nextCpuHealAt: 0 }, + hockeyPvpOpponent: { + ...state.hockeyPvpOpponent, + party: state.hockeyPvpOpponent.party.map((member, index) => ({ ...member, hp: index === 0 ? 50 : member.hp })), + boss: { ...state.hockeyPvpOpponent.boss, nextMeleeAt: 999 }, + bossMotion: { ...state.hockeyPvpOpponent.bossMotion, nextMechanicAt: 999 }, + }, + })); + + const localBefore = useGameStore.getState().party[0].hp; + expect(useGameStore.getState().castAbility("ability5")).toBe(true); + expect(useGameStore.getState().party[0].hp - localBefore).toBeCloseTo(22 * 0.9, 5); + + useGameStore.getState().tick(0.01); + expect(useGameStore.getState().hockeyPvpOpponent.party[0].hp).toBeCloseTo(50 + 32 * 0.9, 5); + }); + + it("keeps faster and slower parties on same deterministic boss order", () => { + useGameStore.getState().startEncounter(); + useGameStore.setState((state) => ({ + boss: { ...state.boss, hp: 0, defeatedAt: state.time }, + endlessBossKills: 1, + })); + useGameStore.getState().tick(0.01); + expect(useGameStore.getState().boss.id).toBe(hockeyPvpBossAt(MATCH.seed, 1)); + expect(useGameStore.getState().hockeyPvpOpponent.boss.id).toBe(hockeyPvpBossAt(MATCH.seed, 0)); + + useGameStore.setState((state) => ({ + hockeyPvp: { ...state.hockeyPvp, opponentBossKills: 1 }, + hockeyPvpOpponent: { + ...state.hockeyPvpOpponent, + boss: { ...state.hockeyPvpOpponent.boss, hp: 0, defeatedAt: state.time }, + }, + })); + useGameStore.getState().tick(0.01); + + expect(useGameStore.getState().hockeyPvpOpponent.boss.id).toBe(hockeyPvpBossAt(MATCH.seed, 1)); + expect(useGameStore.getState().boss.id).toBe(useGameStore.getState().hockeyPvpOpponent.boss.id); + }); + + it("wins when opponent party falls", () => { + useGameStore.getState().startEncounter(); + useGameStore.getState().setActiveTab("map"); + useGameStore.setState((state) => ({ + hockeyPvpOpponent: { + ...state.hockeyPvpOpponent, + party: state.hockeyPvpOpponent.party.map((member) => ({ ...member, hp: 0 })), + }, + })); + useGameStore.getState().tick(0.01); + expect(useGameStore.getState().phase).toBe("victory"); + expect(useGameStore.getState().hockeyPvp.status).toBe("won"); + expect(useGameStore.getState().activeTab).toBe("combat"); + }); + + it("keeps HUD mechanic data defined during instant boss replacement", () => { + useGameStore.setState((state) => ({ boss: { ...state.boss, hp: 0 } })); + expect(upcomingEncounterMechanic(useGameStore.getState())).toEqual({ + name: "Replacement incoming", + remaining: 0, + cycle: 1, + urgent: false, + }); + }); +}); diff --git a/src/game/hockeyHealingStore.test.ts b/src/game/hockeyHealingStore.test.ts new file mode 100644 index 0000000..0d831d1 --- /dev/null +++ b/src/game/hockeyHealingStore.test.ts @@ -0,0 +1,146 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { BOSS_DEATH_DESPAWN_SECONDS } from "./bossDeath"; +import { createClassInventory } from "./healers"; +import { HOCKEY_HEALER_GOAL_Z, HOCKEY_MIDLINE_Z, HOCKEY_NPC_PADDLE_Z } from "./hockeyHealing"; +import { useGameStore } from "./store"; + +describe("Hockey Healing encounter integration", () => { + beforeEach(() => { + useGameStore.getState().configureHealer( + "priest", + "Aelia", + createClassInventory("priest"), + ["bulldrome", "broodfang-spider"], + "hockey-healing", + ); + }); + + it("starts two bosses on enemy half with healer defending own half", () => { + const state = useGameStore.getState(); + expect(state.endlessMode).toBe(true); + expect(state.additionalBosses).toHaveLength(1); + expect(state.draftBuffIds).toEqual([]); + expect(state.bossMotion.position[1]).toBeLessThan(HOCKEY_MIDLINE_Z); + expect(state.additionalBosses[0].motion.position[1]).toBeLessThan(HOCKEY_MIDLINE_Z); + expect(state.partyPositions.aelia[1]).toBeGreaterThan(HOCKEY_MIDLINE_Z); + for (const memberId of ["brann", "nia", "orin", "vale"] as const) { + expect(state.partyPositions[memberId][1]).toBeLessThan(HOCKEY_MIDLINE_Z); + } + }); + + it("uses world-space left-stick direction when healer returns puck", () => { + useGameStore.getState().startEncounter(); + useGameStore.getState().setHockeyAimDirection([1, -0.5]); + useGameStore.setState((state) => ({ + hockey: { ...state.hockey, puckPosition: [0, 7.8], puckVelocity: [0, 5.2] }, + boss: { ...state.boss, nextMeleeAt: 999 }, + bossMotion: { ...state.bossMotion, nextMechanicAt: 999 }, + additionalBosses: state.additionalBosses.map((entry) => ({ + ...entry, + boss: { ...entry.boss, nextMeleeAt: 999 }, + motion: { ...entry.motion, nextMechanicAt: 999 }, + })), + })); + + useGameStore.getState().tick(0.2); + + expect(useGameStore.getState().hockey.returns).toBe(1); + expect(useGameStore.getState().hockey.puckVelocity[0]).toBeGreaterThan(0); + expect(useGameStore.getState().hockey.puckVelocity[1]).toBeLessThan(0); + }); + + it("ends run on wide-goal breach", () => { + useGameStore.getState().startEncounter(); + useGameStore.getState().setPlayerPosition([7, 8]); + useGameStore.setState((state) => ({ + hockey: { ...state.hockey, puckPosition: [0, HOCKEY_HEALER_GOAL_Z - 0.2], puckVelocity: [0, 5.2] }, + })); + + useGameStore.getState().tick(0.1); + + expect(useGameStore.getState().hockey.status).toBe("lost"); + expect(useGameStore.getState().phase).toBe("defeat"); + }); + + it("keeps the rally going through the enemy Pong paddle", () => { + useGameStore.getState().startEncounter(); + useGameStore.setState((state) => ({ + hockey: { ...state.hockey, puckPosition: [0, HOCKEY_NPC_PADDLE_Z + 1], puckVelocity: [0, -5.2] }, + })); + + useGameStore.getState().tick(0.2); + + const hockey = useGameStore.getState().hockey; + expect(hockey.paddleReturns).toBe(1); + expect(hockey.puckVelocity[1]).toBeGreaterThan(0); + expect(useGameStore.getState().phase).toBe("combat"); + }); + + it("spawns Soul Siphon only on the healer side", () => { + useGameStore.getState().configureHealer( + "priest", + "Aelia", + createClassInventory("priest"), + ["mournveil-ghost", "bulldrome"], + "hockey-healing", + ); + useGameStore.getState().startEncounter(); + useGameStore.setState((state) => ({ + boss: { ...state.boss, nextMeleeAt: 999 }, + bossMotion: { ...state.bossMotion, mechanicCount: 2, nextMechanicAt: 0 }, + additionalBosses: state.additionalBosses.map((entry) => ({ + ...entry, + boss: { ...entry.boss, nextMeleeAt: 999 }, + motion: { ...entry.motion, nextMechanicAt: 999 }, + })), + })); + + useGameStore.getState().tick(0.1); + + const siphon = useGameStore.getState().bossMotion.poolTelegraphs[0].soulSiphon!; + expect(siphon.wardPosition[1] - siphon.wardRadius).toBeGreaterThan(HOCKEY_MIDLINE_Z); + expect(siphon.ghostPosition[1]).toBeGreaterThan(HOCKEY_MIDLINE_Z); + }); + + it("never marks the healer for Crushing Pounce", () => { + useGameStore.getState().startEncounter(); + useGameStore.setState((state) => ({ + boss: { ...state.boss, nextMeleeAt: 999 }, + bossMotion: { ...state.bossMotion, mechanicCount: 9, nextMechanicAt: 0 }, + additionalBosses: state.additionalBosses.map((entry) => ({ + ...entry, + boss: { ...entry.boss, nextMeleeAt: 999 }, + motion: { ...entry.motion, nextMechanicAt: 999 }, + })), + })); + + useGameStore.getState().tick(0.05); + + const motion = useGameStore.getState().bossMotion; + expect(motion.activeMechanicId).toBe("crushing-pounce"); + expect(motion.pounceTargetId).not.toBe("aelia"); + }); + + it("replaces a fallen boss in same slot after death visual finishes", () => { + useGameStore.getState().startEncounter(); + const defeatedInstanceId = useGameStore.getState().bossInstanceId; + useGameStore.setState((state) => ({ + boss: { ...state.boss, hp: 0, defeatedAt: state.time }, + hockey: { ...state.hockey, puckVelocity: [0, 0] }, + })); + + let elapsed = 0; + while (elapsed <= BOSS_DEATH_DESPAWN_SECONDS + 0.2) { + useGameStore.getState().tick(0.1); + elapsed += 0.1; + } + + const replaced = useGameStore.getState(); + expect(replaced.phase).toBe("combat"); + expect(replaced.bossInstanceId).not.toBe(defeatedInstanceId); + expect(replaced.bossInstanceId.startsWith("hockey-")).toBe(true); + expect(replaced.boss.hp).toBe(replaced.boss.maxHp); + expect(replaced.bossMotion.position[1]).toBeLessThan(HOCKEY_MIDLINE_Z); + expect(replaced.additionalBosses).toHaveLength(1); + }); +}); diff --git a/src/game/partyBehaviors.test.ts b/src/game/partyBehaviors.test.ts index e6b8cfc..9ea75b2 100644 --- a/src/game/partyBehaviors.test.ts +++ b/src/game/partyBehaviors.test.ts @@ -22,6 +22,18 @@ describe("party boss positioning", () => { expect(formation.vale).toEqual([2, -2.7]); }); + it("places drafted companions by combat kit instead of stable slot id", () => { + const party = freshParty().map((member) => member.id === "brann" + ? { ...member, runProfile: { instanceId: "ranged", combatProfileId: "warlock-damage", combatKitId: "ranged" as const, tier: "white", visualArchetype: "mage" as const } } + : member.id === "nia" + ? { ...member, runProfile: { instanceId: "tank", combatProfileId: "warrior-tank", combatKitId: "tank" as const, tier: "white", visualArchetype: "knight" as const } } + : member); + const formation = combatFormation([0, 0], party); + + expect(formation.brann[1]).toBe(7.2); + expect(formation.nia[1]).toBe(4.25); + }); + it("moves the tank toward the front and the rogue toward the rear during uptime", () => { const sharedStart: WorldPosition = [0, 0]; const positions: Record = { diff --git a/src/game/partyBehaviors.ts b/src/game/partyBehaviors.ts index 48f6297..1b02162 100644 --- a/src/game/partyBehaviors.ts +++ b/src/game/partyBehaviors.ts @@ -1,12 +1,13 @@ import { ARENA_CENTER, ARENA_RADIUS, clampToArena } from "./arena"; import { BULL_CHARGE, BULL_POUNCE, SKY_SWEEPER_BREATH } from "./bosses/mechanicPool"; import { angularDistance, moveToward, pointOutsideCircle, pointOutsideLane, pointToSegmentDistance } from "./geometry"; -import type { BossMotionState, MemberId, PartyMember, WorldPosition } from "./types"; +import type { BossMotionState, MemberId, PartyCombatKitId, PartyMember, WorldPosition } from "./types"; export type AiMemberId = Exclude; export interface PartyBehaviorContext { memberId: AiMemberId; + combatKitId: PartyCombatKitId; current: WorldPosition; formationTarget: WorldPosition; bossMotion: BossMotionState; @@ -26,7 +27,13 @@ export interface PartyBehavior { } const AI_MEMBER_IDS: readonly AiMemberId[] = ["brann", "nia", "orin", "vale"]; -const MOVE_SPEEDS: Record = { brann: 1.45, nia: 1.2, orin: 1.1, vale: 2.2 }; +const LEGACY_COMBAT_KITS: Record = { + brann: "tank", + nia: "ranged", + orin: "caster", + vale: "melee", +}; +const MOVE_SPEEDS: Record = { tank: 1.45, ranged: 1.2, caster: 1.1, melee: 2.2 }; const EVADE_SIDES: Record = { brann: -1, nia: -1, orin: 1, vale: 1 }; const HAZARD_LOOKAHEAD = 2.2; const CIRCLE_HAZARD_CLEARANCE = 0.55; @@ -50,21 +57,28 @@ const FORMATION_MODES: readonly BossMotionState["mode"][] = [ ]; const DASH_MODES: readonly BossMotionState["mode"][] = ["telegraph", "charging"]; -const FORMATION_SLOTS: Record = { - // Bosses face Brann during normal uptime, making positive Z their front. - brann: [0, 4.25], - nia: [-3.3, 7.2], - orin: [3.3, 7.2], - vale: [0, -1.7], +const FORMATION_LATERAL_OFFSETS: Record = { brann: 0, nia: -3.3, orin: 3.3, vale: 0 }; +const FORMATION_DEPTHS: Record = { + // Bosses face tanks during normal uptime, making positive Z their front. + tank: 4.25, + ranged: 7.2, + caster: 7.2, + melee: -1.7, }; -export function combatFormation(boss: WorldPosition): Record { - return { - brann: [boss[0] + FORMATION_SLOTS.brann[0], boss[1] + FORMATION_SLOTS.brann[1]], - nia: [boss[0] + FORMATION_SLOTS.nia[0], boss[1] + FORMATION_SLOTS.nia[1]], - orin: [boss[0] + FORMATION_SLOTS.orin[0], boss[1] + FORMATION_SLOTS.orin[1]], - vale: [boss[0] + FORMATION_SLOTS.vale[0], boss[1] + FORMATION_SLOTS.vale[1]], - }; +function combatKit(memberId: AiMemberId, party?: readonly PartyMember[]): PartyCombatKitId { + return party?.find((member) => member.id === memberId)?.runProfile?.combatKitId ?? LEGACY_COMBAT_KITS[memberId]; +} + +export function combatFormation(boss: WorldPosition, party?: readonly PartyMember[]): Record { + const formation = {} as Record; + for (const memberId of AI_MEMBER_IDS) { + formation[memberId] = [ + boss[0] + FORMATION_LATERAL_OFFSETS[memberId], + boss[1] + FORMATION_DEPTHS[combatKit(memberId, party)], + ]; + } + return formation; } function circleDanger( @@ -252,11 +266,11 @@ export const evadeChargeBehavior: PartyBehavior = { export const avoidBreathBehavior: PartyBehavior = { id: "avoid-breath", - decide: ({ memberId, bossMotion }) => { + decide: ({ memberId, combatKitId, bossMotion }) => { if (bossMotion.mode !== "breath_telegraph" && bossMotion.mode !== "breath_sweeping") return null; const side = EVADE_SIDES[memberId]; const safeAngle = bossMotion.breathAngle + side * (SKY_SWEEPER_BREATH.halfAngle + Math.PI * 0.42); - const radius = memberId === "brann" ? 4.1 : memberId === "vale" ? 3.5 : 5.4; + const radius = combatKitId === "tank" ? 4.1 : combatKitId === "melee" ? 3.5 : 5.4; return { target: clampToArena([ bossMotion.position[0] + Math.sin(safeAngle) * radius, @@ -375,9 +389,9 @@ export const reactToPooledTelegraphsBehavior: PartyBehavior = { export const maintainFormationBehavior: PartyBehavior = { id: "maintain-formation", - decide: ({ formationTarget, bossMotion, memberId }) => { + decide: ({ formationTarget, bossMotion, combatKitId }) => { if (!FORMATION_MODES.includes(bossMotion.mode)) return null; - return { target: formationTarget, speed: MOVE_SPEEDS[memberId] }; + return { target: formationTarget, speed: MOVE_SPEEDS[combatKitId] }; }, }; @@ -420,17 +434,18 @@ export function updatePartyPositions( formationZ += origin[1]; } const formationOrigin: WorldPosition = [formationX / activeMotions.length, formationZ / activeMotions.length]; - const formation = combatFormation(formationOrigin); + const formation = combatFormation(formationOrigin, party); - for (let index = 0; index < AI_MEMBER_IDS.length; index += 1) { - const memberId = AI_MEMBER_IDS[index]; - const member = party[index + 1]; + for (const memberId of AI_MEMBER_IDS) { + const member = party.find((candidate) => candidate.id === memberId); if (!member || member.hp <= 0 || member.knockedUntil > time) continue; + const combatKitId = combatKit(memberId, party); for (const behavior of behaviors) { let handled = false; for (const bossMotion of activeMotions) { const decision = behavior.decide({ memberId, + combatKitId, current: next[memberId], formationTarget: formation[memberId], bossMotion, diff --git a/src/game/partyCombat.test.ts b/src/game/partyCombat.test.ts index 66622af..93ee8e3 100644 --- a/src/game/partyCombat.test.ts +++ b/src/game/partyCombat.test.ts @@ -28,6 +28,7 @@ interface SimulationOptions { targetPositions?: readonly WorldPosition[]; stopOnVictory?: boolean; upcomingMechanicRemaining?: number; + damageProfiles?: Partial>; } function createTargets(bossIds: readonly BossId[], positions?: readonly WorldPosition[]): PartyCombatTarget[] { @@ -68,6 +69,7 @@ function simulate(bossIds: readonly BossId[], options: SimulationOptions = {}) { positions, targets, upcomingMechanicRemaining: options.upcomingMechanicRemaining ?? Number.POSITIVE_INFINITY, + damageProfiles: options.damageProfiles as Record | undefined, }); combat = result.state; for (const actor of Object.values(combat.combatants)) { @@ -122,6 +124,43 @@ describe("party ability combat", () => { expect(clustered.damageBySource.vale).toBeGreaterThan(separated.damageBySource.vale ?? 0); }); + it("accepts injected single-target and area profiles without class checks in combat core", () => { + const baseline = simulate(["bulldrome"], { duration: 12, activeIds: ["vale"], stopOnVictory: false }); + const focused = simulate(["bulldrome"], { + duration: 12, + activeIds: ["vale"], + stopOnVictory: false, + damageProfiles: { vale: { singleTarget: 1.5, areaDamage: 0.5 } }, + }); + expect(focused.damageBySource.vale).toBeGreaterThan((baseline.damageBySource.vale ?? 0) * 1.4); + }); + + it("uses injected AoE stats for ranged volleys and caster splash", () => { + const legacy = simulate(["broodfang-spider", "tempestscale-dragon"], { + duration: 18, + activeIds: ["nia"], + stopOnVictory: false, + }); + expect(legacy.secondaryEvents).toHaveLength(0); + + for (const memberId of ["nia", "orin"] as const) { + const lowArea = simulate(["broodfang-spider", "tempestscale-dragon"], { + duration: 18, + activeIds: [memberId], + stopOnVictory: false, + damageProfiles: { [memberId]: { singleTarget: 1, areaDamage: 0.6 } }, + }); + const highArea = simulate(["broodfang-spider", "tempestscale-dragon"], { + duration: 18, + activeIds: [memberId], + stopOnVictory: false, + damageProfiles: { [memberId]: { singleTarget: 1, areaDamage: 1.4 } }, + }); + expect(lowArea.secondaryEvents.length).toBeGreaterThan(0); + expect(highArea.damageBySource[memberId]).toBeGreaterThan(lowArea.damageBySource[memberId] ?? 0); + } + }); + it("activates Brann's moving six-second Bulwark aura before incoming damage", () => { const result = simulate(["bulldrome"], { duration: 0.1, activeIds: ["brann"], stopOnVictory: false, upcomingMechanicRemaining: 1 }); expect(result.combat.combatants.brann.visualAction?.abilityId).toBe("bulwark_march"); diff --git a/src/game/partyCombat.ts b/src/game/partyCombat.ts index 9f3d6f4..f525b5b 100644 --- a/src/game/partyCombat.ts +++ b/src/game/partyCombat.ts @@ -1,5 +1,5 @@ import { distance } from "./geometry"; -import type { BossMotionState, BossState, MemberId, PartyMember, WorldPosition } from "./types"; +import type { BossMotionState, BossState, MemberId, PartyCombatKitId, PartyMember, WorldPosition } from "./types"; export type AiCombatantId = Exclude; @@ -94,6 +94,7 @@ export interface TankAuraState { expiresAt: number; radius: number; damageReduction: number; + sourceId: AiCombatantId; } export interface PartyCombatState { @@ -111,6 +112,7 @@ export interface PartyCombatContext { targets: PartyCombatTarget[]; upcomingMechanicRemaining: number; gearModifiers?: Record; + damageProfiles?: Record; } interface AbilitySpec { @@ -123,19 +125,40 @@ interface AbilitySpec { requiresStationary?: boolean; } -const EMPTY_AURA: TankAuraState = { expiresAt: 0, radius: 3, damageReduction: 0.3 }; -const RANGED_IDS: readonly AiCombatantId[] = ["nia", "orin"]; +interface AreaDamageShape { + readonly radius: number; + readonly secondaryDamageRatio: number; +} + +/** Ability geometry stays data-driven; injected class stats tune actual AoE power. */ +const AREA_DAMAGE_SHAPES: Partial> = { + rapid_fire: { radius: 6, secondaryDamageRatio: 0.35 }, + arcane_burst: { radius: 6, secondaryDamageRatio: 0.8 }, + comet: { radius: 7, secondaryDamageRatio: 0.65 }, +}; + +const EMPTY_AURA: TankAuraState = { expiresAt: 0, radius: 3, damageReduction: 0.3, sourceId: "brann" }; +const LEGACY_COMBAT_KITS: Record = { + brann: "tank", + nia: "ranged", + orin: "caster", + vale: "melee", +}; const VALE_CLEAVE_RADIUS = 3.6; const VALE_MELEE_RANGE = 3.65; const BRANN_MELEE_RANGE = 4.8; // Calibrated against full two-boss rotations: intended 50–90 seconds, never over 100. const PARTY_DAMAGE_SCALE = 2; -function combatant(id: AiCombatantId, hp: number): PartyCombatantState { +function combatKit(id: AiCombatantId, party: readonly PartyMember[]): PartyCombatKitId { + return party.find((member) => member.id === id)?.runProfile?.combatKitId ?? LEGACY_COMBAT_KITS[id]; +} + +function combatant(id: AiCombatantId, hp: number, kit: PartyCombatKitId): PartyCombatantState { return { id, readyAt: 0, - resource: id === "vale" ? 100 : 0, + resource: kit === "melee" ? 100 : 0, points: 0, cooldowns: {}, activeAction: null, @@ -152,10 +175,10 @@ export function createPartyCombatState(party: PartyMember[]): PartyCombatState { const hp = (id: AiCombatantId) => party.find((member) => member.id === id)?.hp ?? 0; return { combatants: { - brann: combatant("brann", hp("brann")), - nia: combatant("nia", hp("nia")), - orin: combatant("orin", hp("orin")), - vale: combatant("vale", hp("vale")), + brann: combatant("brann", hp("brann"), combatKit("brann", party)), + nia: combatant("nia", hp("nia"), combatKit("nia", party)), + orin: combatant("orin", hp("orin"), combatKit("orin", party)), + vale: combatant("vale", hp("vale"), combatKit("vale", party)), }, tankAura: { ...EMPTY_AURA }, nextEventId: 1, @@ -191,8 +214,9 @@ function targetsInRange(source: WorldPosition, targets: PartyCombatTarget[], ran function targetFor(id: AiCombatantId, context: PartyCombatContext, range = Number.POSITIVE_INFINITY) { const eligible = targetsInRange(context.positions[id], context.targets, range); if (!eligible.length) return undefined; - if (id === "orin" && eligible.length > 1) return eligible[1]; - if (RANGED_IDS.includes(id)) return eligible[0]; + const kit = combatKit(id, context.party); + if (kit === "caster" && eligible.length > 1) return eligible[1]; + if (kit === "ranged" || kit === "caster") return eligible[0]; let nearest = eligible[0]; for (let index = 1; index < eligible.length; index += 1) { if (distance(context.positions[id], eligible[index].motion.position) < distance(context.positions[id], nearest.motion.position)) nearest = eligible[index]; @@ -200,16 +224,17 @@ function targetFor(id: AiCombatantId, context: PartyCombatContext, range = Numbe return nearest; } -function partyNeedsBulwark(context: PartyCombatContext) { - const brann = context.party.find((member) => member.id === "brann"); +function partyNeedsBulwark(context: PartyCombatContext, tankId: AiCombatantId) { + const tank = context.party.find((member) => member.id === tankId); const lowMembers = context.party.filter((member) => member.hp > 0 && member.hp / member.maxHp < 0.6).length; - return context.upcomingMechanicRemaining <= 2 || (brann?.hp ?? 0) / Math.max(1, brann?.maxHp ?? 1) < 0.7 || lowMembers >= 2; + return context.upcomingMechanicRemaining <= 2 || (tank?.hp ?? 0) / Math.max(1, tank?.maxHp ?? 1) < 0.7 || lowMembers >= 2; } function chooseAbility(actor: PartyCombatantState, at: number, isMoving: boolean, context: PartyCombatContext): AbilitySpec | null { const source = context.positions[actor.id]; const rangedTarget = targetFor(actor.id, context); - if (actor.id === "nia") { + const kit = combatKit(actor.id, context.party); + if (kit === "ranged") { if (!rangedTarget) return null; if (rangedTarget.boss.hp / rangedTarget.boss.maxHp <= 0.25 && isReady(actor, "kill_shot", at)) return { id: "kill_shot", duration: 0.45, impactOffsets: [0.24], damage: 7, gcd: 1.1, cooldown: 10 }; if (!isMoving && isReady(actor, "rapid_fire", at)) return { id: "rapid_fire", duration: 2, impactOffsets: [0.4, 0.8, 1.2, 1.6], damage: 2, gcd: 2, cooldown: 9, requiresStationary: true }; @@ -218,7 +243,7 @@ function chooseAbility(actor: PartyCombatantState, at: number, isMoving: boolean return { id: "quick_shot", duration: 0.45, impactOffsets: [0.24], damage: 2, gcd: 1.15 }; } - if (actor.id === "orin") { + if (kit === "caster") { if (!rangedTarget) return null; if (!isMoving && actor.overchargeStacks === 0 && isReady(actor, "overcharge", at)) return { id: "overcharge", duration: 0.35, impactOffsets: [], damage: 0, gcd: 0.7, cooldown: 20 }; if (!isMoving && isReady(actor, "comet", at)) return { id: "comet", duration: 2, impactOffsets: [2], damage: 9, gcd: 2, cooldown: 12, requiresStationary: true }; @@ -228,7 +253,7 @@ function chooseAbility(actor: PartyCombatantState, at: number, isMoving: boolean return { id: "arcane_bolt", duration: 1.4, impactOffsets: [1.4], damage: 3, gcd: 1.4, requiresStationary: true }; } - if (actor.id === "vale") { + if (kit === "melee") { const nearby = targetsInRange(source, context.targets, VALE_CLEAVE_RADIUS); const target = targetFor(actor.id, context, VALE_MELEE_RANGE); if (!target) return null; @@ -241,7 +266,7 @@ function chooseAbility(actor: PartyCombatantState, at: number, isMoving: boolean const target = targetFor(actor.id, context, BRANN_MELEE_RANGE); if (!target) return null; - if (partyNeedsBulwark(context) && isReady(actor, "bulwark_march", at)) return { id: "bulwark_march", duration: 0.6, impactOffsets: [0.35], damage: 2, gcd: 1.1, cooldown: 30 }; + if (partyNeedsBulwark(context, actor.id) && isReady(actor, "bulwark_march", at)) return { id: "bulwark_march", duration: 0.6, impactOffsets: [0.35], damage: 2, gcd: 1.1, cooldown: 30 }; if (actor.revengeReadyUntil > at && isReady(actor, "revenge", at)) return { id: "revenge", duration: 0.6, impactOffsets: [0.35], damage: 3, gcd: 1.1, cooldown: 5 }; if (isReady(actor, "shield_slam", at)) return { id: "shield_slam", duration: 0.6, impactOffsets: [0.35], damage: 2, gcd: 1.1, cooldown: 6 }; if (targetsInRange(source, context.targets, BRANN_MELEE_RANGE).length > 1 && isReady(actor, "sweeping_guard", at)) return { id: "sweeping_guard", duration: 0.65, impactOffsets: [0.4], damage: 2, gcd: 1.1, cooldown: 4 }; @@ -261,13 +286,23 @@ function applyStartCosts(actor: PartyCombatantState, spec: AbilitySpec, at: numb if (spec.id === "backstab") { actor.resource -= 25; actor.points = Math.max(0, actor.points - 3); } if (spec.id === "fan_of_blades") actor.resource -= 35; if (spec.id === "blade_flurry") { actor.resource -= 20; actor.bladeFlurryUntil = at + 8; } - if (spec.id === "bulwark_march") state.tankAura.expiresAt = at + 6; + if (spec.id === "bulwark_march") { + state.tankAura.expiresAt = at + 6; + state.tankAura.sourceId = actor.id; + } } -function startAction(actor: PartyCombatantState, spec: AbilitySpec, target: PartyCombatTarget, at: number, state: PartyCombatState) { +function startAction( + actor: PartyCombatantState, + spec: AbilitySpec, + target: PartyCombatTarget, + at: number, + state: PartyCombatState, + kit: PartyCombatKitId, +) { applyStartCosts(actor, spec, at, state); let multiplier = 1; - if (actor.id === "orin" && spec.damage > 0 && actor.overchargeStacks > 0) { + if (kit === "caster" && spec.damage > 0 && actor.overchargeStacks > 0) { multiplier = 1.25; actor.overchargeStacks -= 1; } @@ -301,9 +336,10 @@ function damageTarget( at: number, secondary: boolean, events: PartyDamageEvent[], + profileMultiplier = 1, ) { if (target.boss.hp <= 0 || amount <= 0) return; - const dealt = Math.min(target.boss.hp, amount * action.multiplier * PARTY_DAMAGE_SCALE); + const dealt = Math.min(target.boss.hp, amount * action.multiplier * profileMultiplier * PARTY_DAMAGE_SCALE); target.boss.hp -= dealt; actor.damageDone += dealt; events.push({ id: state.nextEventId++, at, sourceId: actor.id, abilityId: action.abilityId, targetInstanceId: target.instanceId, amount: dealt, secondary }); @@ -311,23 +347,47 @@ function damageTarget( function resolveImpact(state: PartyCombatState, actor: PartyCombatantState, action: PartyCombatAction, at: number, context: PartyCombatContext, targets: PartyCombatTarget[], events: PartyDamageEvent[]) { const source = context.positions[actor.id]; - const range = actor.id === "vale" ? VALE_MELEE_RANGE : actor.id === "brann" ? BRANN_MELEE_RANGE : Number.POSITIVE_INFINITY; + const kit = combatKit(actor.id, context.party); + const range = kit === "melee" ? VALE_MELEE_RANGE : kit === "tank" ? BRANN_MELEE_RANGE : Number.POSITIVE_INFINITY; let target = targets.find((entry) => entry.instanceId === action.targetInstanceId && entry.boss.hp > 0 && distance(source, entry.motion.position) <= range); target ??= targetFor(actor.id, context, range); if (!target) return; + const damageProfile = context.damageProfiles?.[actor.id] ?? { singleTarget: 1, areaDamage: 1 }; if (action.abilityId === "fan_of_blades" || action.abilityId === "sweeping_guard") { const radius = action.abilityId === "fan_of_blades" ? VALE_CLEAVE_RADIUS : BRANN_MELEE_RANGE; - for (const nearby of targetsInRange(source, targets, radius)) damageTarget(state, actor, action, nearby, action.baseDamage, at, nearby.instanceId !== target.instanceId, events); + for (const nearby of targetsInRange(source, targets, radius)) { + const secondary = nearby.instanceId !== target.instanceId; + damageTarget(state, actor, action, nearby, action.baseDamage, at, secondary, events, secondary ? damageProfile.areaDamage : damageProfile.singleTarget); + } return; } - damageTarget(state, actor, action, target, action.baseDamage, at, false, events); - const secondaryTargets = targetsInRange(source, targets, actor.id === "vale" ? VALE_CLEAVE_RADIUS : BRANN_MELEE_RANGE).filter((entry) => entry.instanceId !== target!.instanceId); + damageTarget(state, actor, action, target, action.baseDamage, at, false, events, damageProfile.singleTarget); + // Legacy encounters keep their established pacing. Run-provided profiles opt + // ranged/caster kits into the configurable splash geometry used by RPG mode. + const areaShape = context.damageProfiles ? AREA_DAMAGE_SHAPES[action.abilityId] : undefined; + if (areaShape) { + for (const secondary of livingTargets(targets)) { + if (secondary.instanceId === target.instanceId || distance(secondary.motion.position, target.motion.position) > areaShape.radius) continue; + damageTarget( + state, + actor, + action, + secondary, + action.baseDamage * areaShape.secondaryDamageRatio, + at, + true, + events, + damageProfile.areaDamage, + ); + } + } + const secondaryTargets = targetsInRange(source, targets, kit === "melee" ? VALE_CLEAVE_RADIUS : BRANN_MELEE_RANGE).filter((entry) => entry.instanceId !== target!.instanceId); if (action.abilityId === "twin_fang") { - for (const secondary of secondaryTargets) damageTarget(state, actor, action, secondary, 2, at, true, events); - } else if (actor.id === "vale" && actor.bladeFlurryUntil > at && !["fan_of_blades", "blade_flurry"].includes(action.abilityId)) { - for (const secondary of secondaryTargets) damageTarget(state, actor, action, secondary, action.baseDamage * 0.5, at, true, events); + for (const secondary of secondaryTargets) damageTarget(state, actor, action, secondary, 2, at, true, events, damageProfile.areaDamage); + } else if (kit === "melee" && actor.bladeFlurryUntil > at && !["fan_of_blades", "blade_flurry"].includes(action.abilityId)) { + for (const secondary of secondaryTargets) damageTarget(state, actor, action, secondary, action.baseDamage * 0.5, at, true, events, damageProfile.areaDamage); } } @@ -351,13 +411,14 @@ export function advancePartyCombat(source: PartyCombatState, context: PartyComba const actor = state.combatants[id]; const member = context.party.find((entry) => entry.id === id); if (!member || member.hp <= 0) continue; + const kit = combatKit(id, combatContext.party); if (member.knockedUntil > context.time) { actor.activeAction = null; actor.readyAt = Math.max(actor.readyAt, member.knockedUntil); continue; } - if (id === "vale") actor.resource = Math.min(100, actor.resource + 12 * elapsed); - if (id === "brann" && member.hp < actor.lastHp) actor.revengeReadyUntil = context.time + 5; + if (kit === "melee") actor.resource = Math.min(100, actor.resource + 12 * elapsed); + if (kit === "tank" && member.hp < actor.lastHp) actor.revengeReadyUntil = context.time + 5; actor.lastHp = member.hp; const isMoving = moving(id, combatContext); if (isMoving && actor.activeAction?.requiresStationary) { @@ -391,10 +452,10 @@ export function advancePartyCombat(source: PartyCombatState, context: PartyComba cooldown: baseSpec.cooldown === undefined ? undefined : baseSpec.cooldown * modifier.cooldown, } : baseSpec; if (!spec) { actor.readyAt = context.time + 0.1; break; } - const range = id === "vale" ? VALE_MELEE_RANGE : id === "brann" ? BRANN_MELEE_RANGE : Number.POSITIVE_INFINITY; + const range = kit === "melee" ? VALE_MELEE_RANGE : kit === "tank" ? BRANN_MELEE_RANGE : Number.POSITIVE_INFINITY; const target = targetFor(id, combatContext, range); if (!target) { actor.readyAt = context.time + 0.1; break; } - startAction(actor, spec, target, startAt, state); + startAction(actor, spec, target, startAt, state, kit); } } diff --git a/src/game/partyState.ts b/src/game/partyState.ts index 256e134..3b0818b 100644 --- a/src/game/partyState.ts +++ b/src/game/partyState.ts @@ -3,3 +3,13 @@ import type { PartyMember } from "./types"; export function isPartyWiped(party: readonly PartyMember[]) { return party.length > 0 && party.every((member) => member.hp <= 0); } + +export function areAllNonHealerAlliesDefeated(party: readonly PartyMember[]) { + let hasAlly = false; + for (const member of party) { + if (member.id === "aelia") continue; + hasAlly = true; + if (member.hp > 0) return false; + } + return hasAlly; +} diff --git a/src/game/performance.test.ts b/src/game/performance.test.ts index 582bb2c..920f100 100644 --- a/src/game/performance.test.ts +++ b/src/game/performance.test.ts @@ -1,13 +1,17 @@ import { describe, expect, it } from "vitest"; +import { AETHER_MAX_ENEMY_SHOTS, AETHER_MAX_PLAYER_SHOTS, AETHER_MAX_SHIPS } from "./aetherAssault"; import { useGameStore } from "./store"; describe("runtime performance budgets", () => { it.each([ - { label: "dual-boss", bossIds: ["emberfox", "sandglass-scorpion"] as const, maxElapsedMs: 3_500 }, - { label: "Rogue Trials trio", bossIds: ["emberfox", "sandglass-scorpion", "tempestscale-dragon"] as const, maxElapsedMs: 5_000 }, - ])("keeps ten minutes of $label simulation bounded", ({ bossIds, maxElapsedMs }) => { + { label: "dual-boss", bossIds: ["emberfox", "sandglass-scorpion"] as const, runMode: "encounter" as const, maxElapsedMs: 3_500 }, + { label: "Rogue Trials trio", bossIds: ["emberfox", "sandglass-scorpion", "tempestscale-dragon"] as const, runMode: "encounter" as const, maxElapsedMs: 5_000 }, + { label: "Hockey Healing", bossIds: ["emberfox", "sandglass-scorpion"] as const, runMode: "hockey-healing" as const, maxElapsedMs: 4_000 }, + { label: "Blockbreaker", bossIds: ["emberfox", "sandglass-scorpion"] as const, runMode: "blockbreaker" as const, maxElapsedMs: 5_000 }, + { label: "Aether Assault", bossIds: ["emberfox", "sandglass-scorpion"] as const, runMode: "aether-assault" as const, maxElapsedMs: 6_000 }, + ])("keeps ten minutes of $label simulation bounded", ({ bossIds, runMode, maxElapsedMs }) => { const store = useGameStore.getState(); - store.configureHealer("priest", "Perf", [], bossIds); + store.configureHealer("priest", "Perf", [], bossIds, runMode); store.startEncounter(); useGameStore.setState((state) => ({ boss: { ...state.boss, hp: 1_000_000_000, maxHp: 1_000_000_000 }, @@ -21,8 +25,22 @@ describe("runtime performance budgets", () => { let maxHazards = 0; let maxDamageEvents = 0; let maxCombatLog = 0; + let maxAetherShips = 0; + let maxAetherPlayerShots = 0; + let maxAetherEnemyShots = 0; const startedAt = performance.now(); for (let step = 0; step < 6_000; step += 1) { + if (runMode === "hockey-healing") { + const hockey = useGameStore.getState().hockey; + useGameStore.setState((state) => ({ + partyPositions: { ...state.partyPositions, aelia: [hockey.puckPosition[0], 8.5] }, + })); + } + if (runMode === "blockbreaker") { + useGameStore.setState((state) => ({ + blockbreaker: { ...state.blockbreaker, bricks: [] }, + })); + } useGameStore.getState().tick(0.1); const state = useGameStore.getState(); const hazards = state.bossMotion.hazards.length @@ -30,6 +48,9 @@ describe("runtime performance budgets", () => { maxHazards = Math.max(maxHazards, hazards); maxDamageEvents = Math.max(maxDamageEvents, state.partyDamageEvents.length); maxCombatLog = Math.max(maxCombatLog, state.combatLog.length); + maxAetherShips = Math.max(maxAetherShips, state.aetherAssault.ships.length); + maxAetherPlayerShots = Math.max(maxAetherPlayerShots, state.aetherAssault.playerShots.length); + maxAetherEnemyShots = Math.max(maxAetherEnemyShots, state.aetherAssault.enemyShots.length); } const elapsedMs = performance.now() - startedAt; const result = useGameStore.getState(); @@ -39,6 +60,54 @@ describe("runtime performance budgets", () => { expect(maxHazards).toBeLessThanOrEqual(64); expect(maxDamageEvents).toBeLessThanOrEqual(24); expect(maxCombatLog).toBeLessThanOrEqual(12); + expect(maxAetherShips).toBeLessThanOrEqual(AETHER_MAX_SHIPS); + expect(maxAetherPlayerShots).toBeLessThanOrEqual(AETHER_MAX_PLAYER_SHOTS); + expect(maxAetherEnemyShots).toBeLessThanOrEqual(AETHER_MAX_ENEMY_SHOTS); expect(elapsedMs).toBeLessThan(maxElapsedMs); }); + + it("keeps ten minutes of Healing Hockey PVP simulation bounded", () => { + const store = useGameStore.getState(); + store.configureHealer( + "priest", + "Perf", + [], + ["emberfox"], + "hockey-healing-pvp", + undefined, + "initiate", + { matchId: null, seed: 17, opponentName: "CPU Perf", role: "cpu" }, + ); + store.startEncounter(); + useGameStore.setState((state) => ({ + boss: { ...state.boss, hp: 1_000_000_000, maxHp: 1_000_000_000 }, + party: state.party.map((member) => ({ ...member, absorb: 1_000_000_000 })), + hockeyPvpOpponent: { + ...state.hockeyPvpOpponent, + boss: { ...state.hockeyPvpOpponent.boss, hp: 1_000_000_000, maxHp: 1_000_000_000 }, + party: state.hockeyPvpOpponent.party.map((member) => ({ ...member, absorb: 1_000_000_000 })), + }, + })); + + let maxHazards = 0; + let maxCombatLog = 0; + const startedAt = performance.now(); + for (let step = 0; step < 6_000; step += 1) { + useGameStore.getState().tick(0.1); + const state = useGameStore.getState(); + maxHazards = Math.max( + maxHazards, + state.bossMotion.hazards.length + state.hockeyPvpOpponent.bossMotion.hazards.length, + ); + maxCombatLog = Math.max(maxCombatLog, state.combatLog.length); + } + const elapsedMs = performance.now() - startedAt; + const result = useGameStore.getState(); + + expect(result.phase).toBe("combat"); + expect(result.time).toBeCloseTo(600, 5); + expect(maxHazards).toBeLessThanOrEqual(64); + expect(maxCombatLog).toBeLessThanOrEqual(12); + expect(elapsedMs).toBeLessThan(8_000); + }); }); diff --git a/src/game/progression/gear.ts b/src/game/progression/gear.ts index 0ea02be..4705d46 100644 --- a/src/game/progression/gear.ts +++ b/src/game/progression/gear.ts @@ -1,4 +1,5 @@ import type { BossGroupId } from "../bossCatalog"; +import { HEALER_CLASS_ORDER, isHealerClassId } from "../healers"; import type { HealerClassId, MemberId, RunBuffId } from "../types"; import { groupDrop, type DifficultySlug, type MaterialStack } from "./loot"; @@ -33,7 +34,7 @@ export interface GearUpgradeCost { quantity: number; } -export const GEAR_OWNER_ORDER: readonly GearOwnerId[] = ["priest", "druid", "shaman", "brann", "nia", "orin", "vale"]; +export const GEAR_OWNER_ORDER: readonly GearOwnerId[] = [...HEALER_CLASS_ORDER, "brann", "nia", "orin", "vale"]; export const GEAR_SLOT_ORDER: readonly GearSlotId[] = ["weapon", "helmet", "chest", "legs", "feet"]; export const MAX_GEAR_LEVEL: GearLevel = 10; @@ -41,6 +42,8 @@ export const GEAR_OWNER_LABELS: Record = { priest: "Priest", druid: "Druid", shaman: "Shaman", + paladin: "Paladin", + chronomancer: "Chronomancer", brann: "Brann · Knight", nia: "Nia · Ranger", orin: "Orin · Mage", @@ -79,6 +82,8 @@ const OWNER_RECIPE_SEEDS: Record = { priest: HEALER_RECIPES, druid: HEALER_RECIPES, shaman: HEALER_RECIPES, + paladin: HEALER_RECIPES, + chronomancer: HEALER_RECIPES, brann: { weapon: ["charge", "scuttle-burst"], helmet: ["slash-cross", "burrow-eruption"], @@ -110,7 +115,7 @@ const OWNER_RECIPE_SEEDS: Record = { }; function statFor(ownerId: GearOwnerId, slotId: GearSlotId): GearStatId { - if (slotId === "weapon") return ownerId === "priest" || ownerId === "druid" || ownerId === "shaman" ? "healingPower" : "damage"; + if (slotId === "weapon") return isHealerClassId(ownerId) ? "healingPower" : "damage"; if (slotId === "helmet") return ownerId === "brann" ? "stunResist" : "attackCooldown"; if (slotId === "chest") return "maxHealth"; if (slotId === "legs") return "moveSpeed"; diff --git a/src/game/progression/gearEffects.ts b/src/game/progression/gearEffects.ts index 641a854..8d75381 100644 --- a/src/game/progression/gearEffects.ts +++ b/src/game/progression/gearEffects.ts @@ -25,6 +25,16 @@ export const EMPTY_MEMBER_GEAR_MODIFIERS: MemberGearModifiers = { stunDuration: 1, }; +export function createBaseEncounterGearModifiers(): EncounterGearModifiers { + return { + aelia: { ...EMPTY_MEMBER_GEAR_MODIFIERS }, + brann: { ...EMPTY_MEMBER_GEAR_MODIFIERS }, + nia: { ...EMPTY_MEMBER_GEAR_MODIFIERS }, + orin: { ...EMPTY_MEMBER_GEAR_MODIFIERS }, + vale: { ...EMPTY_MEMBER_GEAR_MODIFIERS }, + }; +} + export function createEncounterGearModifiers(progress: GearProgress, healerClassId: HealerClassId): EncounterGearModifiers { return { aelia: modifiersForOwner(progress, healerClassId), diff --git a/src/game/progression/hunterStats.test.ts b/src/game/progression/hunterStats.test.ts index 01e03c9..d19c40b 100644 --- a/src/game/progression/hunterStats.test.ts +++ b/src/game/progression/hunterStats.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { highestEndlessBossKillsAfterDefeat, highestRoguelikeRoundAfterDefeat } from "./hunterStats"; +import { bestAetherAssaultRecord, bestBlockbreakerRecords, bestHockeyHealingRecord, highestEndlessBossKillsAfterDefeat, highestRoguelikeRoundAfterDefeat, hockeyPvpRecordAfterMatch } from "./hunterStats"; describe("roguelike hunter records", () => { it("records the reached defeat round without lowering a previous best", () => { @@ -26,3 +26,53 @@ describe("Rogue Trials endless hunter records", () => { expect(highestEndlessBossKillsAfterDefeat(4.9, 8.9)).toBe(8); }); }); + +describe("Hockey Healing records", () => { + it("ranks returns first and duration second", () => { + expect(bestHockeyHealingRecord(12, 90, 13, 70)).toEqual({ returns: 13, durationSeconds: 70 }); + expect(bestHockeyHealingRecord(13, 70, 13, 82)).toEqual({ returns: 13, durationSeconds: 82 }); + expect(bestHockeyHealingRecord(13, 82, 12, 120)).toEqual({ returns: 13, durationSeconds: 82 }); + }); + + it("normalizes invalid record values", () => { + expect(bestHockeyHealingRecord(Number.NaN, Number.NaN, 4.9, 12.5)).toEqual({ returns: 4, durationSeconds: 12.5 }); + }); +}); + +describe("Healing Hockey PVP records", () => { + it("increments wins and losses independently", () => { + expect(hockeyPvpRecordAfterMatch(3, 2, true)).toEqual({ wins: 4, losses: 2 }); + expect(hockeyPvpRecordAfterMatch(3, 2, false)).toEqual({ wins: 3, losses: 3 }); + }); +}); + +describe("Blockbreaker records", () => { + it("keeps bricks, duration, and score as independent lifetime highs", () => { + expect(bestBlockbreakerRecords(50, 120, 4_000, 60, 90, 3_500)).toEqual({ + bricks: 60, + durationSeconds: 120, + score: 4_000, + }); + }); + + it("normalizes invalid and fractional values", () => { + expect(bestBlockbreakerRecords(Number.NaN, Number.NaN, Number.NaN, 4.9, 12.5, 99.9)).toEqual({ + bricks: 4, + durationSeconds: 12.5, + score: 99, + }); + }); +}); + +describe("Aether Assault records", () => { + it("keeps wave and duration from the best-score record", () => { + expect(bestAetherAssaultRecord(10_000, 5, 120, 11_000, 4, 90)).toEqual({ score: 11_000, wave: 4, durationSeconds: 90 }); + expect(bestAetherAssaultRecord(11_000, 4, 90, 11_000, 6, 80)).toEqual({ score: 11_000, wave: 6, durationSeconds: 80 }); + expect(bestAetherAssaultRecord(11_000, 6, 80, 10_500, 9, 200)).toEqual({ score: 11_000, wave: 6, durationSeconds: 80 }); + }); + + it("uses duration only after score and wave ties", () => { + expect(bestAetherAssaultRecord(11_000, 6, 80, 11_000, 6, 95)).toEqual({ score: 11_000, wave: 6, durationSeconds: 95 }); + expect(bestAetherAssaultRecord(Number.NaN, Number.NaN, Number.NaN, 99.9, 3.9, 12.5)).toEqual({ score: 99, wave: 3, durationSeconds: 12.5 }); + }); +}); diff --git a/src/game/progression/hunterStats.ts b/src/game/progression/hunterStats.ts index 83e4809..486f7d5 100644 --- a/src/game/progression/hunterStats.ts +++ b/src/game/progression/hunterStats.ts @@ -9,3 +9,94 @@ export function highestEndlessBossKillsAfterDefeat(currentRecord: number, bossKi const normalizedKills = Math.max(0, Math.floor(Number(bossKills) || 0)); return Math.max(normalizedRecord, normalizedKills); } + +export interface HockeyHealingRecord { + returns: number; + durationSeconds: number; +} + +export function bestHockeyHealingRecord( + currentReturns: number, + currentDurationSeconds: number, + runReturns: number, + runDurationSeconds: number, +): HockeyHealingRecord { + const current: HockeyHealingRecord = { + returns: Math.max(0, Math.floor(Number(currentReturns) || 0)), + durationSeconds: Math.max(0, Number(currentDurationSeconds) || 0), + }; + const run: HockeyHealingRecord = { + returns: Math.max(0, Math.floor(Number(runReturns) || 0)), + durationSeconds: Math.max(0, Number(runDurationSeconds) || 0), + }; + return run.returns > current.returns || run.returns === current.returns && run.durationSeconds > current.durationSeconds + ? run + : current; +} + +export function hockeyPvpRecordAfterMatch(currentWins: number, currentLosses: number, won: boolean) { + const wins = Math.max(0, Math.floor(Number(currentWins) || 0)); + const losses = Math.max(0, Math.floor(Number(currentLosses) || 0)); + return won ? { wins: wins + 1, losses } : { wins, losses: losses + 1 }; +} + +export interface BlockbreakerRecords { + bricks: number; + durationSeconds: number; + score: number; +} + +export function bestBlockbreakerRecords( + currentBricks: number, + currentDurationSeconds: number, + currentScore: number, + runBricks: number, + runDurationSeconds: number, + runScore: number, +): BlockbreakerRecords { + return { + bricks: Math.max( + Math.max(0, Math.floor(Number(currentBricks) || 0)), + Math.max(0, Math.floor(Number(runBricks) || 0)), + ), + durationSeconds: Math.max( + Math.max(0, Number(currentDurationSeconds) || 0), + Math.max(0, Number(runDurationSeconds) || 0), + ), + score: Math.max( + Math.max(0, Math.floor(Number(currentScore) || 0)), + Math.max(0, Math.floor(Number(runScore) || 0)), + ), + }; +} + +export interface AetherAssaultRecord { + score: number; + wave: number; + durationSeconds: number; +} + +export function bestAetherAssaultRecord( + currentScore: number, + currentWave: number, + currentDurationSeconds: number, + runScore: number, + runWave: number, + runDurationSeconds: number, +): AetherAssaultRecord { + const current = { + score: Math.max(0, Math.floor(Number(currentScore) || 0)), + wave: Math.max(0, Math.floor(Number(currentWave) || 0)), + durationSeconds: Math.max(0, Number(currentDurationSeconds) || 0), + }; + const run = { + score: Math.max(0, Math.floor(Number(runScore) || 0)), + wave: Math.max(0, Math.floor(Number(runWave) || 0)), + durationSeconds: Math.max(0, Number(runDurationSeconds) || 0), + }; + return run.score > current.score + || run.score === current.score && run.wave > current.wave + || run.score === current.score && run.wave === current.wave && run.durationSeconds > current.durationSeconds + ? run + : current; +} diff --git a/src/game/progression/infusions.ts b/src/game/progression/infusions.ts index dca4261..9578478 100644 --- a/src/game/progression/infusions.ts +++ b/src/game/progression/infusions.ts @@ -1,4 +1,5 @@ import { RUN_BUFFS, RUN_BUFF_ORDER } from "../roguelike"; +import { isHealerClassId } from "../healers"; import type { BossId, RunBuffId } from "../types"; import { GEAR_RECIPES, @@ -52,6 +53,16 @@ const INFUSION_SEEDS: Record = { { id: "shaman-ancestral-surge", name: "Ancestral Surge", icon: "ϟ", description: "8% more healing power.", linkedBossId: "emberfox", effectKey: "healing-power" }, { id: "shaman-windwalk", name: "Windwalk", icon: "≋", description: "8% faster movement.", linkedBossId: "sandglass-scorpion", effectKey: "move-speed" }, ], + paladin: [ + { id: "paladin-sanctified-plate", name: "Sanctified Plate", icon: "▣", description: "8% more maximum health.", linkedBossId: "bristlequake-boar", effectKey: "max-health" }, + { id: "paladin-burning-oath", name: "Burning Oath", icon: "⚔", description: "8% more healing power.", linkedBossId: "emberfox", effectKey: "healing-power" }, + { id: "paladin-crusaders-step", name: "Crusader's Step", icon: "⌁", description: "8% faster movement.", linkedBossId: "sandglass-scorpion", effectKey: "move-speed" }, + ], + chronomancer: [ + { id: "chronomancer-stolen-second", name: "Stolen Second", icon: "◷", description: "8% faster cooldowns.", linkedBossId: "sandglass-scorpion", effectKey: "cooldown" }, + { id: "chronomancer-preserved-future", name: "Preserved Future", icon: "∞", description: "8% more healing power.", linkedBossId: "tempestscale-dragon", effectKey: "healing-power" }, + { id: "chronomancer-paradox-ward", name: "Paradox Ward", icon: "◇", description: "15% less hazard damage.", linkedBossId: "broodfang-spider", effectKey: "hazard-shield" }, + ], brann: [ { id: "brann-unbreakable", name: "Unbreakable", icon: "▣", description: "Immune to stuns.", linkedBossId: "bristlequake-boar", effectKey: "stun-immune" }, { id: "brann-bulwark", name: "Bulwark", icon: "⬡", description: "8% more maximum health.", linkedBossId: "bulldrome", effectKey: "max-health" }, @@ -130,7 +141,7 @@ export function equipActiveInfusion( } export function equipPassiveInfusion(progress: GearProgress, ownerId: GearOwnerId, passiveId: RunBuffId): GearProgress { - if (ownerId !== "priest" && ownerId !== "druid" && ownerId !== "shaman") throw new Error("Passive infusions belong to healer gear."); + if (!isHealerClassId(ownerId)) throw new Error("Passive infusions belong to healer gear."); if (!RUN_BUFF_ORDER.includes(passiveId)) throw new Error("Unknown passive infusion."); if (!passiveInfusionUnlocked(progress)) throw new Error(`Raise any gear slot to +${PASSIVE_INFUSION_MIN_GEAR_LEVEL}.`); return { ...progress, [ownerId]: { ...progress[ownerId], passiveInfusionId: passiveId } }; @@ -141,6 +152,6 @@ export function normalizeActiveInfusionId(ownerId: GearOwnerId, value: unknown): } export function normalizePassiveInfusionId(ownerId: GearOwnerId, value: unknown): RunBuffId | null { - if (ownerId !== "priest" && ownerId !== "druid" && ownerId !== "shaman") return null; + if (!isHealerClassId(ownerId)) return null; return typeof value === "string" && RUN_BUFF_ORDER.includes(value as RunBuffId) ? value as RunBuffId : null; } diff --git a/src/game/roguelike.ts b/src/game/roguelike.ts index 14a0b85..03be434 100644 --- a/src/game/roguelike.ts +++ b/src/game/roguelike.ts @@ -1,6 +1,6 @@ import { AVAILABLE_BOSS_IDS } from "./bossCatalog"; import { canAddBossToEncounter } from "./bossSelection"; -import type { AbilityId, BossId, RunBuffId, RunBuffRanks } from "./types"; +import type { AbilitySlotId, BossId, RunBuffId, RunBuffRanks } from "./types"; export type RunBuffEffectKind = | "extra-target" @@ -18,7 +18,7 @@ export type RunBuffEffectKind = export interface RunBuffDefinition { id: RunBuffId; - abilityId: AbilityId; + abilitySlotId: AbilitySlotId; effectKind: RunBuffEffectKind; name: string; icon: string; @@ -73,7 +73,7 @@ export const RUN_BUFF_ORDER: readonly RunBuffId[] = [ const buff = ( id: RunBuffId, - abilityId: AbilityId, + abilitySlotId: AbilitySlotId, effectKind: RunBuffEffectKind, name: string, icon: string, @@ -81,27 +81,27 @@ const buff = ( detail: string, accent: string, maxRank: 1 | 3 = 3, -): RunBuffDefinition => ({ id, abilityId, effectKind, name, icon, summary, detail, accent, maxRank, infusionEligible: true }); +): RunBuffDefinition => ({ id, abilitySlotId, effectKind, name, icon, summary, detail, accent, maxRank, infusionEligible: true }); export const RUN_BUFFS: Record = { - "mend-echo": buff("mend-echo", "mend", "extra-target", "Echoing", "+", "+1 secondary ally", "Mend heals another injured ally for 50% power per rank.", "#f2d690"), - "mend-efficiency": buff("mend-efficiency", "mend", "mana-cost", "Efficient", "▽", "−25% mana cost", "Mend mana cost is multiplied by 0.75 per rank, rounded up.", "#72c8ef"), - "mend-cast-speed": buff("mend-cast-speed", "mend", "cast-time", "Swift", "»", "−25% cast time", "Mend cast time is multiplied by 0.75 per rank.", "#d8b4ff"), - "renew-spread": buff("renew-spread", "renew", "extra-target", "Spreading", "✣", "+1 injured ally", "Direct Renew casts affect another injured ally per rank.", "#71df9c"), - "renew-duration": buff("renew-duration", "renew", "duration", "Enduring", "◷", "+2s duration", "Every Renew effect lasts 2 seconds longer per rank.", "#83d9aa"), - "renew-potency": buff("renew-potency", "renew", "healing", "Potent", "↑", "+20% tick healing", "Every Renew tick heals 20% more per rank.", "#a8e875"), - "shield-echo": buff("shield-echo", "shield", "extra-target", "Echoing Aegis", "◇", "+1 secondary ally", "Shield another injured ally for 50% power per rank.", "#75c9ff"), - "shield-potency": buff("shield-potency", "shield", "absorb", "Reinforced Aegis", "⬡", "+25% absorption", "All healer-created absorption is 25% stronger per rank.", "#61b9ee"), - "shield-guard": buff("shield-guard", "shield", "damage-reduction", "Guardian Aegis", "▣", "−8% shielded damage", "Targets with absorption take 8% less incoming damage per rank.", "#8baeff"), - "purify-renew": buff("purify-renew", "purify", "trigger-renew", "Cleansing Renewal", "✧", "Purify applies Renew", "Every ally cleansed by Purify also gains Renew.", "#b58cff", 1), - "purify-shield": buff("purify-shield", "purify", "trigger-shield", "Purifying Ward", "◈", "Purify grants 50% Shield", "Every ally cleansed by Purify gains half-strength absorption.", "#9f9aff", 1), - "purify-chain": buff("purify-chain", "purify", "chain-cleanse", "Mass Purification", "✦", "+1 cleansed ally", "Purify also cleanses the most injured other debuffed ally.", "#d4a7ff", 1), - "radiance-cooldown": buff("radiance-cooldown", "radiance", "cooldown", "Quickened Radiance", "☀", "−20% cooldown", "Radiance cooldown is multiplied by 0.8 per rank.", "#ffd66b"), - "radiance-renew": buff("radiance-renew", "radiance", "trigger-renew", "Radiant Renewal", "❈", "Radiance applies Renew", "Radiance applies Renew to every living party member.", "#d4e978", 1), - "radiance-shield": buff("radiance-shield", "radiance", "absorb", "Radiant Aegis", "◎", "+9 party absorption", "Radiance grants 9 base absorption to every living ally per rank.", "#ffe58c"), - "barrier-cooldown": buff("barrier-cooldown", "barrier", "cooldown", "Hallowed Ground", "◉", "−20% cooldown", "Barrier cooldown is multiplied by 0.8 per rank.", "#e7cb62"), - "barrier-duration": buff("barrier-duration", "barrier", "duration", "Lingering Barrier", "⌛", "+2s duration", "Barrier remains active 2 seconds longer per rank.", "#cdbd69"), - "barrier-regen": buff("barrier-regen", "barrier", "barrier-healing", "Restorative Ground", "✚", "+3 healing per second", "Living allies inside Barrier heal every second per rank.", "#84d69a"), + "mend-echo": buff("mend-echo", "ability1", "extra-target", "Echoing", "+", "+1 secondary ally", "Ability 1 heals another injured ally for 50% power per rank.", "#f2d690"), + "mend-efficiency": buff("mend-efficiency", "ability1", "mana-cost", "Efficient", "▽", "−25% mana cost", "Ability 1 mana cost is multiplied by 0.75 per rank, rounded up.", "#72c8ef"), + "mend-cast-speed": buff("mend-cast-speed", "ability1", "cast-time", "Swift", "»", "−25% cast time", "Ability 1 cast time is multiplied by 0.75 per rank.", "#d8b4ff"), + "renew-spread": buff("renew-spread", "ability2", "extra-target", "Spreading", "✣", "+1 injured ally", "Direct Ability 2 casts affect another injured ally per rank.", "#71df9c"), + "renew-duration": buff("renew-duration", "ability2", "duration", "Enduring", "◷", "+2s duration", "Ability 2 periodic healing lasts 2 seconds longer per rank.", "#83d9aa"), + "renew-potency": buff("renew-potency", "ability2", "healing", "Potent", "↑", "+20% tick healing", "Ability 2 periodic healing is 20% stronger per rank.", "#a8e875"), + "shield-echo": buff("shield-echo", "ability3", "extra-target", "Echoing Ward", "◇", "+1 secondary ally", "Ability 3 affects another injured ally at 50% power per rank.", "#75c9ff"), + "shield-potency": buff("shield-potency", "ability3", "absorb", "Reinforced Ward", "⬡", "+25% ward power", "Ability 3 healing or absorption is 25% stronger per rank.", "#61b9ee"), + "shield-guard": buff("shield-guard", "ability3", "damage-reduction", "Guardian Ward", "▣", "−8% warded damage", "Targets protected by Ability 3 take 8% less incoming damage per rank.", "#8baeff"), + "purify-renew": buff("purify-renew", "ability4", "trigger-renew", "Cleansing Renewal", "✧", "Cleanse applies Ability 2", "Every ally cleansed by Ability 4 also gains Ability 2's healing effect.", "#b58cff", 1), + "purify-shield": buff("purify-shield", "ability4", "trigger-shield", "Purifying Ward", "◈", "Cleanse grants Ability 3", "Every ally cleansed by Ability 4 gains a half-strength Ability 3 effect.", "#9f9aff", 1), + "purify-chain": buff("purify-chain", "ability4", "chain-cleanse", "Mass Purification", "✦", "+1 cleansed ally", "Ability 4 also cleanses the most injured other debuffed ally.", "#d4a7ff", 1), + "radiance-cooldown": buff("radiance-cooldown", "ability5", "cooldown", "Quickened", "☀", "−20% cooldown", "Ability 5 cooldown is multiplied by 0.8 per rank.", "#ffd66b"), + "radiance-renew": buff("radiance-renew", "ability5", "trigger-renew", "Restorative Wave", "❈", "Ability 5 applies Ability 2", "Ability 5 applies Ability 2's healing effect to every living party member.", "#d4e978", 1), + "radiance-shield": buff("radiance-shield", "ability5", "absorb", "Warding Wave", "◎", "+9 party ward", "Ability 5 grants 9 base healing or absorption to every living ally per rank.", "#ffe58c"), + "barrier-cooldown": buff("barrier-cooldown", "ability6", "cooldown", "Quickened Mastery", "◉", "−20% cooldown", "Ability 6 cooldown is multiplied by 0.8 per rank.", "#e7cb62"), + "barrier-duration": buff("barrier-duration", "ability6", "duration", "Lingering Mastery", "⌛", "+2s duration", "Ability 6 remains active 2 seconds longer per rank.", "#cdbd69"), + "barrier-regen": buff("barrier-regen", "ability6", "barrier-healing", "Restorative Mastery", "✚", "+3 healing per second", "Ability 6 adds 3 party healing per second per rank.", "#84d69a"), }; export function runBuffRank(ranks: RunBuffRanks, buffId: RunBuffId): number { @@ -160,44 +160,53 @@ export function compileRunModifiers(ranks: RunBuffRanks, passiveInfusionId: RunB }; } -export function runAbilityManaCost(abilityId: AbilityId, baseCost: number, modifiers: CompiledRunModifiers): number { +export function runAbilityManaCost(abilitySlotId: AbilitySlotId, baseCost: number, modifiers: CompiledRunModifiers): number { if (baseCost <= 0) return 0; - const multiplier = abilityId === "mend" ? modifiers.mendManaMultiplier : 1; + const multiplier = abilitySlotId === "ability1" ? modifiers.mendManaMultiplier : 1; return Math.max(1, Math.ceil(baseCost * multiplier)); } -export function runAbilityCastTime(abilityId: AbilityId, baseCastTime: number, modifiers: CompiledRunModifiers): number { - return abilityId === "mend" ? baseCastTime * modifiers.mendCastTimeMultiplier : baseCastTime; +export function runAbilityCastTime(abilitySlotId: AbilitySlotId, baseCastTime: number, modifiers: CompiledRunModifiers): number { + return abilitySlotId === "ability1" ? baseCastTime * modifiers.mendCastTimeMultiplier : baseCastTime; } -export function runAbilityCooldown(abilityId: AbilityId, baseCooldown: number, modifiers: CompiledRunModifiers): number { - if (abilityId === "radiance") return baseCooldown * modifiers.radianceCooldownMultiplier; - if (abilityId === "barrier") return baseCooldown * modifiers.barrierCooldownMultiplier; +export function runAbilityCooldown(abilitySlotId: AbilitySlotId, baseCooldown: number, modifiers: CompiledRunModifiers): number { + if (abilitySlotId === "ability5") return baseCooldown * modifiers.radianceCooldownMultiplier; + if (abilitySlotId === "ability6") return baseCooldown * modifiers.barrierCooldownMultiplier; return baseCooldown; } -export function formatRunBuffEffect(buffId: RunBuffId, requestedRank: number): string { +export function formatRunBuffEffect(buffId: RunBuffId, requestedRank: number, abilityName?: string): string { const rank = Math.max(1, Math.min(RUN_BUFFS[buffId].maxRank, requestedRank)); const reduced = (multiplier: number) => `${Math.round((1 - multiplier ** rank) * 100)}% less`; + const defaultAbilityNames: Record = { + ability1: "Mend", + ability2: "Renew", + ability3: "Shield", + ability4: "Purify", + ability5: "Radiance", + ability6: "Barrier", + }; + const name = abilityName ?? defaultAbilityNames[RUN_BUFFS[buffId].abilitySlotId]; switch (buffId) { case "mend-echo": return `${rank} secondary ${rank === 1 ? "ally" : "allies"} at 50% healing`; - case "mend-efficiency": return `${reduced(0.75)} Mend mana cost`; - case "mend-cast-speed": return `${reduced(0.75)} Mend cast time`; - case "renew-spread": return `${rank} additional Renew ${rank === 1 ? "target" : "targets"}`; - case "renew-duration": return `+${rank * 2}s Renew duration`; - case "renew-potency": return `+${rank * 20}% Renew tick healing`; - case "shield-echo": return `${rank} secondary Shield ${rank === 1 ? "target" : "targets"} at 50% power`; - case "shield-potency": return `+${rank * 25}% healer absorption`; - case "shield-guard": return `${rank * 8}% less damage while shielded`; - case "purify-renew": return "Purify applies Renew"; - case "purify-shield": return "Purify grants 50% Shield"; - case "purify-chain": return "Purify cleanses one additional ally"; - case "radiance-cooldown": return `${reduced(0.8)} Radiance cooldown`; - case "radiance-renew": return "Radiance applies Renew party-wide"; - case "radiance-shield": return `+${rank * 9} base party absorption`; - case "barrier-cooldown": return `${reduced(0.8)} Barrier cooldown`; - case "barrier-duration": return `+${rank * 2}s Barrier duration`; - case "barrier-regen": return `${rank * 3} Barrier healing per second`; + case "mend-efficiency": return `${reduced(0.75)} ${name} mana cost`; + case "mend-cast-speed": return `${reduced(0.75)} ${name} cast time`; + case "renew-spread": return `${rank} additional ${name} ${rank === 1 ? "target" : "targets"}`; + case "renew-duration": return `+${rank * 2}s ${name} duration`; + case "renew-potency": return `+${rank * 20}% ${name} tick healing`; + case "shield-echo": return `${rank} secondary ${name} ${rank === 1 ? "target" : "targets"} at 50% power`; + case "shield-potency": return `+${rank * 25}% ${name} power`; + case "shield-guard": return `${rank * 8}% less damage while protected by ${name}`; + case "purify-renew": return `${name} applies periodic healing`; + case "purify-shield": return `${name} grants a 50% ward`; + case "purify-chain": return `${name} cleanses one additional ally`; + case "radiance-cooldown": return `${reduced(0.8)} ${name} cooldown`; + case "radiance-renew": return `${name} spreads periodic healing party-wide`; + case "radiance-shield": return `+${rank * 9} base party healing or absorption`; + case "barrier-cooldown": return `${reduced(0.8)} ${name} cooldown`; + case "barrier-duration": return `+${rank * 2}s ${name} duration`; + case "barrier-regen": return `${rank * 3} ${name} healing per second`; } } diff --git a/src/game/rpgRoguelike/challenges.ts b/src/game/rpgRoguelike/challenges.ts new file mode 100644 index 0000000..b3bee78 --- /dev/null +++ b/src/game/rpgRoguelike/challenges.ts @@ -0,0 +1,100 @@ +import { randomInt } from "./random"; +import type { + ChallengeId, + ChallengeMetric, + ChallengeMetrics, + ChallengeObjective, + ChallengeResult, + RandomState, +} from "./types"; + +export interface ChallengeDefinition { + readonly id: ChallengeId; + readonly name: string; + readonly metric: ChallengeMetric; + readonly targets: readonly number[]; + readonly baseCurrency: number; +} + +export const CHALLENGE_REGISTRY: Record = { + blockbreaker: { + id: "blockbreaker", + name: "Brickbreaker Trial", + metric: "bricksBroken", + targets: [30, 45, 60], + baseCurrency: 24, + }, + hockey: { + id: "hockey", + name: "Hockey Shutout", + metric: "bossKills", + targets: [1, 2, 3], + baseCurrency: 28, + }, + "aether-assault": { + id: "aether-assault", + name: "Aether Assault", + metric: "kills", + targets: [20, 35, 50], + baseCurrency: 24, + }, +}; + +export const CHALLENGE_ORDER = Object.keys(CHALLENGE_REGISTRY) as ChallengeId[]; + +export function emptyChallengeMetrics(): ChallengeMetrics { + return { bricksBroken: 0, bossKills: 0, kills: 0 }; +} + +export function emptyChallengeCounts(): Record { + return { blockbreaker: 0, hockey: 0, "aether-assault": 0 }; +} + +export function challengeObjective(challengeId: ChallengeId, priorOccurrences: number): ChallengeObjective { + const definition = CHALLENGE_REGISTRY[challengeId]; + const repeatIndex = Math.max(0, Math.floor(priorOccurrences)); + const tier = Math.min(repeatIndex, definition.targets.length - 1); + return { + challengeId, + name: definition.name, + metric: definition.metric, + target: definition.targets[tier], + repeatIndex, + rewardCurrency: definition.baseCurrency + tier * 8, + chestQualityBonus: tier >= 2 ? 2 : 1, + }; +} + +export function selectChallenge( + source: RandomState, + counts: Readonly>, +): { + objective: ChallengeObjective; + counts: Record; + random: RandomState; +} { + const sampled = randomInt(source, CHALLENGE_ORDER.length); + const challengeId = CHALLENGE_ORDER[sampled.value]; + const priorOccurrences = counts[challengeId] ?? 0; + return { + objective: challengeObjective(challengeId, priorOccurrences), + counts: { ...counts, [challengeId]: priorOccurrences + 1 }, + random: sampled.random, + }; +} + +export function mergeChallengeMetrics( + current: ChallengeMetrics, + update: Partial, +): ChallengeMetrics { + return { + bricksBroken: Math.max(0, Math.floor(update.bricksBroken ?? current.bricksBroken)), + bossKills: Math.max(0, Math.floor(update.bossKills ?? current.bossKills)), + kills: Math.max(0, Math.floor(update.kills ?? current.kills)), + }; +} + +export function evaluateChallenge(objective: ChallengeObjective, metrics: ChallengeMetrics): ChallengeResult { + return { objective, metrics, succeeded: metrics[objective.metric] >= objective.target }; +} + diff --git a/src/game/rpgRoguelike/combatAdapter.test.ts b/src/game/rpgRoguelike/combatAdapter.test.ts new file mode 100644 index 0000000..6374e4f --- /dev/null +++ b/src/game/rpgRoguelike/combatAdapter.test.ts @@ -0,0 +1,168 @@ +import { describe, expect, it } from "vitest"; +import { assignRosterToCombatSlots } from "./party"; +import { createRunGearItem } from "./rewards"; +import type { + CombatPartyAssignment, + PartyArchetypeId, + PartyRosterMember, + RunEquipment, +} from "./types"; +import { + createRpgDamageProfiles, + extractRpgPartyVitals, + normalizedSpellRank, + projectRpgCombat, + runGearMultipliers, + spellRankCooldownMultiplier, + spellRankPowerMultiplier, +} from "./combatAdapter"; + +function member( + instanceId: string, + archetypeId: PartyArchetypeId, + overrides: Partial = {}, +): PartyRosterMember { + return { + candidateId: `candidate-${instanceId}`, + instanceId, + name: instanceId, + archetypeId, + classId: "warrior", + className: "Warrior", + role: "Damage", + rarity: "green", + stats: { maxHp: 100, singleTarget: 1, areaDamage: 1, defense: 1, moveSpeed: 1 }, + traitIds: [], + color: "#fff", + hp: 100, + ...overrides, + }; +} + +function draftedRoster(): PartyRosterMember[] { + return [ + member("tank-instance", "warrior-tank", { + role: "Tank", + stats: { maxHp: 145, singleTarget: 0.8, areaDamage: 1.02, defense: 1.14, moveSpeed: 0.96 }, + hp: 72.5, + }), + member("warlock-instance", "warlock-damage", { + classId: "warlock", + className: "Warlock", + stats: { maxHp: 84, singleTarget: 1.24, areaDamage: 0.8, defense: 0.84, moveSpeed: 0.94 }, + hp: 84, + }), + member("monk-instance", "monk-damage", { + classId: "monk", + className: "Monk", + stats: { maxHp: 82, singleTarget: 1.28, areaDamage: 0.76, defense: 0.82, moveSpeed: 1.22 }, + hp: 82, + }), + member("warrior-instance", "warrior-damage", { + stats: { maxHp: 112, singleTarget: 1.02, areaDamage: 1.28, defense: 1, moveSpeed: 1 }, + hp: 112, + }), + ]; +} + +function runEquipment(): RunEquipment { + return { + player: { + weapon: createRunGearItem("player-weapon", "player", "weapon", 4), + armor: createRunGearItem("player-armor", "player", "armor", 1), + trinket: createRunGearItem("player-trinket", "player", "trinket", 1), + }, + "tank-instance": { + weapon: createRunGearItem("tank-weapon", "tank-instance", "weapon", 1), + armor: createRunGearItem("tank-armor", "tank-instance", "armor", 2), + trinket: createRunGearItem("tank-trinket", "tank-instance", "trinket", 2), + }, + }; +} + +describe("RPG roguelike combat adapter", () => { + it("projects drafted identities into stable combat slots with run-only gear", () => { + const roster = draftedRoster(); + const projection = projectRpgCombat( + { roster, equipment: runEquipment() }, + { healerClassId: "druid", playerName: "Willow" }, + ); + + expect(projection.party.map((entry) => entry.id)).toEqual(["aelia", "brann", "nia", "orin", "vale"]); + expect(projection.party[0]).toMatchObject({ name: "Willow", className: "Restoration Druid", maxHp: 108, hp: 108 }); + + const tank = projection.party[1]; + expect(tank).toMatchObject({ + id: "brann", + name: "tank-instance", + role: "Tank", + maxHp: 162, + hp: 81, + runProfile: { + instanceId: "tank-instance", + combatProfileId: "warrior-tank", + combatKitId: "tank", + tier: "green", + visualArchetype: "knight", + }, + }); + expect(projection.party[2].runProfile?.visualArchetype).toBe("mage"); + expect(projection.party[3].runProfile?.visualArchetype).toBe("rogue"); + expect(projection.gearModifiers.brann.damage).toBeCloseTo(1.08); + expect(projection.gearModifiers.brann.cooldown).toBeCloseTo(1 / 1.06); + expect(projection.gearModifiers.aelia.healingPower).toBeCloseTo(1.2); + expect(projection.gearModifiers.aelia.cooldown).toBeCloseTo(1 / 1.04); + }); + + it("keeps class damage identity and archetype kits independent from stable slots", () => { + const projection = projectRpgCombat( + { roster: draftedRoster(), equipment: runEquipment() }, + { healerClassId: "priest", playerName: "Aelia" }, + ); + expect(projection.party.slice(1).map((member) => member.runProfile?.combatKitId)).toEqual([ + "tank", "ranged", "melee", "melee", + ]); + const profiles = createRpgDamageProfiles(assignRosterToCombatSlots(draftedRoster())); + expect(profiles.brann).toEqual({ singleTarget: 0.8, areaDamage: 1.02 }); + expect(profiles.nia).toEqual({ singleTarget: 1.24, areaDamage: 0.8 }); + expect(profiles.orin.singleTarget).toBeGreaterThan(profiles.vale.singleTarget); + expect(profiles.vale.areaDamage).toBeGreaterThan(profiles.vale.singleTarget); + + const empty: CombatPartyAssignment = { brann: null, nia: null, orin: null, vale: null }; + expect(createRpgDamageProfiles(empty).brann).toEqual({ singleTarget: 0, areaDamage: 0 }); + }); + + it("extracts gear-independent persistent health by drafted instance id", () => { + const roster = draftedRoster(); + const combatParty = assignRosterToCombatSlots(roster); + const projection = projectRpgCombat( + { roster, equipment: runEquipment() }, + { healerClassId: "priest", playerName: "Aelia" }, + ); + const damaged = projection.party.map((entry) => entry.id === "brann" + ? { ...entry, hp: entry.maxHp * 0.25 } + : entry); + + const vitals = extractRpgPartyVitals(damaged, combatParty); + expect(vitals[0]).toEqual({ instanceId: "tank-instance", hp: 36.25 }); + expect(vitals.map((entry) => entry.instanceId)).toEqual([ + "tank-instance", + "warlock-instance", + "monk-instance", + "warrior-instance", + ]); + }); + + it("uses bounded reciprocal haste and bounded spell-rank scaling", () => { + const gear = runGearMultipliers(runEquipment(), "tank-instance"); + expect(gear).toEqual({ maxHealth: 1.12, damage: 1.08, cooldown: 1 / 1.06 }); + + const ranks = { "priest-mend": 3, "druid-regrowth": 99, "shaman-riptide": -4 } as const; + expect(normalizedSpellRank(ranks, "priest-mend")).toBe(3); + expect(normalizedSpellRank(ranks, "druid-regrowth")).toBe(5); + expect(normalizedSpellRank(ranks, "shaman-riptide")).toBe(0); + expect(spellRankPowerMultiplier(ranks, "priest-mend")).toBeCloseTo(1.36); + expect(spellRankCooldownMultiplier(ranks, "priest-mend")).toBeCloseTo(0.85); + expect(spellRankCooldownMultiplier(ranks, "druid-regrowth")).toBeCloseTo(0.75); + }); +}); diff --git a/src/game/rpgRoguelike/combatAdapter.ts b/src/game/rpgRoguelike/combatAdapter.ts new file mode 100644 index 0000000..9e425b9 --- /dev/null +++ b/src/game/rpgRoguelike/combatAdapter.ts @@ -0,0 +1,264 @@ +import { freshParty } from "../data"; +import type { AiCombatantId } from "../partyCombat"; +import { + EMPTY_MEMBER_GEAR_MODIFIERS, + type EncounterGearModifiers, + type MemberGearModifiers, +} from "../progression/gearEffects"; +import type { HealerAbilityId, HealerClassId, PartyCombatKitId, PartyMember } from "../types"; +import { assignRosterToCombatSlots } from "./party"; +import type { + CombatPartyAssignment, + PartyArchetypeId, + PartyRosterMember, + PartyVitalUpdate, + RpgRoguelikeRunState, + RunEquipment, + RunGearOwnerId, + RunGearStatId, +} from "./types"; +import { MAX_SPELL_RANK } from "./types"; + +export const RPG_COMBAT_SLOT_IDS: readonly AiCombatantId[] = ["brann", "nia", "orin", "vale"]; + +export const SPELL_POWER_PER_RANK = 0.12; +export const SPELL_COOLDOWN_REDUCTION_PER_RANK = 0.05; + +export type RpgPartyDamageProfiles = Record; + +export interface RpgCombatProjection { + /** Existing combat systems keep their stable ids; runProfile carries drafted identity. */ + readonly party: PartyMember[]; + readonly gearModifiers: EncounterGearModifiers; + readonly damageProfiles: RpgPartyDamageProfiles; +} + +export interface ProjectRpgCombatOptions { + readonly healerClassId: HealerClassId; + readonly playerName: string; +} + +type CombatProjectionSource = Pick; + +const PERCENT_SCALE = 100; + +function clamp(value: number, minimum: number, maximum: number): number { + return Math.max(minimum, Math.min(maximum, value)); +} + +function normalizedPercent(value: number): number { + return Math.max(0, Number.isFinite(value) ? value : 0) / PERCENT_SCALE; +} + +function ownerGearStats(equipment: RunEquipment, ownerId: RunGearOwnerId): Record { + const totals: Record = { damage: 0, maxHealth: 0, haste: 0 }; + for (const item of Object.values(equipment[ownerId] ?? {})) { + if (item) totals[item.statId] += normalizedPercent(item.statValue); + } + return totals; +} + +/** + * Converts additive percentage-point run gear into simulation multipliers. + * Haste uses reciprocal scaling so future gear tiers cannot create zero cooldowns. + */ +export function runGearMultipliers( + equipment: RunEquipment, + ownerId: RunGearOwnerId, +): { readonly maxHealth: number; readonly damage: number; readonly cooldown: number } { + const stats = ownerGearStats(equipment, ownerId); + return { + maxHealth: 1 + stats.maxHealth, + damage: 1 + stats.damage, + cooldown: 1 / (1 + stats.haste), + }; +} + +function visualArchetype(archetypeId: PartyArchetypeId): NonNullable["visualArchetype"] { + if (archetypeId === "knight-tank" || archetypeId.startsWith("warrior-")) return "knight"; + if (archetypeId === "ranger-damage") return "ranger"; + if (archetypeId === "mage-damage" || archetypeId === "warlock-damage") return "mage"; + return "rogue"; +} + +export function combatKitForRosterMember(member: PartyRosterMember): PartyCombatKitId { + if (member.role === "Tank") return "tank"; + if (member.archetypeId === "mage-damage") return "caster"; + if (member.archetypeId === "ranger-damage" || member.archetypeId === "warlock-damage") return "ranged"; + return "melee"; +} + +function memberModifiers(member: PartyRosterMember | null, equipment: RunEquipment): MemberGearModifiers { + if (!member) return { ...EMPTY_MEMBER_GEAR_MODIFIERS }; + const gear = runGearMultipliers(equipment, member.instanceId); + return { + ...EMPTY_MEMBER_GEAR_MODIFIERS, + maxHealth: gear.maxHealth, + moveSpeed: member.stats.moveSpeed, + damage: gear.damage, + cooldown: gear.cooldown, + // Defense has no renderer dependency. Existing combat applies this field to + // hazards; other damage kinds can consume it when their shared API supports it. + hazardDamageTaken: clamp(1 / Math.max(0.01, member.stats.defense), 0.65, 1.25), + }; +} + +function healerModifiers(equipment: RunEquipment): MemberGearModifiers { + const gear = runGearMultipliers(equipment, "player"); + return { + ...EMPTY_MEMBER_GEAR_MODIFIERS, + maxHealth: gear.maxHealth, + healingPower: gear.damage, + cooldown: gear.cooldown, + }; +} + +/** Run-only player gear treats weapon damage as healing power. */ +export function createRpgHealerModifiers(equipment: RunEquipment): MemberGearModifiers { + return healerModifiers(equipment); +} + +function encounterGearModifiers( + combatParty: CombatPartyAssignment, + equipment: RunEquipment, +): EncounterGearModifiers { + return { + aelia: healerModifiers(equipment), + brann: memberModifiers(combatParty.brann, equipment), + nia: memberModifiers(combatParty.nia, equipment), + orin: memberModifiers(combatParty.orin, equipment), + vale: memberModifiers(combatParty.vale, equipment), + }; +} + +export function createRpgEncounterGearModifiers(source: CombatProjectionSource): EncounterGearModifiers { + return encounterGearModifiers(assignRosterToCombatSlots(source.roster), source.equipment); +} + +export function createRpgDamageProfiles(combatParty: CombatPartyAssignment): RpgPartyDamageProfiles { + return { + brann: combatParty.brann + ? { singleTarget: combatParty.brann.stats.singleTarget, areaDamage: combatParty.brann.stats.areaDamage } + : { singleTarget: 0, areaDamage: 0 }, + nia: combatParty.nia + ? { singleTarget: combatParty.nia.stats.singleTarget, areaDamage: combatParty.nia.stats.areaDamage } + : { singleTarget: 0, areaDamage: 0 }, + orin: combatParty.orin + ? { singleTarget: combatParty.orin.stats.singleTarget, areaDamage: combatParty.orin.stats.areaDamage } + : { singleTarget: 0, areaDamage: 0 }, + vale: combatParty.vale + ? { singleTarget: combatParty.vale.stats.singleTarget, areaDamage: combatParty.vale.stats.areaDamage } + : { singleTarget: 0, areaDamage: 0 }, + }; +} + +function projectRosterMember( + slotId: AiCombatantId, + member: PartyRosterMember, + modifier: MemberGearModifiers, +): PartyMember { + const maxHp = Math.max(1, Math.round(member.stats.maxHp * modifier.maxHealth)); + const healthRatio = member.stats.maxHp > 0 ? clamp(member.hp / member.stats.maxHp, 0, 1) : 0; + return { + id: slotId, + name: member.name, + className: member.className, + role: member.role, + color: member.color, + maxHp, + hp: member.hp <= 0 ? 0 : maxHp * healthRatio, + absorb: 0, + healingEffects: [], + reactiveHeal: null, + knockedUntil: 0, + debuffs: [], + runProfile: { + instanceId: member.instanceId, + combatProfileId: member.archetypeId, + combatKitId: combatKitForRosterMember(member), + tier: member.rarity, + visualArchetype: visualArchetype(member.archetypeId), + }, + }; +} + +/** + * Projects immutable run state into the legacy five-slot combat shape. Missing + * draft slots stay absent instead of creating targetable placeholder allies. + */ +export function projectRpgCombat( + source: CombatProjectionSource, + options: ProjectRpgCombatOptions, +): RpgCombatProjection { + const combatParty = assignRosterToCombatSlots(source.roster); + const gearModifiers = encounterGearModifiers(combatParty, source.equipment); + const healerBase = freshParty(options.healerClassId, options.playerName)[0]; + const healerMaxHp = Math.max(1, Math.round(healerBase.maxHp * gearModifiers.aelia.maxHealth)); + const healer: PartyMember = { ...healerBase, maxHp: healerMaxHp, hp: healerMaxHp }; + const party: PartyMember[] = [healer]; + + for (const slotId of RPG_COMBAT_SLOT_IDS) { + const member = combatParty[slotId]; + if (member) party.push(projectRosterMember(slotId, member, gearModifiers[slotId])); + } + + return { + party, + gearModifiers, + damageProfiles: createRpgDamageProfiles(combatParty), + }; +} + +/** + * Converts combat health back to gear-independent run health. This prevents an + * armor swap from permanently inflating or reducing a companion's saved HP. + */ +export function extractRpgPartyVitals( + party: readonly PartyMember[], + combatParty: CombatPartyAssignment, +): PartyVitalUpdate[] { + const vitals: PartyVitalUpdate[] = []; + for (const slotId of RPG_COMBAT_SLOT_IDS) { + const rosterMember = combatParty[slotId]; + if (!rosterMember) continue; + const combatMember = party.find((member) => member.id === slotId); + if (!combatMember) continue; + const ratio = combatMember.maxHp > 0 ? clamp(combatMember.hp / combatMember.maxHp, 0, 1) : 0; + vitals.push({ instanceId: rosterMember.instanceId, hp: rosterMember.stats.maxHp * ratio }); + } + return vitals; +} + +/** Converts gear-adjusted healer health back to the run's 100 HP base. */ +export function extractRpgPlayerVital(party: readonly PartyMember[]): number { + const healer = party.find((member) => member.id === "aelia"); + if (!healer || healer.maxHp <= 0) return 0; + return 100 * clamp(healer.hp / healer.maxHp, 0, 1); +} + +export function normalizedSpellRank( + ranks: Partial>, + spellId: HealerAbilityId, +): number { + const value = ranks[spellId] ?? 0; + return Number.isFinite(value) ? clamp(Math.floor(value), 0, MAX_SPELL_RANK) : 0; +} + +/** Applies to direct healing, absorbs, and periodic-heal magnitudes. */ +export function spellRankPowerMultiplier( + ranks: Partial>, + spellId: HealerAbilityId, +): number { + return 1 + normalizedSpellRank(ranks, spellId) * SPELL_POWER_PER_RANK; +} + +/** Applies only to the upgraded spell, before the player's run-gear haste. */ +export function spellRankCooldownMultiplier( + ranks: Partial>, + spellId: HealerAbilityId, +): number { + return 1 - normalizedSpellRank(ranks, spellId) * SPELL_COOLDOWN_REDUCTION_PER_RANK; +} diff --git a/src/game/rpgRoguelike/index.ts b/src/game/rpgRoguelike/index.ts new file mode 100644 index 0000000..0651958 --- /dev/null +++ b/src/game/rpgRoguelike/index.ts @@ -0,0 +1,9 @@ +export * from "./types"; +export * from "./random"; +export * from "./party"; +export * from "./spells"; +export * from "./challenges"; +export * from "./rewards"; +export * from "./run"; +export * from "./playSpace"; +export * from "./uiModel"; diff --git a/src/game/rpgRoguelike/party.ts b/src/game/rpgRoguelike/party.ts new file mode 100644 index 0000000..a46f402 --- /dev/null +++ b/src/game/rpgRoguelike/party.ts @@ -0,0 +1,224 @@ +import { randomInt, shuffleWithRandom, weightedIndex } from "./random"; +import type { + AllyCombatSlotId, + CombatPartyAssignment, + PartyArchetypeId, + PartyClassId, + PartyCombatStats, + PartyDraftCandidate, + PartyRarity, + PartyRole, + PartyRosterMember, + PartyTraitId, + RandomState, +} from "./types"; +import { MAX_ACTIVE_ROSTER, PARTY_OFFERS_PER_WAVE, PARTY_RECRUITS_PER_WAVE } from "./types"; + +export interface PartyRarityDefinition { + readonly id: PartyRarity; + readonly rank: number; + readonly color: string; + readonly healthMultiplier: number; + readonly damageMultiplier: number; + readonly traitCount: number; +} + +export interface PartyArchetypeDefinition { + readonly id: PartyArchetypeId; + readonly classId: PartyClassId; + readonly className: string; + readonly role: PartyRole; + readonly color: string; + readonly baseStats: PartyCombatStats; + readonly traits: readonly PartyTraitId[]; +} + +export const PARTY_RARITY_ORDER: readonly PartyRarity[] = ["white", "green", "blue", "purple", "gold"]; + +export const PARTY_RARITIES: Record = { + white: { id: "white", rank: 0, color: "#e7e9ec", healthMultiplier: 1, damageMultiplier: 1, traitCount: 0 }, + green: { id: "green", rank: 1, color: "#70d886", healthMultiplier: 1.04, damageMultiplier: 1.035, traitCount: 1 }, + blue: { id: "blue", rank: 2, color: "#65aef2", healthMultiplier: 1.08, damageMultiplier: 1.07, traitCount: 1 }, + purple: { id: "purple", rank: 3, color: "#b47aec", healthMultiplier: 1.12, damageMultiplier: 1.105, traitCount: 2 }, + gold: { id: "gold", rank: 4, color: "#efc858", healthMultiplier: 1.16, damageMultiplier: 1.14, traitCount: 3 }, +}; + +const stats = ( + maxHp: number, + singleTarget: number, + areaDamage: number, + defense: number, + moveSpeed: number, +): PartyCombatStats => ({ maxHp, singleTarget, areaDamage, defense, moveSpeed }); + +export const PARTY_ARCHETYPES: Record = { + "knight-tank": { + id: "knight-tank", classId: "knight", className: "Knight", role: "Tank", color: "#69a8dd", + baseStats: stats(150, 0.72, 0.75, 1.2, 0.92), + traits: ["steadfast", "battle-hardened", "guardian-instinct", "royal-aegis"], + }, + "ranger-damage": { + id: "ranger-damage", classId: "ranger", className: "Ranger", role: "Damage", color: "#74c987", + baseStats: stats(94, 1, 0.88, 0.92, 1.05), + traits: ["sure-footed", "eagle-eye", "rapid-volley", "perfect-shot"], + }, + "mage-damage": { + id: "mage-damage", classId: "mage", className: "Mage", role: "Damage", color: "#b17ee6", + baseStats: stats(86, 1.02, 1.18, 0.86, 0.96), + traits: ["arcane-focus", "spell-echo", "volatile-power", "astral-mastery"], + }, + "rogue-damage": { + id: "rogue-damage", classId: "rogue", className: "Rogue", role: "Damage", color: "#d97171", + baseStats: stats(92, 1.18, 0.82, 0.88, 1.18), + traits: ["light-footed", "exploit-opening", "blade-dance", "shadow-master"], + }, + "warrior-tank": { + id: "warrior-tank", classId: "warrior", className: "Warrior · Vanguard", role: "Tank", color: "#d98b55", + baseStats: stats(145, 0.8, 1.02, 1.14, 0.96), + traits: ["iron-blood", "battle-hardened", "war-cry", "unstoppable"], + }, + "warrior-damage": { + id: "warrior-damage", classId: "warrior", className: "Warrior · Ravager", role: "Damage", color: "#e07a4f", + baseStats: stats(112, 1.02, 1.28, 1, 1), + traits: ["iron-blood", "whirlwind", "war-cry", "unstoppable"], + }, + "warlock-damage": { + id: "warlock-damage", classId: "warlock", className: "Warlock", role: "Damage", color: "#8d6ad4", + baseStats: stats(84, 1.24, 0.8, 0.84, 0.94), + traits: ["soul-drain", "doom-mark", "dark-pact", "soul-tyrant"], + }, + "monk-tank": { + id: "monk-tank", classId: "monk", className: "Monk · Guardian", role: "Tank", color: "#d7b35c", + baseStats: stats(122, 0.94, 0.86, 1.08, 1.12), + traits: ["centered", "counterstrike", "flow-state", "perfect-form"], + }, + "monk-damage": { + id: "monk-damage", classId: "monk", className: "Monk · Striker", role: "Damage", color: "#e0ba62", + baseStats: stats(82, 1.28, 0.76, 0.82, 1.22), + traits: ["centered", "counterstrike", "flow-state", "perfect-form"], + }, +}; + +export const PARTY_ARCHETYPE_ORDER = Object.keys(PARTY_ARCHETYPES) as PartyArchetypeId[]; +export const TANK_ARCHETYPE_IDS = PARTY_ARCHETYPE_ORDER.filter((id) => PARTY_ARCHETYPES[id].role === "Tank"); + +const PARTY_NAMES = [ + "Ada", "Bram", "Cato", "Dara", "Eris", "Fenn", "Gale", "Hana", "Ivo", "Jora", + "Kest", "Lio", "Mara", "Nox", "Oona", "Pax", "Quin", "Rhea", "Soren", "Tali", +] as const; + +const RARITY_WEIGHTS_BY_WAVE: readonly (readonly number[])[] = [ + [55, 27, 12, 5, 1], + [45, 30, 16, 7, 2], + [35, 32, 20, 10, 3], +]; + +export function statsAtRarity(archetypeId: PartyArchetypeId, rarity: PartyRarity): PartyCombatStats { + const base = PARTY_ARCHETYPES[archetypeId].baseStats; + const tier = PARTY_RARITIES[rarity]; + return { + maxHp: Math.round(base.maxHp * tier.healthMultiplier), + singleTarget: base.singleTarget * tier.damageMultiplier, + areaDamage: base.areaDamage * tier.damageMultiplier, + defense: base.defense * tier.healthMultiplier, + moveSpeed: base.moveSpeed, + }; +} + +export function traitsAtRarity(archetypeId: PartyArchetypeId, rarity: PartyRarity): PartyTraitId[] { + return [...PARTY_ARCHETYPES[archetypeId].traits.slice(0, PARTY_RARITIES[rarity].traitCount)]; +} + +function createCandidate( + random: RandomState, + waveIndex: number, + offerIndex: number, + name: string, + archetypeId: PartyArchetypeId, +): { candidate: PartyDraftCandidate; random: RandomState } { + const weights = RARITY_WEIGHTS_BY_WAVE[Math.max(0, Math.min(RARITY_WEIGHTS_BY_WAVE.length - 1, waveIndex))]; + const rarityRoll = weightedIndex(random, weights); + const rarity = PARTY_RARITY_ORDER[rarityRoll.value]; + const archetype = PARTY_ARCHETYPES[archetypeId]; + return { + candidate: { + candidateId: `party-w${waveIndex + 1}-o${offerIndex + 1}-${archetypeId}-${rarityRoll.random.state.toString(36)}`, + name, + archetypeId, + classId: archetype.classId, + className: archetype.className, + role: archetype.role, + rarity, + stats: statsAtRarity(archetypeId, rarity), + traitIds: traitsAtRarity(archetypeId, rarity), + color: archetype.color, + }, + random: rarityRoll.random, + }; +} + +export function generatePartyDraftOffers( + source: RandomState, + waveIndex: number, +): { offers: PartyDraftCandidate[]; random: RandomState } { + let random = source; + const tankRoll = randomInt(random, TANK_ARCHETYPE_IDS.length); + random = tankRoll.random; + const guaranteedTank = TANK_ARCHETYPE_IDS[tankRoll.value]; + const otherArchetypes = PARTY_ARCHETYPE_ORDER.filter((id) => id !== guaranteedTank); + const shuffledArchetypes = shuffleWithRandom(random, otherArchetypes); + random = shuffledArchetypes.random; + const offerArchetypes = [guaranteedTank, ...shuffledArchetypes.values.slice(0, PARTY_OFFERS_PER_WAVE - 1)]; + const mixedArchetypes = shuffleWithRandom(random, offerArchetypes); + random = mixedArchetypes.random; + const shuffledNames = shuffleWithRandom(random, PARTY_NAMES); + random = shuffledNames.random; + + const offers: PartyDraftCandidate[] = []; + for (let index = 0; index < PARTY_OFFERS_PER_WAVE; index += 1) { + const created = createCandidate(random, waveIndex, index, shuffledNames.values[index], mixedArchetypes.values[index]); + offers.push(created.candidate); + random = created.random; + } + return { offers, random }; +} + +export function recruitCandidate(candidate: PartyDraftCandidate): PartyRosterMember { + return { ...candidate, instanceId: candidate.candidateId, hp: candidate.stats.maxHp }; +} + +export function canRecruitCandidate(roster: readonly PartyRosterMember[], recruitsThisWave: number): boolean { + return roster.length < MAX_ACTIVE_ROSTER && recruitsThisWave < PARTY_RECRUITS_PER_WAVE; +} + +export function nextPartyRarity(rarity: PartyRarity): PartyRarity | null { + const index = PARTY_RARITY_ORDER.indexOf(rarity); + return index >= 0 && index < PARTY_RARITY_ORDER.length - 1 ? PARTY_RARITY_ORDER[index + 1] : null; +} + +export function upgradeRosterMemberRarity(member: PartyRosterMember): PartyRosterMember { + const rarity = nextPartyRarity(member.rarity); + if (!rarity) return member; + const stats = statsAtRarity(member.archetypeId, rarity); + const gainedMaxHp = stats.maxHp - member.stats.maxHp; + return { + ...member, + rarity, + stats, + hp: member.hp <= 0 ? 0 : Math.min(stats.maxHp, member.hp + gainedMaxHp), + traitIds: traitsAtRarity(member.archetypeId, rarity), + }; +} + +const ALLY_COMBAT_SLOTS: readonly AllyCombatSlotId[] = ["brann", "nia", "orin", "vale"]; + +export function assignRosterToCombatSlots(roster: readonly PartyRosterMember[]): CombatPartyAssignment { + const assignment: CombatPartyAssignment = { brann: null, nia: null, orin: null, vale: null }; + const available = roster.slice(0, MAX_ACTIVE_ROSTER); + const tank = available.find((member) => member.role === "Tank") ?? available[0]; + if (!tank) return assignment; + assignment.brann = tank; + const remaining = available.filter((member) => member.instanceId !== tank.instanceId); + for (let index = 0; index < remaining.length; index += 1) assignment[ALLY_COMBAT_SLOTS[index + 1]] = remaining[index]; + return assignment; +} diff --git a/src/game/rpgRoguelike/playSpace.test.ts b/src/game/rpgRoguelike/playSpace.test.ts new file mode 100644 index 0000000..26b77f4 --- /dev/null +++ b/src/game/rpgRoguelike/playSpace.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; +import { ARENA_CENTER, ARENA_RADIUS, ARENA_WALL_RADIUS, clampToArena } from "../arena"; +import type { WorldPosition } from "../types"; +import { + BOSS_ARENA_PORTAL_EXIT_DEPTH, + BOSS_ARENA_PORTAL_HALF_WIDTH, + BOSS_ARENA_PORTAL_OUTER_DEPTH, + clampToBossArenaWithPortals, + detectBossArenaExit, + type BossArenaPortalOpenState, +} from "./playSpace"; + +const CLOSED: BossArenaPortalOpenState = { north: false, south: false }; +const NORTH_OPEN: BossArenaPortalOpenState = { north: true, south: false }; +const SOUTH_OPEN: BossArenaPortalOpenState = { north: false, south: true }; + +describe("RPG roguelike boss arena play space", () => { + it("preserves the existing arena clamp while both portals are closed", () => { + const samples: WorldPosition[] = [ + [0, ARENA_CENTER[1]], + [0, ARENA_CENTER[1] - ARENA_WALL_RADIUS], + [0.8, ARENA_CENTER[1] + ARENA_WALL_RADIUS + 2], + [ARENA_WALL_RADIUS, ARENA_CENTER[1]], + ]; + + for (const sample of samples) { + expect(clampToBossArenaWithPortals(sample, CLOSED, 0.35)).toEqual(clampToArena(sample, 0.35)); + } + }); + + it("allows travel through only the corresponding open rectangular throat", () => { + const northThroat: WorldPosition = [0, ARENA_CENTER[1] - ARENA_WALL_RADIUS]; + const southThroat: WorldPosition = [0, ARENA_CENTER[1] + ARENA_WALL_RADIUS]; + + expect(clampToBossArenaWithPortals(northThroat, NORTH_OPEN)).toEqual(northThroat); + expect(clampToBossArenaWithPortals(southThroat, SOUTH_OPEN)).toEqual(southThroat); + expect(clampToBossArenaWithPortals(southThroat, NORTH_OPEN)).toEqual(clampToArena(southThroat)); + expect(clampToBossArenaWithPortals(northThroat, SOUTH_OPEN)).toEqual(clampToArena(northThroat)); + }); + + it("keeps an open throat narrow and clamps its outer end", () => { + const beyondSide: WorldPosition = [BOSS_ARENA_PORTAL_HALF_WIDTH + 2, ARENA_CENTER[1] - ARENA_WALL_RADIUS]; + const beyondEnd: WorldPosition = [0, ARENA_CENTER[1] - ARENA_WALL_RADIUS - BOSS_ARENA_PORTAL_OUTER_DEPTH - 4]; + const sideResult = clampToBossArenaWithPortals(beyondSide, NORTH_OPEN); + const endResult = clampToBossArenaWithPortals(beyondEnd, NORTH_OPEN); + + expect(sideResult).not.toEqual(beyondSide); + expect(sideResult[0]).toBeLessThanOrEqual(BOSS_ARENA_PORTAL_HALF_WIDTH); + expect(endResult).toEqual([ + 0, + ARENA_CENTER[1] - ARENA_WALL_RADIUS - BOSS_ARENA_PORTAL_OUTER_DEPTH, + ]); + }); + + it("keeps ordinary in-arena movement unchanged when a portal is open", () => { + const position: WorldPosition = [ARENA_RADIUS * 0.4, ARENA_CENTER[1] + ARENA_RADIUS * 0.25]; + expect(clampToBossArenaWithPortals(position, NORTH_OPEN)).toEqual(position); + }); + + it("detects exits only after crossing an open portal's exit plane", () => { + const northExitZ = ARENA_CENTER[1] - ARENA_WALL_RADIUS - BOSS_ARENA_PORTAL_EXIT_DEPTH; + const southExitZ = ARENA_CENTER[1] + ARENA_WALL_RADIUS + BOSS_ARENA_PORTAL_EXIT_DEPTH; + + expect(detectBossArenaExit([0, northExitZ], NORTH_OPEN)).toBe("north"); + expect(detectBossArenaExit([0, southExitZ], SOUTH_OPEN)).toBe("south"); + expect(detectBossArenaExit([0, northExitZ + 0.01], NORTH_OPEN)).toBeNull(); + expect(detectBossArenaExit([0, northExitZ], CLOSED)).toBeNull(); + expect(detectBossArenaExit([BOSS_ARENA_PORTAL_HALF_WIDTH + 0.01, northExitZ], NORTH_OPEN)).toBeNull(); + }); +}); + diff --git a/src/game/rpgRoguelike/playSpace.ts b/src/game/rpgRoguelike/playSpace.ts new file mode 100644 index 0000000..e4305fa --- /dev/null +++ b/src/game/rpgRoguelike/playSpace.ts @@ -0,0 +1,143 @@ +import { + ARENA_CENTER, + ARENA_RADIUS, + ARENA_WALL_RADIUS, + clampToArena, +} from "../arena"; +import type { WorldPosition } from "../types"; + +/** North follows Three.js convention toward negative Z; south points toward positive Z. */ +export type BossArenaPortalSide = "north" | "south"; + +export type BossArenaPortalOpenState = Readonly>; + +export const CLOSED_BOSS_ARENA_PORTALS: BossArenaPortalOpenState = { + north: false, + south: false, +}; + +export const NORTH_OPEN_BOSS_ARENA_PORTALS: BossArenaPortalOpenState = { + north: true, + south: false, +}; + +/** Half-width of the walkable opening between each doorway frame. */ +export const BOSS_ARENA_PORTAL_HALF_WIDTH = 1.45; +/** Overlap keeps each rectangular throat connected to the circular arena. */ +export const BOSS_ARENA_PORTAL_THROAT_OVERLAP = 0.75; +/** Walkable space beyond the visible wall before the room transition fires. */ +export const BOSS_ARENA_PORTAL_OUTER_DEPTH = 1.5; +/** Exit plane sits just beyond the doorway instead of at the arena's combat edge. */ +export const BOSS_ARENA_PORTAL_EXIT_DEPTH = 0.55; + +interface Bounds { + minX: number; + maxX: number; + minZ: number; + maxZ: number; +} + +function normalizedPadding(padding: number) { + return Number.isFinite(padding) ? Math.max(0, padding) : 0; +} + +function portalBounds(side: BossArenaPortalSide, padding: number): Bounds | null { + const safePadding = normalizedPadding(padding); + const halfWidth = Math.max(0, BOSS_ARENA_PORTAL_HALF_WIDTH - safePadding); + if (halfWidth <= 0) return null; + + const arenaDistance = Math.max(0, ARENA_RADIUS - safePadding); + const innerDistance = Math.max(0, arenaDistance - BOSS_ARENA_PORTAL_THROAT_OVERLAP); + const outerDistance = Math.max( + innerDistance, + ARENA_WALL_RADIUS + BOSS_ARENA_PORTAL_OUTER_DEPTH - safePadding, + ); + const centerZ = ARENA_CENTER[1]; + + return side === "north" + ? { + minX: ARENA_CENTER[0] - halfWidth, + maxX: ARENA_CENTER[0] + halfWidth, + minZ: centerZ - outerDistance, + maxZ: centerZ - innerDistance, + } + : { + minX: ARENA_CENTER[0] - halfWidth, + maxX: ARENA_CENTER[0] + halfWidth, + minZ: centerZ + innerDistance, + maxZ: centerZ + outerDistance, + }; +} + +function contains(bounds: Bounds, position: WorldPosition) { + return position[0] >= bounds.minX + && position[0] <= bounds.maxX + && position[1] >= bounds.minZ + && position[1] <= bounds.maxZ; +} + +function clampToBounds(bounds: Bounds, position: WorldPosition): WorldPosition { + return [ + Math.max(bounds.minX, Math.min(bounds.maxX, position[0])), + Math.max(bounds.minZ, Math.min(bounds.maxZ, position[1])), + ]; +} + +function distanceSquared(left: WorldPosition, right: WorldPosition) { + return (left[0] - right[0]) ** 2 + (left[1] - right[1]) ** 2; +} + +/** + * Clamps movement to the circular boss arena plus any currently open doorway + * throats. With both portals closed this is exactly the existing arena clamp. + */ +export function clampToBossArenaWithPortals( + position: WorldPosition, + portals: BossArenaPortalOpenState = CLOSED_BOSS_ARENA_PORTALS, + padding = 0, +): WorldPosition { + if (!portals.north && !portals.south) return clampToArena(position, padding); + + const safePadding = normalizedPadding(padding); + const radius = Math.max(0, ARENA_RADIUS - safePadding); + const offsetX = position[0] - ARENA_CENTER[0]; + const offsetZ = position[1] - ARENA_CENTER[1]; + if (Math.hypot(offsetX, offsetZ) <= radius) return [position[0], position[1]]; + + const openBounds = (["north", "south"] as const) + .filter((side) => portals[side]) + .map((side) => portalBounds(side, safePadding)) + .filter((bounds): bounds is Bounds => bounds !== null); + + if (openBounds.some((bounds) => contains(bounds, position))) { + return [position[0], position[1]]; + } + + let nearest = clampToArena(position, safePadding); + let nearestDistance = distanceSquared(position, nearest); + for (const bounds of openBounds) { + const candidate = clampToBounds(bounds, position); + const candidateDistance = distanceSquared(position, candidate); + if (candidateDistance < nearestDistance) { + nearest = candidate; + nearestDistance = candidateDistance; + } + } + return nearest; +} + +/** Returns the open portal crossed past its visible wall plane, if any. */ +export function detectBossArenaExit( + position: WorldPosition, + portals: BossArenaPortalOpenState = CLOSED_BOSS_ARENA_PORTALS, + padding = 0, +): BossArenaPortalSide | null { + const halfWidth = Math.max(0, BOSS_ARENA_PORTAL_HALF_WIDTH - normalizedPadding(padding)); + if (Math.abs(position[0] - ARENA_CENTER[0]) > halfWidth) return null; + + const exitDistance = ARENA_WALL_RADIUS + BOSS_ARENA_PORTAL_EXIT_DEPTH; + const offsetZ = position[1] - ARENA_CENTER[1]; + if (portals.north && offsetZ <= -exitDistance) return "north"; + if (portals.south && offsetZ >= exitDistance) return "south"; + return null; +} diff --git a/src/game/rpgRoguelike/random.ts b/src/game/rpgRoguelike/random.ts new file mode 100644 index 0000000..319e2e3 --- /dev/null +++ b/src/game/rpgRoguelike/random.ts @@ -0,0 +1,73 @@ +import type { RandomState } from "./types"; + +const DEFAULT_NON_ZERO_SEED = 0x9e3779b9; + +function hashString(value: string): number { + let hash = 0x811c9dc5; + for (let index = 0; index < value.length; index += 1) { + hash ^= value.charCodeAt(index); + hash = Math.imul(hash, 0x01000193); + } + return hash >>> 0; +} + +export function normalizeSeed(seed: number | string): number { + const normalized = typeof seed === "string" + ? hashString(seed) + : Number.isFinite(seed) + ? Math.trunc(seed) >>> 0 + : DEFAULT_NON_ZERO_SEED; + return normalized === 0 ? DEFAULT_NON_ZERO_SEED : normalized; +} + +export function createRandomState(seed: number | string): RandomState { + const normalized = normalizeSeed(seed); + return { seed: normalized, state: normalized, draws: 0 }; +} + +export function nextRandom(source: RandomState): { value: number; random: RandomState } { + const state = (source.state + 0x6d2b79f5) >>> 0; + let value = state; + value = Math.imul(value ^ (value >>> 15), value | 1); + value ^= value + Math.imul(value ^ (value >>> 7), value | 61); + const result = ((value ^ (value >>> 14)) >>> 0) / 4294967296; + return { + value: result, + random: { seed: source.seed, state, draws: source.draws + 1 }, + }; +} + +export function randomInt(source: RandomState, maximumExclusive: number): { value: number; random: RandomState } { + if (!Number.isInteger(maximumExclusive) || maximumExclusive <= 0) { + throw new Error("maximumExclusive must be a positive integer."); + } + const next = nextRandom(source); + return { value: Math.floor(next.value * maximumExclusive), random: next.random }; +} + +export function shuffleWithRandom(source: RandomState, values: readonly T[]): { values: T[]; random: RandomState } { + const shuffled = [...values]; + let random = source; + for (let index = shuffled.length - 1; index > 0; index -= 1) { + const sampled = randomInt(random, index + 1); + random = sampled.random; + [shuffled[index], shuffled[sampled.value]] = [shuffled[sampled.value], shuffled[index]]; + } + return { values: shuffled, random }; +} + +export function weightedIndex(source: RandomState, weights: readonly number[]): { value: number; random: RandomState } { + if (!weights.length || weights.some((weight) => !Number.isFinite(weight) || weight < 0)) { + throw new Error("weights must contain non-negative finite values."); + } + const total = weights.reduce((sum, weight) => sum + weight, 0); + if (total <= 0) throw new Error("weights must contain at least one positive value."); + const next = nextRandom(source); + let cursor = next.value * total; + for (let index = 0; index < weights.length; index += 1) { + cursor -= weights[index]; + if (cursor < 0) return { value: index, random: next.random }; + } + return { value: weights.length - 1, random: next.random }; +} + diff --git a/src/game/rpgRoguelike/rewards.ts b/src/game/rpgRoguelike/rewards.ts new file mode 100644 index 0000000..92bbc05 --- /dev/null +++ b/src/game/rpgRoguelike/rewards.ts @@ -0,0 +1,255 @@ +import { nextPartyRarity, upgradeRosterMemberRarity } from "./party"; +import { randomInt, shuffleWithRandom } from "./random"; +import { incrementSpellRank } from "./spells"; +import type { + RandomState, + RewardChest, + RewardChoice, + RpgRoguelikeRunState, + RunEquipment, + RunGearItem, + RunGearOwnerId, + RunGearSlotId, + RunGearStatId, + RunShopState, + ShopOffer, +} from "./types"; +import { MAX_RUN_GEAR_ENHANCEMENT, MAX_SPELL_RANK } from "./types"; + +export const RUN_GEAR_SLOT_ORDER: readonly RunGearSlotId[] = ["weapon", "armor", "trinket"]; + +const GEAR_SLOT_DATA: Record = { + weapon: { label: "Weapon", statId: "damage" }, + armor: { label: "Armor", statId: "maxHealth" }, + trinket: { label: "Trinket", statId: "haste" }, +}; + +function enhancementLevel(value: number): 0 | 1 | 2 | 3 | 4 | 5 { + return Math.max(0, Math.min(MAX_RUN_GEAR_ENHANCEMENT, Math.floor(value))) as 0 | 1 | 2 | 3 | 4 | 5; +} + +export function equippedRunGear( + equipment: RunEquipment, + ownerId: RunGearOwnerId, + slotId: RunGearSlotId, +): RunGearItem | undefined { + return equipment[ownerId]?.[slotId]; +} + +export function createRunGearItem( + id: string, + ownerId: RunGearOwnerId, + slotId: RunGearSlotId, + enhancement: number, +): RunGearItem { + const level = enhancementLevel(enhancement); + const data = GEAR_SLOT_DATA[slotId]; + const ownerLabel = ownerId === "player" ? "Healer" : "Companion"; + return { + id, + ownerId, + slotId, + enhancement: level, + name: `${ownerLabel} ${data.label} +${level}`, + statId: data.statId, + statValue: data.statId === "haste" ? 2 + level * 2 : 4 + level * 4, + sellPrice: 18 + level * 12, + }; +} + +export function autoEquipRunGear( + equipment: RunEquipment, + bag: readonly RunGearItem[], + item: RunGearItem, +): { equipment: RunEquipment; bag: RunGearItem[]; equipped: boolean } { + const current = equippedRunGear(equipment, item.ownerId, item.slotId); + if (current && current.enhancement >= item.enhancement) { + return { equipment, bag: [...bag, item], equipped: false }; + } + const ownerEquipment = { ...(equipment[item.ownerId] ?? {}) }; + ownerEquipment[item.slotId] = item; + return { + equipment: { ...equipment, [item.ownerId]: ownerEquipment }, + bag: current ? [...bag, current] : [...bag], + equipped: true, + }; +} + +function gearOwners(state: Pick): RunGearOwnerId[] { + return ["player", ...state.roster.map((member) => member.instanceId)]; +} + +function gearRewardChoices( + source: RandomState, + state: Pick, + quality: number, + prefix: string, +): { choices: RewardChoice[]; random: RandomState } { + const choices: RewardChoice[] = []; + let random = source; + for (const ownerId of gearOwners(state)) { + for (const slotId of RUN_GEAR_SLOT_ORDER) { + const current = equippedRunGear(state.equipment, ownerId, slotId)?.enhancement ?? -1; + if (current >= MAX_RUN_GEAR_ENHANCEMENT) continue; + const minimum = enhancementLevel(Math.max(current + 1, quality)); + const rolled = randomInt(random, MAX_RUN_GEAR_ENHANCEMENT - minimum + 1); + random = rolled.random; + const next = enhancementLevel(minimum + rolled.value); + const item = createRunGearItem(`${prefix}-${ownerId}-${slotId}-${next}`, ownerId, slotId, next); + choices.push({ id: `reward-${item.id}`, kind: "run-gear", item, label: item.name }); + } + } + return { choices, random }; +} + +export function generateRewardChest( + source: RandomState, + state: Pick< + RpgRoguelikeRunState, + "roster" | "selectedSpellIds" | "spellRanks" | "equipment" | "bossesDefeated" + >, + requestedQuality: number, +): { chest: RewardChest; random: RandomState } { + const quality = Math.max(0, Math.min(3, Math.floor(requestedQuality))); + const prefix = `chest-${Math.max(1, state.bossesDefeated)}-q${quality}`; + const pool: RewardChoice[] = []; + + for (const spellId of state.selectedSpellIds) { + const current = Math.max(0, Math.floor(state.spellRanks[spellId] ?? 0)); + if (current >= MAX_SPELL_RANK) continue; + pool.push({ + id: `${prefix}-spell-${spellId}`, + kind: "spell-rank", + spellId, + nextRank: current + 1, + label: `${spellId} rank ${current + 1}`, + }); + } + + for (const member of state.roster) { + const rarity = nextPartyRarity(member.rarity); + if (!rarity) continue; + pool.push({ + id: `${prefix}-member-${member.instanceId}`, + kind: "member-rarity", + memberId: member.instanceId, + nextRarity: rarity, + label: `${member.name} → ${rarity}`, + }); + } + + const gear = gearRewardChoices(source, state, quality, prefix); + pool.push(...gear.choices); + for (let index = pool.length; index < 3; index += 1) { + const amount = 35 + quality * 15 + index * 5; + pool.push({ id: `${prefix}-currency-${index}`, kind: "currency", amount, label: `${amount} gold` }); + } + + const shuffled = shuffleWithRandom(gear.random, pool); + return { + chest: { id: prefix, quality, choices: shuffled.values.slice(0, 3) }, + random: shuffled.random, + }; +} + +export function applyRewardChoice( + state: RpgRoguelikeRunState, + choice: RewardChoice, +): RpgRoguelikeRunState { + if (choice.kind === "spell-rank") { + return { ...state, spellRanks: incrementSpellRank(state.spellRanks, choice.spellId) }; + } + if (choice.kind === "member-rarity") { + const roster = state.roster.map((member) => member.instanceId === choice.memberId ? upgradeRosterMemberRarity(member) : member); + return { + ...state, + roster, + }; + } + if (choice.kind === "run-gear") { + const applied = autoEquipRunGear(state.equipment, state.bag, choice.item); + return { ...state, equipment: applied.equipment, bag: applied.bag }; + } + return { ...state, currency: state.currency + choice.amount }; +} + +export function generateRunShop( + source: RandomState, + state: Pick, + act: number, +): { shop: RunShopState; random: RandomState } { + const quality = Math.max(0, Math.min(3, act)); + const gear = gearRewardChoices(source, state, quality, `shop-a${act}`); + const rewardChoices = gear.choices + .filter((choice): choice is Extract => choice.kind === "run-gear"); + const shuffled = shuffleWithRandom(gear.random, rewardChoices); + const offers: ShopOffer[] = shuffled.values.slice(0, 4).map((choice) => ({ + id: `offer-${choice.item.id}`, + item: choice.item, + price: 55 + choice.item.enhancement * 28, + sold: false, + })); + return { + shop: { + act, + offers, + restCost: 25 + act * 10, + reviveCost: 45 + act * 15, + }, + random: shuffled.random, + }; +} + +export function buyShopOffer(state: RpgRoguelikeRunState, offerId: string): RpgRoguelikeRunState { + const offer = state.shop?.offers.find((candidate) => candidate.id === offerId); + if (!offer || offer.sold || state.currency < offer.price || !state.shop) return state; + const applied = autoEquipRunGear(state.equipment, state.bag, offer.item); + return { + ...state, + currency: state.currency - offer.price, + equipment: applied.equipment, + bag: applied.bag, + shop: { + ...state.shop, + offers: state.shop.offers.map((candidate) => candidate.id === offerId ? { ...candidate, sold: true } : candidate), + }, + }; +} + +export function sellBagItem(state: RpgRoguelikeRunState, itemId: string): RpgRoguelikeRunState { + const item = state.bag.find((candidate) => candidate.id === itemId); + if (!item) return state; + return { + ...state, + bag: state.bag.filter((candidate) => candidate.id !== itemId), + currency: state.currency + item.sellPrice, + }; +} + +export function restParty(state: RpgRoguelikeRunState): RpgRoguelikeRunState { + if (!state.shop || state.currency < state.shop.restCost) return state; + const needsRest = state.playerHp > 0 && state.playerHp < 100 + || state.roster.some((member) => member.hp > 0 && member.hp < member.stats.maxHp); + if (!needsRest) return state; + const roster = state.roster.map((member) => member.hp > 0 ? { ...member, hp: member.stats.maxHp } : member); + return { + ...state, + currency: state.currency - state.shop.restCost, + playerHp: state.playerHp > 0 ? 100 : state.playerHp, + roster, + }; +} + +export function revivePartyMember(state: RpgRoguelikeRunState, memberId: string): RpgRoguelikeRunState { + if (!state.shop || state.currency < state.shop.reviveCost) return state; + const member = state.roster.find((candidate) => candidate.instanceId === memberId); + if (!member || member.hp > 0) return state; + const roster = state.roster.map((candidate) => candidate.instanceId === memberId + ? { ...candidate, hp: Math.max(1, Math.ceil(candidate.stats.maxHp * 0.5)) } + : candidate); + return { + ...state, + currency: state.currency - state.shop.reviveCost, + roster, + }; +} diff --git a/src/game/rpgRoguelike/rpgRoguelike.test.ts b/src/game/rpgRoguelike/rpgRoguelike.test.ts new file mode 100644 index 0000000..b84fd78 --- /dev/null +++ b/src/game/rpgRoguelike/rpgRoguelike.test.ts @@ -0,0 +1,340 @@ +import { describe, expect, it } from "vitest"; +import type { HealerAbilityId } from "../types"; +import { + MAX_ACTIVE_ROSTER, + MAX_RUN_GEAR_ENHANCEMENT, + MAX_SPELL_RANK, + PARTY_RECRUITS_PER_WAVE, + PARTY_RARITY_ORDER, + TOTAL_BOSS_COUNT, + abilityLoadoutFromSpellIds, + applyRewardChoice, + assignRosterToCombatSlots, + autoEquipRunGear, + challengeObjective, + createRandomState, + createRpgRoguelikeRun, + createRunGearItem, + equippedRunGear, + generateRewardChest, + generateRunShop, + nextPartyRarity, + reduceRpgRoguelikeRun, + selectCurrentBossId, + type PartyRosterMember, + type RewardChoice, + type RpgRoguelikeAction, + type RpgRoguelikeRunState, + type RunGearItem, +} from "."; + +function dispatch(state: RpgRoguelikeRunState, action: RpgRoguelikeAction) { + return reduceRpgRoguelikeRun(state, action); +} + +function finishDrafts(seed: number | string = 42): RpgRoguelikeRunState { + let state = createRpgRoguelikeRun({ seed }); + while (state.phase === "party-draft") { + const draft = state.partyDraft!; + const tank = draft.offers.find((offer) => offer.role === "Tank"); + const ordered = tank ? [tank, ...draft.offers.filter((offer) => offer !== tank)] : draft.offers; + for (const offer of ordered) { + if (state.roster.length >= MAX_ACTIVE_ROSTER) break; + if ((state.partyDraft?.recruitedThisWaveIds.length ?? 0) >= PARTY_RECRUITS_PER_WAVE) break; + state = dispatch(state, { type: "party-recruit", candidateId: offer.candidateId }); + } + state = dispatch(state, { type: "party-next-wave" }); + } + while (state.phase === "spell-draft") { + for (const spellId of state.spellDraft!.offers.slice(0, 2)) state = dispatch(state, { type: "spell-pick", spellId }); + state = dispatch(state, { type: "spell-next-wave" }); + } + return state; +} + +function usableReward(state: RpgRoguelikeRunState, choice: RewardChoice) { + if (choice.kind === "spell-rank") return state.selectedSpellIds.includes(choice.spellId) + && choice.nextRank > (state.spellRanks[choice.spellId] ?? 0) + && choice.nextRank <= MAX_SPELL_RANK; + if (choice.kind === "member-rarity") { + const member = state.roster.find((candidate) => candidate.instanceId === choice.memberId); + return Boolean(member && nextPartyRarity(member.rarity) === choice.nextRarity); + } + if (choice.kind === "run-gear") { + const current = equippedRunGear(state.equipment, choice.item.ownerId, choice.item.slotId)?.enhancement ?? -1; + return choice.item.enhancement > current && choice.item.enhancement <= MAX_RUN_GEAR_ENHANCEMENT; + } + return choice.amount > 0; +} + +describe("RPG Roguelike deterministic domain", () => { + it("replays identical offers, route, state, and RNG cursor from one seed", () => { + const left = finishDrafts("deterministic-run"); + const right = finishDrafts("deterministic-run"); + expect(left).toEqual(right); + expect(left.random.draws).toBeGreaterThan(0); + expect(JSON.parse(JSON.stringify(left))).toEqual(left); + }); + + it("enforces party wave/global caps, supports replacement, and guarantees tank-capable offers", () => { + let state = createRpgRoguelikeRun({ seed: 7 }); + expect(state.partyDraft!.offers).toHaveLength(5); + expect(state.partyDraft!.offers.some((offer) => offer.role === "Tank")).toBe(true); + const firstFour = state.partyDraft!.offers.slice(0, 4); + for (const offer of firstFour) state = dispatch(state, { type: "party-recruit", candidateId: offer.candidateId }); + expect(state.roster).toHaveLength(3); + + const removed = state.roster[0]; + expect(dispatch(state, { type: "party-remove", memberId: removed.instanceId })).toBe(state); + state = dispatch(state, { type: "party-next-wave" }); + expect(state.partyDraft!.waveIndex).toBe(1); + expect(state.partyDraft!.offers.some((offer) => offer.role === "Tank")).toBe(true); + + state = dispatch(state, { type: "party-recruit", candidateId: state.partyDraft!.offers[0].candidateId }); + expect(state.roster).toHaveLength(4); + state = dispatch(state, { type: "party-remove", memberId: removed.instanceId }); + expect(state.roster).toHaveLength(3); + expect(state.partyDraft!.recruitedThisWaveIds).toHaveLength(1); + state = dispatch(state, { type: "party-recruit", candidateId: state.partyDraft!.offers[1].candidateId }); + expect(state.roster).toHaveLength(4); + const before = state; + state = dispatch(state, { type: "party-recruit", candidateId: state.partyDraft!.offers[2].candidateId }); + expect(state).toBe(before); + }); + + it("assigns a drafted tank to Brann while preserving four distinct instances", () => { + const state = finishDrafts(19); + const assignment = assignRosterToCombatSlots(state.roster); + expect(assignment.brann?.role).toBe("Tank"); + expect(Object.values(assignment).filter(Boolean)).toHaveLength(4); + expect(new Set(Object.values(assignment).map((member) => member?.instanceId))).toHaveLength(4); + }); + + it("will not finalize four companions without a tank", () => { + let state = createRpgRoguelikeRun({ seed: 91 }); + while (state.phase === "party-draft" && state.partyDraft!.waveIndex < 2) { + for (const offer of state.partyDraft!.offers) { + if (state.roster.length >= 4 || state.partyDraft!.recruitedThisWaveIds.length >= 3) break; + state = dispatch(state, { type: "party-recruit", candidateId: offer.candidateId }); + } + state = dispatch(state, { type: "party-next-wave" }); + } + const tankless = { + ...state, + roster: state.roster.map((member) => ({ ...member, role: "Damage" as const })), + }; + expect(tankless.roster).toHaveLength(4); + expect(dispatch(tankless, { type: "party-next-wave" })).toBe(tankless); + }); + + it("caps spell picks, refunds a current-wave removal, and emits mixed spells in pick order", () => { + let state = createRpgRoguelikeRun({ seed: 22 }); + while (state.phase === "party-draft") { + for (const offer of state.partyDraft!.offers) { + if (state.roster.length >= 4 || state.partyDraft!.recruitedThisWaveIds.length >= 3) break; + state = dispatch(state, { type: "party-recruit", candidateId: offer.candidateId }); + } + state = dispatch(state, { type: "party-next-wave" }); + } + const firstWave = state.spellDraft!.offers; + state = dispatch(state, { type: "spell-pick", spellId: firstWave[0] }); + state = dispatch(state, { type: "spell-pick", spellId: firstWave[1] }); + expect(dispatch(state, { type: "spell-remove", spellId: firstWave[0] })).toBe(state); + state = dispatch(state, { type: "spell-next-wave" }); + const offers = state.spellDraft!.offers; + state = dispatch(state, { type: "spell-pick", spellId: offers[0] }); + state = dispatch(state, { type: "spell-pick", spellId: offers[1] }); + const capped = dispatch(state, { type: "spell-pick", spellId: offers[2] }); + expect(capped).toBe(state); + state = dispatch(state, { type: "spell-remove", spellId: offers[0] }); + state = dispatch(state, { type: "spell-pick", spellId: offers[2] }); + expect(state.selectedSpellIds).toEqual([firstWave[0], firstWave[1], offers[1], offers[2]]); + expect(state.abilityLoadout).toEqual({ + ability1: firstWave[0], + ability2: firstWave[1], + ability3: offers[2], + ability4: offers[1], + }); + + const mixed: HealerAbilityId[] = ["priest-mend", "druid-rejuvenation", "shaman-chain-heal"]; + expect(abilityLoadoutFromSpellIds(mixed)).toEqual({ + ability1: "priest-mend", + ability2: "druid-rejuvenation", + ability3: "shaman-chain-heal", + }); + }); + + it("builds three acts of three bosses plus a finale, with nine challenges and three shops", () => { + let state = finishDrafts(9001); + expect(state.bossRoute).toHaveLength(TOTAL_BOSS_COUNT); + let challenges = 0; + let shops = 0; + let safety = 0; + while (state.phase !== "victory" && safety++ < 200) { + if (state.phase === "challenge-briefing") { + challenges += 1; + state = dispatch(state, { type: "challenge-start" }); + } else if (state.phase === "challenge-active") { + state = dispatch(state, { type: "challenge-complete", metrics: { bricksBroken: 999, bossKills: 999, kills: 999 } }); + } else if (state.phase === "boss-briefing") { + expect(selectCurrentBossId(state)).not.toBeNull(); + state = dispatch(state, { type: "boss-start" }); + } else if (state.phase === "boss-combat") { + state = dispatch(state, { type: "boss-won" }); + } else if (state.phase === "boss-cleared") { + state = dispatch(state, { type: "reward-open" }); + } else if (state.phase === "reward") { + state = dispatch(state, { type: "reward-choose", choiceId: state.pendingReward!.choices[0].id }); + } else if (state.phase === "shop") { + shops += 1; + state = dispatch(state, { type: "shop-leave" }); + } else { + throw new Error(`Unexpected phase ${state.phase}`); + } + } + expect(state.phase).toBe("victory"); + expect(state.bossesDefeated).toBe(10); + expect(challenges).toBe(9); + expect(shops).toBe(3); + }); + + it("bounds repeat challenge scaling and treats challenge failure as soft", () => { + expect(challengeObjective("blockbreaker", 0).target).toBe(30); + expect(challengeObjective("blockbreaker", 1).target).toBe(45); + expect(challengeObjective("blockbreaker", 2).target).toBe(60); + expect(challengeObjective("blockbreaker", 99).target).toBe(60); + expect(challengeObjective("hockey", 99).target).toBe(3); + + let state = finishDrafts(77); + const currency = state.currency; + state = dispatch(state, { type: "challenge-start" }); + state = dispatch(state, { type: "challenge-complete" }); + expect(state.phase).toBe("boss-briefing"); + expect(state.lastChallengeResult?.succeeded).toBe(false); + expect(state.currency).toBe(currency); + expect(state.nextChestQualityBonus).toBe(0); + + let simultaneous = finishDrafts(78); + simultaneous = dispatch(simultaneous, { type: "challenge-start" }); + const objective = simultaneous.currentChallenge!.objective; + const metrics = objective.metric === "bricksBroken" + ? { bricksBroken: objective.target } + : objective.metric === "bossKills" + ? { bossKills: objective.target } + : { kills: objective.target }; + simultaneous = dispatch(simultaneous, { type: "challenge-complete", metrics, forcedFailure: true }); + expect(simultaneous.lastChallengeResult?.succeeded).toBe(false); + expect(simultaneous.nextChestQualityBonus).toBe(0); + }); + + it("generates exactly three distinct, usable chest choices and applies the selected upgrade", () => { + const state = finishDrafts(300); + const generated = generateRewardChest(createRandomState(1), state, 2); + expect(generated.chest.choices).toHaveLength(3); + expect(new Set(generated.chest.choices.map((choice) => choice.id))).toHaveLength(3); + expect(generated.chest.choices.every((choice) => usableReward(state, choice))).toBe(true); + const choice = generated.chest.choices[0]; + const applied = applyRewardChoice(state, choice); + if (choice.kind === "spell-rank") expect(applied.spellRanks[choice.spellId]).toBe(1); + else if (choice.kind === "member-rarity") { + expect(applied.roster.find((member) => member.instanceId === choice.memberId)?.rarity).toBe(choice.nextRarity); + } else if (choice.kind === "run-gear") { + expect(equippedRunGear(applied.equipment, choice.item.ownerId, choice.item.slotId)).toEqual(choice.item); + } else expect(applied.currency).toBe(state.currency + choice.amount); + }); + + it("caps run-only gear at +5 and keeps displaced gear in the sellable bag", () => { + const lower = createRunGearItem("lower", "player", "weapon", 2); + const capped = createRunGearItem("capped", "player", "weapon", 99); + expect(capped.enhancement).toBe(5); + const first = autoEquipRunGear({}, [], lower); + const second = autoEquipRunGear(first.equipment, first.bag, capped); + expect(equippedRunGear(second.equipment, "player", "weapon")?.enhancement).toBe(5); + expect(second.bag).toEqual([lower]); + const weaker = createRunGearItem("weaker", "player", "weapon", 4); + const third = autoEquipRunGear(second.equipment, second.bag, weaker); + expect(third.equipped).toBe(false); + expect(third.bag).toContain(weaker); + }); + + it("supports shop buy, sell, rest, revive, and leave", () => { + let state = finishDrafts(501); + const generated = generateRunShop(state.random, state, 1); + const downId = state.roster[0].instanceId; + const injuredId = state.roster[1].instanceId; + const sellable = createRunGearItem("sellable", "player", "armor", 0); + state = { + ...state, + phase: "shop", + random: generated.random, + shop: generated.shop, + currency: 2_000, + bag: [sellable], + roster: state.roster.map((member) => member.instanceId === downId + ? { ...member, hp: 0 } + : member.instanceId === injuredId + ? { ...member, hp: 1 } + : member), + }; + const offer = state.shop!.offers[0]; + state = dispatch(state, { type: "shop-buy", offerId: offer.id }); + expect(state.shop!.offers[0].sold).toBe(true); + expect(equippedRunGear(state.equipment, offer.item.ownerId, offer.item.slotId)).toEqual(offer.item); + const afterBuyCurrency = state.currency; + state = dispatch(state, { type: "shop-sell", itemId: sellable.id }); + expect(state.currency).toBe(afterBuyCurrency + sellable.sellPrice); + state = dispatch(state, { type: "shop-rest" }); + expect(state.roster.find((member) => member.instanceId === injuredId)?.hp) + .toBe(state.roster.find((member) => member.instanceId === injuredId)?.stats.maxHp); + expect(Object.values(assignRosterToCombatSlots(state.roster)).find((member) => member?.instanceId === injuredId)?.hp) + .toBe(state.roster.find((member) => member.instanceId === injuredId)?.stats.maxHp); + state = dispatch(state, { type: "shop-revive", memberId: downId }); + expect(state.roster.find((member) => member.instanceId === downId)!.hp).toBeGreaterThan(0); + expect(Object.values(assignRosterToCombatSlots(state.roster)).find((member) => member?.instanceId === downId)?.hp) + .toBe(Math.ceil(state.roster.find((member) => member.instanceId === downId)!.stats.maxHp * 0.5)); + state = dispatch(state, { type: "shop-leave" }); + expect(state.phase).not.toBe("shop"); + expect(state.shop).toBeNull(); + }); + + it("persists bounded party health and ends immediately on a boss loss", () => { + let state = finishDrafts(808); + state = dispatch(state, { type: "challenge-start" }); + state = dispatch(state, { type: "challenge-complete" }); + state = dispatch(state, { type: "boss-start" }); + const member = state.roster[0]; + state = dispatch(state, { + type: "boss-lost", + vitals: [{ instanceId: member.instanceId, hp: -20 }], + }); + expect(state.phase).toBe("defeat"); + expect(state.roster[0].hp).toBe(0); + expect(dispatch(state, { type: "reward-open" })).toBe(state); + }); + + it("defines monotonic rarity order and modest archetype upgrades", () => { + expect(PARTY_RARITY_ORDER).toEqual(["white", "green", "blue", "purple", "gold"]); + const state = finishDrafts(15); + const white = state.roster.find((member) => member.rarity === "white") as PartyRosterMember | undefined; + if (!white) return; + const choice: RewardChoice = { + id: "test-rarity", + kind: "member-rarity", + memberId: white.instanceId, + nextRarity: "green", + label: "upgrade", + }; + const upgraded = applyRewardChoice(state, choice).roster.find((member) => member.instanceId === white.instanceId)!; + expect(upgraded.stats.maxHp).toBeGreaterThan(white.stats.maxHp); + expect(upgraded.stats.singleTarget).toBeGreaterThan(white.stats.singleTarget); + expect(upgraded.traitIds.length).toBeGreaterThan(white.traitIds.length); + + const defeatedState = { + ...state, + roster: state.roster.map((member) => member.instanceId === white.instanceId ? { ...member, hp: 0 } : member), + }; + const defeatedUpgrade = applyRewardChoice(defeatedState, choice).roster + .find((member) => member.instanceId === white.instanceId)!; + expect(defeatedUpgrade.hp).toBe(0); + }); +}); diff --git a/src/game/rpgRoguelike/run.ts b/src/game/rpgRoguelike/run.ts new file mode 100644 index 0000000..eaeda2d --- /dev/null +++ b/src/game/rpgRoguelike/run.ts @@ -0,0 +1,352 @@ +import { AVAILABLE_BOSS_IDS } from "../bossCatalog"; +import type { BossId, HealerAbilityId } from "../types"; +import { + emptyChallengeCounts, + emptyChallengeMetrics, + evaluateChallenge, + mergeChallengeMetrics, + selectChallenge, +} from "./challenges"; +import { + canRecruitCandidate, + generatePartyDraftOffers, + recruitCandidate, +} from "./party"; +import { createRandomState, shuffleWithRandom } from "./random"; +import { + applyRewardChoice, + buyShopOffer, + generateRewardChest, + generateRunShop, + restParty, + revivePartyMember, + sellBagItem, +} from "./rewards"; +import { + addSpellToAbilityLoadout, + canPickSpell, + generateSpellDraftOffers, + removeSpellFromAbilityLoadout, +} from "./spells"; +import type { + PartyVitalUpdate, + RandomState, + RpgRoguelikeAction, + RpgRoguelikeRunConfig, + RpgRoguelikeRunState, +} from "./types"; +import { + ACT_BOSS_COUNT, + BOSSES_PER_ACT, + MAX_ACTIVE_ROSTER, + PARTY_DRAFT_WAVE_COUNT, + PARTY_RECRUITS_PER_WAVE, + SPELL_DRAFT_WAVE_COUNT, + TOTAL_BOSS_COUNT, +} from "./types"; + +function uniqueBossPool(pool: readonly BossId[]): BossId[] { + return [...new Set(pool)]; +} + +export function generateBossRoute( + source: RandomState, + requestedPool: readonly BossId[] = AVAILABLE_BOSS_IDS, +): { route: BossId[]; random: RandomState } { + const pool = uniqueBossPool(requestedPool); + if (!pool.length) throw new Error("RPG Roguelike requires at least one boss."); + const route: BossId[] = []; + let random = source; + while (route.length < TOTAL_BOSS_COUNT) { + const shuffled = shuffleWithRandom(random, pool); + random = shuffled.random; + for (const bossId of shuffled.values) { + if (route.length >= TOTAL_BOSS_COUNT) break; + if (pool.length > 1 && route[route.length - 1] === bossId) continue; + route.push(bossId); + } + } + return { route, random }; +} + +function syncPartyVitals( + state: RpgRoguelikeRunState, + updates: readonly PartyVitalUpdate[] | undefined, + playerHp?: number, +): RpgRoguelikeRunState { + if (!updates?.length && playerHp === undefined) return state; + const byId = new Map((updates ?? []).map((update) => [update.instanceId, update.hp])); + const roster = state.roster.map((member) => { + const hp = byId.get(member.instanceId); + return hp === undefined + ? member + : { ...member, hp: Math.max(0, Math.min(member.stats.maxHp, hp)) }; + }); + return { + ...state, + playerHp: playerHp === undefined ? state.playerHp : Math.max(0, Math.min(100, playerHp)), + roster, + }; +} + +function prepareEncounter(state: RpgRoguelikeRunState): RpgRoguelikeRunState { + if (state.bossIndex >= TOTAL_BOSS_COUNT) return { ...state, phase: "victory", currentChallenge: null }; + if (state.bossIndex >= ACT_BOSS_COUNT) { + return { ...state, phase: "boss-briefing", currentChallenge: null, lastChallengeResult: null }; + } + const selected = selectChallenge(state.random, state.challengeCounts); + return { + ...state, + random: selected.random, + phase: "challenge-briefing", + challengeCounts: selected.counts, + currentChallenge: { objective: selected.objective, metrics: emptyChallengeMetrics() }, + lastChallengeResult: null, + }; +} + +export function createRpgRoguelikeRun(config: RpgRoguelikeRunConfig): RpgRoguelikeRunState { + let random = createRandomState(config.seed); + const route = generateBossRoute(random, config.bossPool); + random = route.random; + const partyOffers = generatePartyDraftOffers(random, 0); + random = partyOffers.random; + return { + version: 1, + seed: random.seed, + random, + phase: "party-draft", + partyDraft: { waveIndex: 0, offers: partyOffers.offers, recruitedThisWaveIds: [] }, + spellDraft: null, + roster: [], + selectedSpellIds: [], + abilityLoadout: {}, + spellRanks: {}, + playerHp: 100, + bossRoute: route.route, + bossIndex: 0, + bossesDefeated: 0, + challengeCounts: emptyChallengeCounts(), + currentChallenge: null, + lastChallengeResult: null, + nextChestQualityBonus: 0, + pendingReward: null, + equipment: {}, + bag: [], + currency: Math.max(0, Math.floor(config.startingCurrency ?? 80)), + shop: null, + }; +} + +/** Mirrors draft replacement capacity so controller/UI never expose inert removals. */ +export function canRemovePartyMember(state: RpgRoguelikeRunState, memberId: string): boolean { + const draft = state.partyDraft; + if (state.phase !== "party-draft" + || !draft + || draft.waveIndex === 0 + || !state.roster.some((member) => member.instanceId === memberId)) return false; + if (draft.waveIndex < PARTY_DRAFT_WAVE_COUNT - 1) return true; + + const removingCurrentPick = draft.recruitedThisWaveIds.includes(memberId); + const picksAfterRemoval = draft.recruitedThisWaveIds.length - Number(removingCurrentPick); + const maximumFinalRoster = state.roster.length - 1 + (PARTY_RECRUITS_PER_WAVE - picksAfterRemoval); + return maximumFinalRoster >= MAX_ACTIVE_ROSTER; +} + +function reducePartyDraft(state: RpgRoguelikeRunState, action: RpgRoguelikeAction): RpgRoguelikeRunState { + const draft = state.partyDraft; + if (state.phase !== "party-draft" || !draft) return state; + if (action.type === "party-recruit") { + const candidate = draft.offers.find((offer) => offer.candidateId === action.candidateId); + if (!candidate + || state.roster.some((member) => member.instanceId === candidate.candidateId) + || !canRecruitCandidate(state.roster, draft.recruitedThisWaveIds.length)) return state; + const roster = [...state.roster, recruitCandidate(candidate)]; + return { + ...state, + roster, + partyDraft: { ...draft, recruitedThisWaveIds: [...draft.recruitedThisWaveIds, candidate.candidateId] }, + }; + } + if (action.type === "party-remove") { + if (!canRemovePartyMember(state, action.memberId)) return state; + return { + ...state, + roster: state.roster.filter((member) => member.instanceId !== action.memberId), + partyDraft: { + ...draft, + recruitedThisWaveIds: draft.recruitedThisWaveIds.filter((id) => id !== action.memberId), + }, + }; + } + if (action.type !== "party-next-wave") return state; + if (draft.waveIndex < PARTY_DRAFT_WAVE_COUNT - 1) { + const remainingWaves = PARTY_DRAFT_WAVE_COUNT - draft.waveIndex - 1; + if (state.roster.length + remainingWaves * PARTY_RECRUITS_PER_WAVE < MAX_ACTIVE_ROSTER) return state; + const generated = generatePartyDraftOffers(state.random, draft.waveIndex + 1); + return { + ...state, + random: generated.random, + partyDraft: { waveIndex: draft.waveIndex + 1, offers: generated.offers, recruitedThisWaveIds: [] }, + }; + } + if (state.roster.length !== MAX_ACTIVE_ROSTER || !state.roster.some((member) => member.role === "Tank")) return state; + const generated = generateSpellDraftOffers(state.random); + return { + ...state, + random: generated.random, + phase: "spell-draft", + partyDraft: null, + spellDraft: { + waveIndex: 0, + offers: generated.offers, + pickedThisWaveIds: [], + seenOfferIds: generated.offers, + }, + }; +} + +function reduceSpellDraft(state: RpgRoguelikeRunState, action: RpgRoguelikeAction): RpgRoguelikeRunState { + const draft = state.spellDraft; + if (state.phase !== "spell-draft" || !draft) return state; + if (action.type === "spell-pick") { + if (!draft.offers.includes(action.spellId) + || !canPickSpell(state.selectedSpellIds, draft.pickedThisWaveIds.length, action.spellId)) return state; + const selectedSpellIds = [...state.selectedSpellIds, action.spellId]; + return { + ...state, + selectedSpellIds, + abilityLoadout: addSpellToAbilityLoadout(state.abilityLoadout, action.spellId), + spellDraft: { ...draft, pickedThisWaveIds: [...draft.pickedThisWaveIds, action.spellId] }, + }; + } + if (action.type === "spell-remove") { + if (draft.waveIndex === 0) return state; + if (!state.selectedSpellIds.includes(action.spellId)) return state; + const selectedSpellIds = state.selectedSpellIds.filter((spellId) => spellId !== action.spellId); + return { + ...state, + selectedSpellIds, + abilityLoadout: removeSpellFromAbilityLoadout(state.abilityLoadout, action.spellId), + spellDraft: { + ...draft, + pickedThisWaveIds: draft.pickedThisWaveIds.filter((spellId) => spellId !== action.spellId), + }, + }; + } + if (action.type !== "spell-next-wave") return state; + if (draft.waveIndex < SPELL_DRAFT_WAVE_COUNT - 1) { + const generated = generateSpellDraftOffers(state.random, draft.seenOfferIds); + return { + ...state, + random: generated.random, + spellDraft: { + waveIndex: draft.waveIndex + 1, + offers: generated.offers, + pickedThisWaveIds: [], + seenOfferIds: [...draft.seenOfferIds, ...generated.offers], + }, + }; + } + if (!state.selectedSpellIds.length) return state; + return prepareEncounter({ ...state, spellDraft: null }); +} + +function completeReward(state: RpgRoguelikeRunState, choiceId: string): RpgRoguelikeRunState { + const choice = state.pendingReward?.choices.find((candidate) => candidate.id === choiceId); + if (!choice || state.phase !== "reward") return state; + let next = applyRewardChoice(state, choice); + next = { ...next, pendingReward: null }; + if (state.bossIndex === TOTAL_BOSS_COUNT - 1) return { ...next, phase: "victory" }; + + const bossIndex = state.bossIndex + 1; + if (state.bossesDefeated <= ACT_BOSS_COUNT && state.bossesDefeated % BOSSES_PER_ACT === 0) { + const generated = generateRunShop(next.random, next, state.bossesDefeated / BOSSES_PER_ACT); + return { ...next, random: generated.random, bossIndex, phase: "shop", shop: generated.shop }; + } + return prepareEncounter({ ...next, bossIndex }); +} + +export function reduceRpgRoguelikeRun( + state: RpgRoguelikeRunState, + action: RpgRoguelikeAction, +): RpgRoguelikeRunState { + if (state.phase === "victory" || state.phase === "defeat") return state; + if (state.phase === "party-draft") return reducePartyDraft(state, action); + if (state.phase === "spell-draft") return reduceSpellDraft(state, action); + + if (action.type === "party-vitals") return syncPartyVitals(state, action.vitals, action.playerHp); + if (action.type === "challenge-start" && state.phase === "challenge-briefing" && state.currentChallenge) { + return { ...state, phase: "challenge-active" }; + } + if (action.type === "challenge-progress" && state.phase === "challenge-active" && state.currentChallenge) { + return { + ...state, + currentChallenge: { + ...state.currentChallenge, + metrics: mergeChallengeMetrics(state.currentChallenge.metrics, action.metrics), + }, + }; + } + if (action.type === "challenge-complete" && state.phase === "challenge-active" && state.currentChallenge) { + const metrics = mergeChallengeMetrics(state.currentChallenge.metrics, action.metrics ?? {}); + const evaluated = evaluateChallenge(state.currentChallenge.objective, metrics); + const result = action.forcedFailure ? { ...evaluated, succeeded: false } : evaluated; + return { + ...state, + phase: "boss-briefing", + currentChallenge: null, + lastChallengeResult: result, + currency: state.currency + (result.succeeded ? result.objective.rewardCurrency : 0), + nextChestQualityBonus: result.succeeded ? result.objective.chestQualityBonus : 0, + }; + } + if (action.type === "boss-start" && state.phase === "boss-briefing") return { ...state, phase: "boss-combat" }; + if (action.type === "boss-lost" && state.phase === "boss-combat") { + return { ...syncPartyVitals(state, action.vitals, action.playerHp), phase: "defeat", pendingReward: null }; + } + if (action.type === "boss-won" && state.phase === "boss-combat") { + const synced = syncPartyVitals(state, action.vitals, action.playerHp); + return { ...synced, phase: "boss-cleared", bossesDefeated: state.bossesDefeated + 1 }; + } + if (action.type === "reward-open" && state.phase === "boss-cleared" && !state.pendingReward) { + const baseQuality = Math.min(3, Math.floor(state.bossIndex / BOSSES_PER_ACT)); + const generated = generateRewardChest(state.random, state, baseQuality + state.nextChestQualityBonus); + return { + ...state, + random: generated.random, + phase: "reward", + pendingReward: generated.chest, + nextChestQualityBonus: 0, + }; + } + if (action.type === "reward-choose") return completeReward(state, action.choiceId); + if (state.phase === "shop") { + if (action.type === "shop-buy") return buyShopOffer(state, action.offerId); + if (action.type === "shop-sell") return sellBagItem(state, action.itemId); + if (action.type === "shop-rest") return restParty(state); + if (action.type === "shop-revive") return revivePartyMember(state, action.memberId); + if (action.type === "shop-leave") return prepareEncounter({ ...state, phase: "boss-briefing", shop: null }); + } + return state; +} + +export function selectCurrentBossId(state: RpgRoguelikeRunState): BossId | null { + return state.bossRoute[state.bossIndex] ?? null; +} + +export function selectCanRecruit(state: RpgRoguelikeRunState): boolean { + return state.phase === "party-draft" + && Boolean(state.partyDraft) + && canRecruitCandidate(state.roster, state.partyDraft?.recruitedThisWaveIds.length ?? 0); +} + +export function selectCanPickSpell(state: RpgRoguelikeRunState, spellId: HealerAbilityId): boolean { + return state.phase === "spell-draft" + && Boolean(state.spellDraft?.offers.includes(spellId)) + && canPickSpell(state.selectedSpellIds, state.spellDraft?.pickedThisWaveIds.length ?? 0, spellId); +} + +export function isRpgRoguelikeTerminal(state: RpgRoguelikeRunState): boolean { + return state.phase === "victory" || state.phase === "defeat"; +} diff --git a/src/game/rpgRoguelike/spells.test.ts b/src/game/rpgRoguelike/spells.test.ts new file mode 100644 index 0000000..45a8203 --- /dev/null +++ b/src/game/rpgRoguelike/spells.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import { HEALER_ABILITY_IDS } from "../healers"; +import { + buildRpgRoguelikeSpellPool, + generateSpellDraftOffers, + HEALER_SPELL_IDS, + RPG_ROGUELIKE_SPELL_POLICY, +} from "./spells"; +import { createRandomState } from "./random"; +import { SPELL_OFFERS_PER_WAVE } from "./types"; + +describe("RPG Roguelike healer spell discovery", () => { + it("automatically mirrors the canonical healer registry minus explicit blocks", () => { + const blocked = new Set(RPG_ROGUELIKE_SPELL_POLICY.blockedSpellIds); + expect(HEALER_SPELL_IDS).toEqual(HEALER_ABILITY_IDS.filter((spellId) => !blocked.has(spellId))); + }); + + it("supports a typed mode-specific deny-list without changing the healer registry", () => { + const blockedSpellId = HEALER_ABILITY_IDS[0]; + const pool = buildRpgRoguelikeSpellPool(HEALER_ABILITY_IDS, { blockedSpellIds: [blockedSpellId] }); + + expect(pool).not.toContain(blockedSpellId); + expect(pool).toEqual(HEALER_ABILITY_IDS.slice(1)); + expect(HEALER_ABILITY_IDS).toContain(blockedSpellId); + }); + + it("never reintroduces blocked spells when unseen offers run low", () => { + const blockedSpellId = HEALER_ABILITY_IDS[0]; + const eligible = buildRpgRoguelikeSpellPool(HEALER_ABILITY_IDS, { blockedSpellIds: [blockedSpellId] }); + const seen = eligible.slice(0, eligible.length - 1); + const left = generateSpellDraftOffers(createRandomState("blocked-fallback"), seen, eligible); + const right = generateSpellDraftOffers(createRandomState("blocked-fallback"), seen, eligible); + + expect(left).toEqual(right); + expect(left.offers).toHaveLength(SPELL_OFFERS_PER_WAVE); + expect(left.offers).not.toContain(blockedSpellId); + expect(left.offers.every((spellId) => eligible.includes(spellId))).toBe(true); + }); + + it("fails clearly when policy leaves too few draftable abilities", () => { + const minimal = HEALER_ABILITY_IDS.slice(0, SPELL_OFFERS_PER_WAVE); + expect(() => buildRpgRoguelikeSpellPool(minimal, { blockedSpellIds: [minimal[0]] })) + .toThrow(`RPG Roguelike needs at least ${SPELL_OFFERS_PER_WAVE} eligible healer abilities.`); + }); +}); diff --git a/src/game/rpgRoguelike/spells.ts b/src/game/rpgRoguelike/spells.ts new file mode 100644 index 0000000..ccff488 --- /dev/null +++ b/src/game/rpgRoguelike/spells.ts @@ -0,0 +1,107 @@ +import { HEALER_ABILITY_IDS } from "../healers"; +import type { AbilityLoadout, AbilitySlotId, HealerAbilityId } from "../types"; +import { shuffleWithRandom } from "./random"; +import type { RandomState } from "./types"; +import { MAX_EQUIPPED_SPELLS, MAX_SPELL_RANK, SPELL_OFFERS_PER_WAVE, SPELL_PICKS_PER_WAVE } from "./types"; + +export interface RpgRoguelikeSpellPolicy { + readonly blockedSpellIds: readonly HealerAbilityId[]; +} + +/** + * RPG-owned content policy. Add an ability ID here to suppress it from future + * drafts. Existing runs keep already-selected spells so saves stay valid. + */ +export const RPG_ROGUELIKE_SPELL_POLICY: RpgRoguelikeSpellPolicy = { + blockedSpellIds: [ + // "paladin-crusader-strike", + ], +}; + +export function buildRpgRoguelikeSpellPool( + registeredSpellIds: readonly HealerAbilityId[], + policy: RpgRoguelikeSpellPolicy = RPG_ROGUELIKE_SPELL_POLICY, +): HealerAbilityId[] { + const blockedSpellIds = new Set(policy.blockedSpellIds); + const eligibleSpellIds = registeredSpellIds.filter((spellId) => !blockedSpellIds.has(spellId)); + if (eligibleSpellIds.length < SPELL_OFFERS_PER_WAVE) { + throw new Error(`RPG Roguelike needs at least ${SPELL_OFFERS_PER_WAVE} eligible healer abilities.`); + } + return eligibleSpellIds; +} + +/** Compatibility export: canonical healer registry minus RPG-specific blocks. */ +export const HEALER_SPELL_IDS: readonly HealerAbilityId[] = Object.freeze( + buildRpgRoguelikeSpellPool(HEALER_ABILITY_IDS), +); + +export const ABILITY_LOADOUT_SLOTS: readonly AbilitySlotId[] = [ + "ability1", + "ability2", + "ability3", + "ability4", + "ability5", + "ability6", +]; + +export function generateSpellDraftOffers( + source: RandomState, + seenOfferIds: readonly HealerAbilityId[] = [], + eligibleSpellIds: readonly HealerAbilityId[] = HEALER_SPELL_IDS, +): { offers: HealerAbilityId[]; random: RandomState } { + const seen = new Set(seenOfferIds); + const unseen = eligibleSpellIds.filter((spellId) => !seen.has(spellId)); + const pool = unseen.length >= SPELL_OFFERS_PER_WAVE ? unseen : eligibleSpellIds; + const shuffled = shuffleWithRandom(source, pool); + return { offers: shuffled.values.slice(0, SPELL_OFFERS_PER_WAVE), random: shuffled.random }; +} + +export function canPickSpell( + selectedSpellIds: readonly HealerAbilityId[], + picksThisWave: number, + spellId: HealerAbilityId, +): boolean { + return selectedSpellIds.length < MAX_EQUIPPED_SPELLS + && picksThisWave < SPELL_PICKS_PER_WAVE + && !selectedSpellIds.includes(spellId); +} + +/** Final controller order is deterministic draft order after removals. */ +export function abilityLoadoutFromSpellIds(spellIds: readonly HealerAbilityId[]): AbilityLoadout { + const loadout: AbilityLoadout = {}; + for (let index = 0; index < Math.min(MAX_EQUIPPED_SPELLS, spellIds.length); index += 1) { + loadout[ABILITY_LOADOUT_SLOTS[index]] = spellIds[index]; + } + return loadout; +} + +/** Fills the lowest open controller slot without moving retained spells. */ +export function addSpellToAbilityLoadout( + loadout: AbilityLoadout, + spellId: HealerAbilityId, +): AbilityLoadout { + if (Object.values(loadout).includes(spellId)) return loadout; + const slotId = ABILITY_LOADOUT_SLOTS.find((candidate) => !loadout[candidate]); + return slotId ? { ...loadout, [slotId]: spellId } : loadout; +} + +/** Removes one spell while preserving every other controller binding. */ +export function removeSpellFromAbilityLoadout( + loadout: AbilityLoadout, + spellId: HealerAbilityId, +): AbilityLoadout { + const next: AbilityLoadout = {}; + for (const slotId of ABILITY_LOADOUT_SLOTS) { + const current = loadout[slotId]; + if (current && current !== spellId) next[slotId] = current; + } + return next; +} + +export function incrementSpellRank( + ranks: Partial>, + spellId: HealerAbilityId, +): Partial> { + const current = Math.max(0, Math.min(MAX_SPELL_RANK, Math.floor(ranks[spellId] ?? 0))); + return current >= MAX_SPELL_RANK ? { ...ranks } : { ...ranks, [spellId]: current + 1 }; +} diff --git a/src/game/rpgRoguelike/types.ts b/src/game/rpgRoguelike/types.ts new file mode 100644 index 0000000..f5f655c --- /dev/null +++ b/src/game/rpgRoguelike/types.ts @@ -0,0 +1,278 @@ +import type { AbilityLoadout, BossId, HealerAbilityId } from "../types"; + +export type { AbilityLoadout } from "../types"; + +export const RPG_ROGUELIKE_MODE_ID = "rpg-roguelike" as const; +export const PARTY_DRAFT_WAVE_COUNT = 3; +export const PARTY_OFFERS_PER_WAVE = 5; +export const PARTY_RECRUITS_PER_WAVE = 3; +export const MAX_ACTIVE_ROSTER = 4; +export const SPELL_DRAFT_WAVE_COUNT = 3; +export const SPELL_OFFERS_PER_WAVE = 3; +export const SPELL_PICKS_PER_WAVE = 2; +export const MAX_EQUIPPED_SPELLS = 6; +export const ACT_COUNT = 3; +export const BOSSES_PER_ACT = 3; +export const ACT_BOSS_COUNT = ACT_COUNT * BOSSES_PER_ACT; +export const TOTAL_BOSS_COUNT = ACT_BOSS_COUNT + 1; +export const MAX_SPELL_RANK = 5; +export const MAX_RUN_GEAR_ENHANCEMENT = 5; + +export type RpgRunPhase = + | "party-draft" + | "spell-draft" + | "challenge-briefing" + | "challenge-active" + | "boss-briefing" + | "boss-combat" + | "boss-cleared" + | "reward" + | "shop" + | "victory" + | "defeat"; + +export interface RandomState { + readonly seed: number; + readonly state: number; + readonly draws: number; +} + +export type PartyRarity = "white" | "green" | "blue" | "purple" | "gold"; +export type PartyRole = "Tank" | "Damage"; +export type PartyClassId = "knight" | "ranger" | "mage" | "rogue" | "warrior" | "warlock" | "monk"; +export type PartyArchetypeId = + | "knight-tank" + | "ranger-damage" + | "mage-damage" + | "rogue-damage" + | "warrior-tank" + | "warrior-damage" + | "warlock-damage" + | "monk-tank" + | "monk-damage"; +export type PartyTraitId = + | "steadfast" + | "battle-hardened" + | "guardian-instinct" + | "royal-aegis" + | "sure-footed" + | "eagle-eye" + | "rapid-volley" + | "perfect-shot" + | "arcane-focus" + | "spell-echo" + | "volatile-power" + | "astral-mastery" + | "light-footed" + | "exploit-opening" + | "blade-dance" + | "shadow-master" + | "iron-blood" + | "whirlwind" + | "war-cry" + | "unstoppable" + | "soul-drain" + | "doom-mark" + | "dark-pact" + | "soul-tyrant" + | "centered" + | "counterstrike" + | "flow-state" + | "perfect-form"; + +export interface PartyCombatStats { + readonly maxHp: number; + readonly singleTarget: number; + readonly areaDamage: number; + readonly defense: number; + readonly moveSpeed: number; +} + +export interface PartyDraftCandidate { + readonly candidateId: string; + readonly name: string; + readonly archetypeId: PartyArchetypeId; + readonly classId: PartyClassId; + readonly className: string; + readonly role: PartyRole; + readonly rarity: PartyRarity; + readonly stats: PartyCombatStats; + readonly traitIds: PartyTraitId[]; + readonly color: string; +} + +export interface PartyRosterMember extends PartyDraftCandidate { + readonly instanceId: string; + readonly hp: number; +} + +export type AllyCombatSlotId = "brann" | "nia" | "orin" | "vale"; +export type CombatPartyAssignment = Record; + +export interface PartyDraftState { + readonly waveIndex: number; + readonly offers: PartyDraftCandidate[]; + readonly recruitedThisWaveIds: string[]; +} + +export interface SpellDraftState { + readonly waveIndex: number; + readonly offers: HealerAbilityId[]; + readonly pickedThisWaveIds: HealerAbilityId[]; + readonly seenOfferIds: HealerAbilityId[]; +} + +export type ChallengeId = "blockbreaker" | "hockey" | "aether-assault"; +export type ChallengeMetric = "bricksBroken" | "bossKills" | "kills"; + +export interface ChallengeMetrics { + readonly bricksBroken: number; + readonly bossKills: number; + readonly kills: number; +} + +export interface ChallengeObjective { + readonly challengeId: ChallengeId; + readonly name: string; + readonly metric: ChallengeMetric; + readonly target: number; + readonly repeatIndex: number; + readonly rewardCurrency: number; + readonly chestQualityBonus: number; +} + +export interface ChallengeRunState { + readonly objective: ChallengeObjective; + readonly metrics: ChallengeMetrics; +} + +export interface ChallengeResult { + readonly objective: ChallengeObjective; + readonly metrics: ChallengeMetrics; + readonly succeeded: boolean; +} + +export type RunGearSlotId = "weapon" | "armor" | "trinket"; +export type RunGearOwnerId = "player" | string; +export type RunGearStatId = "damage" | "maxHealth" | "haste"; + +export interface RunGearItem { + readonly id: string; + readonly ownerId: RunGearOwnerId; + readonly slotId: RunGearSlotId; + readonly enhancement: 0 | 1 | 2 | 3 | 4 | 5; + readonly name: string; + readonly statId: RunGearStatId; + readonly statValue: number; + readonly sellPrice: number; +} + +export type RunEquipment = Record>>; + +export type RewardChoice = + | { + readonly id: string; + readonly kind: "spell-rank"; + readonly spellId: HealerAbilityId; + readonly nextRank: number; + readonly label: string; + } + | { + readonly id: string; + readonly kind: "member-rarity"; + readonly memberId: string; + readonly nextRarity: PartyRarity; + readonly label: string; + } + | { + readonly id: string; + readonly kind: "run-gear"; + readonly item: RunGearItem; + readonly label: string; + } + | { + readonly id: string; + readonly kind: "currency"; + readonly amount: number; + readonly label: string; + }; + +export interface RewardChest { + readonly id: string; + readonly quality: number; + readonly choices: RewardChoice[]; +} + +export interface ShopOffer { + readonly id: string; + readonly item: RunGearItem; + readonly price: number; + readonly sold: boolean; +} + +export interface RunShopState { + readonly act: number; + readonly offers: ShopOffer[]; + readonly restCost: number; + readonly reviveCost: number; +} + +export interface PartyVitalUpdate { + readonly instanceId: string; + readonly hp: number; +} + +export interface RpgRoguelikeRunState { + readonly version: 1; + readonly seed: number; + readonly random: RandomState; + readonly phase: RpgRunPhase; + readonly partyDraft: PartyDraftState | null; + readonly spellDraft: SpellDraftState | null; + readonly roster: PartyRosterMember[]; + readonly selectedSpellIds: HealerAbilityId[]; + readonly abilityLoadout: AbilityLoadout; + readonly spellRanks: Partial>; + /** Gear-independent healer vitality, normalized against a 100 HP run base. */ + readonly playerHp: number; + readonly bossRoute: BossId[]; + readonly bossIndex: number; + readonly bossesDefeated: number; + readonly challengeCounts: Record; + readonly currentChallenge: ChallengeRunState | null; + readonly lastChallengeResult: ChallengeResult | null; + readonly nextChestQualityBonus: number; + readonly pendingReward: RewardChest | null; + readonly equipment: RunEquipment; + readonly bag: RunGearItem[]; + readonly currency: number; + readonly shop: RunShopState | null; +} + +export interface RpgRoguelikeRunConfig { + readonly seed: number | string; + readonly bossPool?: readonly BossId[]; + readonly startingCurrency?: number; +} + +export type RpgRoguelikeAction = + | { readonly type: "party-recruit"; readonly candidateId: string } + | { readonly type: "party-remove"; readonly memberId: string } + | { readonly type: "party-next-wave" } + | { readonly type: "spell-pick"; readonly spellId: HealerAbilityId } + | { readonly type: "spell-remove"; readonly spellId: HealerAbilityId } + | { readonly type: "spell-next-wave" } + | { readonly type: "challenge-start" } + | { readonly type: "challenge-progress"; readonly metrics: Partial } + | { readonly type: "challenge-complete"; readonly metrics?: Partial; readonly forcedFailure?: boolean } + | { readonly type: "boss-start" } + | { readonly type: "party-vitals"; readonly vitals: readonly PartyVitalUpdate[]; readonly playerHp?: number } + | { readonly type: "boss-won"; readonly vitals?: readonly PartyVitalUpdate[]; readonly playerHp?: number } + | { readonly type: "boss-lost"; readonly vitals?: readonly PartyVitalUpdate[]; readonly playerHp?: number } + | { readonly type: "reward-open" } + | { readonly type: "reward-choose"; readonly choiceId: string } + | { readonly type: "shop-buy"; readonly offerId: string } + | { readonly type: "shop-sell"; readonly itemId: string } + | { readonly type: "shop-rest" } + | { readonly type: "shop-revive"; readonly memberId: string } + | { readonly type: "shop-leave" }; diff --git a/src/game/rpgRoguelike/uiModel.test.ts b/src/game/rpgRoguelike/uiModel.test.ts new file mode 100644 index 0000000..39e3977 --- /dev/null +++ b/src/game/rpgRoguelike/uiModel.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, it } from "vitest"; +import { + MAX_ACTIVE_ROSTER, + PARTY_RECRUITS_PER_WAVE, + createRpgRoguelikeRun, + createRunGearItem, + moveRpgFocus, + reduceRpgRoguelikeRun, + rpgFocusId, + rpgFocusItems, + type RpgRoguelikeRunState, +} from "."; + +function reachSpellDraft(seed = 810): RpgRoguelikeRunState { + let state = createRpgRoguelikeRun({ seed }); + while (state.phase === "party-draft") { + const draft = state.partyDraft!; + const tank = draft.offers.find((offer) => offer.role === "Tank"); + const ordered = tank ? [tank, ...draft.offers.filter((offer) => offer !== tank)] : draft.offers; + for (const offer of ordered) { + if (state.roster.length >= MAX_ACTIVE_ROSTER || state.partyDraft!.recruitedThisWaveIds.length >= PARTY_RECRUITS_PER_WAVE) break; + state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: offer.candidateId }); + } + state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" }); + } + return state; +} + +describe("RPG Roguelike semantic UI focus", () => { + it("moves horizontally within card rows and vertically between action groups", () => { + let state = createRpgRoguelikeRun({ seed: 120 }); + const offers = state.partyDraft!.offers; + const first = rpgFocusId.partyOffer(offers[0].candidateId); + const second = rpgFocusId.partyOffer(offers[1].candidateId); + const last = rpgFocusId.partyOffer(offers[offers.length - 1].candidateId); + expect(moveRpgFocus(state, first, "right")).toBe(second); + expect(moveRpgFocus(state, first, "left")).toBe(last); + expect(moveRpgFocus(state, second, "down")).toBe(rpgFocusId.partyContinue); + + state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: offers[0].candidateId }); + state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" }); + const laterOffer = rpgFocusId.partyOffer(state.partyDraft!.offers[0].candidateId); + const rosterAction = rpgFocusId.partyMember(offers[0].candidateId); + expect(moveRpgFocus(state, laterOffer, "down")).toBe(rosterAction); + expect(moveRpgFocus(state, rosterAction, "down")).toBe(rpgFocusId.partyContinue); + }); + + it("orders party offers, later-wave roster removals, then Continue", () => { + let state = createRpgRoguelikeRun({ seed: 12 }); + const firstOffer = state.partyDraft!.offers[0]; + expect(rpgFocusItems(state).map((item) => item.id)).toEqual([ + ...state.partyDraft!.offers.map((offer) => rpgFocusId.partyOffer(offer.candidateId)), + rpgFocusId.partyContinue, + ]); + + state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: firstOffer.candidateId }); + expect(rpgFocusItems(state).map((item) => item.id)).not.toContain(rpgFocusId.partyMember(firstOffer.candidateId)); + state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" }); + expect(rpgFocusItems(state).map((item) => item.id)).toEqual([ + ...state.partyDraft!.offers.map((offer) => rpgFocusId.partyOffer(offer.candidateId)), + rpgFocusId.partyMember(firstOffer.candidateId), + rpgFocusId.partyContinue, + ]); + }); + + it("omits final-wave removals that cannot be refilled", () => { + let state = createRpgRoguelikeRun({ seed: 91 }); + state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" }); + const prior = state.partyDraft!.offers.find((offer) => offer.role === "Tank")!; + state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: prior.candidateId }); + state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" }); + for (const offer of state.partyDraft!.offers.slice(0, 3)) { + state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: offer.candidateId }); + } + + expect(state.roster).toHaveLength(4); + expect(rpgFocusItems(state).map((item) => item.id)).not.toContain(rpgFocusId.partyMember(prior.candidateId)); + expect(reduceRpgRoguelikeRun(state, { type: "party-remove", memberId: prior.candidateId })).toBe(state); + }); + + it("locks first-wave spell picks, then exposes selected removals before Continue", () => { + let state = reachSpellDraft(); + const spellId = state.spellDraft!.offers[0]; + state = reduceRpgRoguelikeRun(state, { type: "spell-pick", spellId }); + expect(rpgFocusItems(state).map((item) => item.id)).not.toContain(rpgFocusId.spellSelected(spellId)); + + state = reduceRpgRoguelikeRun(state, { type: "spell-next-wave" }); + expect(rpgFocusItems(state).map((item) => item.id)).toEqual([ + ...state.spellDraft!.offers.map((offer) => rpgFocusId.spellOffer(offer)), + rpgFocusId.spellSelected(spellId), + rpgFocusId.spellContinue, + ]); + }); + + it("orders shop purchases, bag sales, Rest, Revive, then Leave and omits disabled actions", () => { + const drafted = reachSpellDraft(44); + const offerItem = createRunGearItem("shop-weapon", "player", "weapon", 1); + const soldItem = createRunGearItem("sold-armor", "player", "armor", 1); + const bagItem = createRunGearItem("bag-trinket", "player", "trinket", 0); + const first = drafted.roster[0]; + const second = drafted.roster[1]; + const state: RpgRoguelikeRunState = { + ...drafted, + phase: "shop", + currency: 200, + roster: drafted.roster.map((member) => member.instanceId === first.instanceId + ? { ...member, hp: 0 } + : member.instanceId === second.instanceId + ? { ...member, hp: 1 } + : member), + bag: [bagItem], + shop: { + act: 1, + offers: [ + { id: "available", item: offerItem, price: 80, sold: false }, + { id: "sold", item: soldItem, price: 20, sold: true }, + ], + restCost: 35, + reviveCost: 60, + }, + }; + expect(rpgFocusItems(state).map((item) => item.id)).toEqual([ + rpgFocusId.shopOffer("available"), + rpgFocusId.shopSell(bagItem.id), + rpgFocusId.shopRest, + rpgFocusId.shopRevive(first.instanceId), + rpgFocusId.shopLeave, + ]); + }); +}); diff --git a/src/game/rpgRoguelike/uiModel.ts b/src/game/rpgRoguelike/uiModel.ts new file mode 100644 index 0000000..de63686 --- /dev/null +++ b/src/game/rpgRoguelike/uiModel.ts @@ -0,0 +1,247 @@ +import type { RpgRoguelikeAction, RpgRoguelikeRunState } from "./types"; +import { canRemovePartyMember } from "./run"; +import { + MAX_ACTIVE_ROSTER, + MAX_EQUIPPED_SPELLS, + PARTY_DRAFT_WAVE_COUNT, + PARTY_RECRUITS_PER_WAVE, + SPELL_DRAFT_WAVE_COUNT, + SPELL_PICKS_PER_WAVE, +} from "./types"; + +export type RpgUiCommand = + | { readonly type: "run-action"; readonly action: RpgRoguelikeAction } + | { readonly type: "restart-run" } + | { readonly type: "exit-run" }; + +export interface RpgFocusItem { + readonly id: string; + readonly label: string; + readonly command: RpgUiCommand; +} + +export type RpgFocusDirection = "left" | "right" | "up" | "down"; + +export const rpgFocusId = { + partyOffer: (candidateId: string) => `party-offer:${candidateId}`, + partyMember: (memberId: string) => `party-member:${memberId}`, + partyContinue: "party-continue", + spellOffer: (spellId: string) => `spell-offer:${spellId}`, + spellSelected: (spellId: string) => `spell-selected:${spellId}`, + spellContinue: "spell-continue", + challengeStart: "challenge-start", + bossStart: "boss-start", + rewardOpen: "reward-open", + rewardChoice: (choiceId: string) => `reward:${choiceId}`, + shopOffer: (offerId: string) => `shop-offer:${offerId}`, + shopSell: (itemId: string) => `shop-sell:${itemId}`, + shopRest: "shop-rest", + shopRevive: (memberId: string) => `shop-revive:${memberId}`, + shopLeave: "shop-leave", + restartRun: "restart-run", + exitRun: "exit-run", +} as const; + +function action(id: string, label: string, runAction: RpgRoguelikeAction): RpgFocusItem { + return { id, label, command: { type: "run-action", action: runAction } }; +} + +function canAdvancePartyDraft(state: RpgRoguelikeRunState): boolean { + const draft = state.partyDraft; + if (!draft) return false; + if (draft.waveIndex === PARTY_DRAFT_WAVE_COUNT - 1) { + return state.roster.length === MAX_ACTIVE_ROSTER && state.roster.some((member) => member.role === "Tank"); + } + const remainingWaves = PARTY_DRAFT_WAVE_COUNT - draft.waveIndex - 1; + return state.roster.length + remainingWaves * PARTY_RECRUITS_PER_WAVE >= MAX_ACTIVE_ROSTER; +} + +function canAdvanceSpellDraft(state: RpgRoguelikeRunState): boolean { + const draft = state.spellDraft; + return Boolean(draft && (draft.waveIndex < SPELL_DRAFT_WAVE_COUNT - 1 || state.selectedSpellIds.length > 0)); +} + +/** Canonical ordered semantic targets shared by controller and both displays. */ +export function rpgFocusItems(state: RpgRoguelikeRunState): RpgFocusItem[] { + if (state.phase === "party-draft" && state.partyDraft) { + const draft = state.partyDraft; + const canRemove = draft.waveIndex > 0; + const canRecruit = state.roster.length < MAX_ACTIVE_ROSTER + && draft.recruitedThisWaveIds.length < PARTY_RECRUITS_PER_WAVE; + const offerItems = draft.offers.flatMap((candidate) => { + const recruited = state.roster.some((member) => member.instanceId === candidate.candidateId); + if ((recruited && !canRemovePartyMember(state, candidate.candidateId)) || (!recruited && !canRecruit)) return []; + return [action( + rpgFocusId.partyOffer(candidate.candidateId), + `${recruited ? "Remove" : "Recruit"} ${candidate.name}`, + recruited + ? { type: "party-remove", memberId: candidate.candidateId } + : { type: "party-recruit", candidateId: candidate.candidateId }, + )]; + }); + const rosterItems = canRemove ? state.roster.filter((member) => canRemovePartyMember(state, member.instanceId)).map((member) => action( + rpgFocusId.partyMember(member.instanceId), + `Remove ${member.name}`, + { type: "party-remove", memberId: member.instanceId }, + )) : []; + return [ + ...offerItems, + ...rosterItems, + ...(canAdvancePartyDraft(state) + ? [action(rpgFocusId.partyContinue, "Next party draft", { type: "party-next-wave" })] + : []), + ]; + } + + if (state.phase === "spell-draft" && state.spellDraft) { + const draft = state.spellDraft; + const canRemove = draft.waveIndex > 0; + const canPick = state.selectedSpellIds.length < MAX_EQUIPPED_SPELLS + && draft.pickedThisWaveIds.length < SPELL_PICKS_PER_WAVE; + const offerItems = draft.offers.flatMap((spellId) => { + const selected = state.selectedSpellIds.includes(spellId); + if ((selected && !canRemove) || (!selected && !canPick)) return []; + return [action( + rpgFocusId.spellOffer(spellId), + `${selected ? "Remove" : "Learn"} ${spellId}`, + selected ? { type: "spell-remove", spellId } : { type: "spell-pick", spellId }, + )]; + }); + const selectedItems = canRemove ? state.selectedSpellIds.map((spellId) => action( + rpgFocusId.spellSelected(spellId), + `Remove ${spellId}`, + { type: "spell-remove", spellId }, + )) : []; + return [ + ...offerItems, + ...selectedItems, + ...(canAdvanceSpellDraft(state) + ? [action(rpgFocusId.spellContinue, "Next spell draft", { type: "spell-next-wave" })] + : []), + ]; + } + + if (state.phase === "challenge-briefing" && state.currentChallenge) { + return [action(rpgFocusId.challengeStart, "Start challenge", { type: "challenge-start" })]; + } + if (state.phase === "boss-briefing") { + return [action(rpgFocusId.bossStart, "Enter boss room", { type: "boss-start" })]; + } + if (state.phase === "boss-cleared") { + return [action(rpgFocusId.rewardOpen, "Open reward chest", { type: "reward-open" })]; + } + if (state.phase === "reward" && state.pendingReward) { + return state.pendingReward.choices.map((choice) => action( + rpgFocusId.rewardChoice(choice.id), + `Choose ${choice.label}`, + { type: "reward-choose", choiceId: choice.id }, + )); + } + if (state.phase === "shop" && state.shop) { + const items: RpgFocusItem[] = state.shop.offers.flatMap((offer) => ( + !offer.sold && state.currency >= offer.price + ? [action(rpgFocusId.shopOffer(offer.id), `Buy ${offer.item.name}`, { type: "shop-buy", offerId: offer.id })] + : [] + )); + items.push(...state.bag.map((item) => action( + rpgFocusId.shopSell(item.id), + `Sell ${item.name}`, + { type: "shop-sell", itemId: item.id }, + ))); + const needsRest = state.playerHp > 0 && state.playerHp < 100 + || state.roster.some((member) => member.hp > 0 && member.hp < member.stats.maxHp); + if (needsRest && state.currency >= state.shop.restCost) { + items.push(action(rpgFocusId.shopRest, "Rest party", { type: "shop-rest" })); + } + if (state.currency >= state.shop.reviveCost) { + items.push(...state.roster.filter((member) => member.hp <= 0).map((member) => action( + rpgFocusId.shopRevive(member.instanceId), + `Revive ${member.name}`, + { type: "shop-revive", memberId: member.instanceId }, + ))); + } + items.push(action(rpgFocusId.shopLeave, "Leave shop", { type: "shop-leave" })); + return items; + } + if (state.phase === "victory" || state.phase === "defeat") { + return [ + { id: rpgFocusId.restartRun, label: "Start a new run", command: { type: "restart-run" } }, + { id: rpgFocusId.exitRun, label: "Return to mode select", command: { type: "exit-run" } }, + ]; + } + return []; +} + +export function resolveRpgFocusCommand( + state: RpgRoguelikeRunState, + focusId: string | null | undefined, +): RpgUiCommand | null { + const items = rpgFocusItems(state); + return items.find((item) => item.id === focusId)?.command ?? items[0]?.command ?? null; +} + +export function normalizeRpgFocusId( + state: RpgRoguelikeRunState, + focusId: string | null | undefined, +): string | null { + const items = rpgFocusItems(state); + return items.some((item) => item.id === focusId) ? focusId ?? null : items[0]?.id ?? null; +} + +function rpgFocusRows(state: RpgRoguelikeRunState, items: readonly RpgFocusItem[]): string[][] { + const enabled = new Set(items.map((item) => item.id)); + const keep = (ids: readonly string[]) => ids.filter((id) => enabled.has(id)); + if (state.phase === "party-draft" && state.partyDraft) { + return [ + keep(state.partyDraft.offers.map((offer) => rpgFocusId.partyOffer(offer.candidateId))), + keep(state.roster.map((member) => rpgFocusId.partyMember(member.instanceId))), + keep([rpgFocusId.partyContinue]), + ].filter((row) => row.length > 0); + } + if (state.phase === "spell-draft" && state.spellDraft) { + return [ + keep(state.spellDraft.offers.map((spellId) => rpgFocusId.spellOffer(spellId))), + keep(state.selectedSpellIds.map((spellId) => rpgFocusId.spellSelected(spellId))), + keep([rpgFocusId.spellContinue]), + ].filter((row) => row.length > 0); + } + if (state.phase === "shop" && state.shop) { + return [ + keep(state.shop.offers.map((offer) => rpgFocusId.shopOffer(offer.id))), + keep([ + ...state.bag.map((item) => rpgFocusId.shopSell(item.id)), + rpgFocusId.shopRest, + ...state.roster.map((member) => rpgFocusId.shopRevive(member.instanceId)), + ]), + keep([rpgFocusId.shopLeave]), + ].filter((row) => row.length > 0); + } + return items.length ? [items.map((item) => item.id)] : []; +} + +/** Four-direction semantic navigation for card grids and stacked action groups. */ +export function moveRpgFocus( + state: RpgRoguelikeRunState, + focusId: string | null | undefined, + direction: RpgFocusDirection, +): string | null { + const items = rpgFocusItems(state); + if (!items.length) return null; + const current = normalizeRpgFocusId(state, focusId) ?? items[0].id; + const rows = rpgFocusRows(state, items); + const rowIndex = rows.findIndex((row) => row.includes(current)); + if (rowIndex < 0) return items[0].id; + const row = rows[rowIndex]; + const columnIndex = row.indexOf(current); + if (direction === "left" || direction === "right") { + const offset = direction === "left" ? -1 : 1; + return row[(columnIndex + offset + row.length) % row.length]; + } + const targetRowIndex = rowIndex + (direction === "up" ? -1 : 1); + if (targetRowIndex < 0 || targetRowIndex >= rows.length) return current; + const targetRow = rows[targetRowIndex]; + const proportionalColumn = row.length <= 1 + ? 0 + : Math.round(columnIndex / (row.length - 1) * Math.max(0, targetRow.length - 1)); + return targetRow[Math.min(targetRow.length - 1, proportionalColumn)]; +} diff --git a/src/game/rpgRoguelikeStore.test.ts b/src/game/rpgRoguelikeStore.test.ts new file mode 100644 index 0000000..292808b --- /dev/null +++ b/src/game/rpgRoguelikeStore.test.ts @@ -0,0 +1,300 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { ARENA_CENTER, ARENA_WALL_RADIUS } from "./arena"; +import { createClassInventory } from "./healers"; +import { createDefaultGearProgress } from "./progression/gear"; +import { equipPassiveInfusion } from "./progression/infusions"; +import { MAX_ACTIVE_ROSTER, PARTY_RECRUITS_PER_WAVE } from "./rpgRoguelike"; +import { useGameStore } from "./store"; + +function finishRpgDrafts() { + let safety = 0; + while (useGameStore.getState().rpgRun?.phase === "party-draft" && safety++ < 20) { + const run = useGameStore.getState().rpgRun!; + const draft = run.partyDraft!; + const tank = draft.offers.find((offer) => offer.role === "Tank"); + const offers = tank ? [tank, ...draft.offers.filter((offer) => offer !== tank)] : draft.offers; + for (const offer of offers) { + const current = useGameStore.getState().rpgRun!; + if (current.roster.length >= MAX_ACTIVE_ROSTER) break; + if ((current.partyDraft?.recruitedThisWaveIds.length ?? 0) >= PARTY_RECRUITS_PER_WAVE) break; + useGameStore.getState().dispatchRpgAction({ type: "party-recruit", candidateId: offer.candidateId }); + } + useGameStore.getState().dispatchRpgAction({ type: "party-next-wave" }); + } + while (useGameStore.getState().rpgRun?.phase === "spell-draft" && safety++ < 40) { + const offers = useGameStore.getState().rpgRun!.spellDraft!.offers.slice(0, 2); + for (const spellId of offers) useGameStore.getState().dispatchRpgAction({ type: "spell-pick", spellId }); + useGameStore.getState().dispatchRpgAction({ type: "spell-next-wave" }); + } + if (safety >= 40) throw new Error("RPG draft did not terminate"); +} + +function silenceBosses() { + useGameStore.setState((state) => ({ + boss: { ...state.boss, nextMeleeAt: 999 }, + bossMotion: { ...state.bossMotion, nextMechanicAt: 999 }, + additionalBosses: state.additionalBosses.map((entry) => ({ + ...entry, + boss: { ...entry.boss, nextMeleeAt: 999 }, + motion: { ...entry.motion, nextMechanicAt: 999 }, + })), + })); +} + +function completeCurrentChallenge() { + const objective = useGameStore.getState().rpgRun!.currentChallenge!.objective; + useGameStore.setState((state) => ({ + endlessBossKills: objective.metric === "bossKills" ? objective.target : state.endlessBossKills, + blockbreaker: objective.metric === "bricksBroken" + ? { ...state.blockbreaker, bricksBroken: objective.target } + : state.blockbreaker, + aetherAssault: objective.metric === "kills" + ? { ...state.aetherAssault, kills: objective.target } + : state.aetherAssault, + })); + silenceBosses(); + useGameStore.getState().tick(0.1); +} + +describe("RPG Roguelike store integration", () => { + beforeEach(() => { + useGameStore.getState().configureHealer( + "priest", + "Aelia", + createClassInventory("priest"), + "bulldrome", + "rpg-roguelike", + ); + }); + + it("starts in a deterministic gated party draft with semantic focus", () => { + const state = useGameStore.getState(); + expect(state.runMode).toBe("rpg-roguelike"); + expect(state.phase).toBe("briefing"); + expect(state.rpgRun?.phase).toBe("party-draft"); + expect(state.rpgRun?.partyDraft?.offers).toHaveLength(5); + expect(state.abilityLoadout).toEqual({}); + expect(state.rpgFocusId).toMatch(/^party-offer:/); + }); + + it("starts with neutral permanent gear and infusion modifiers", () => { + const progress = createDefaultGearProgress(); + progress.vale.slots.feet.level = 10; + const infused = equipPassiveInfusion(progress, "priest", "mend-efficiency"); + useGameStore.getState().configureHealer( + "priest", + "Aelia", + createClassInventory("priest"), + "bulldrome", + "rpg-roguelike", + infused, + ); + + const state = useGameStore.getState(); + expect(state.passiveRunBuffId).toBeNull(); + expect(state.runModifiers.mendManaMultiplier).toBe(1); + expect(state.gearModifiers.aelia.maxHealth).toBe(1); + }); + + it("projects four drafted companions and a mixed six-spell loadout into a challenge", () => { + finishRpgDrafts(); + const drafted = useGameStore.getState().rpgRun!; + expect(drafted.phase).toBe("challenge-briefing"); + expect(drafted.roster).toHaveLength(4); + expect(drafted.selectedSpellIds).toHaveLength(6); + + expect(useGameStore.getState().dispatchRpgAction({ type: "challenge-start" })).toBe(true); + const state = useGameStore.getState(); + expect(state.phase).toBe("combat"); + expect(state.rpgRun?.phase).toBe("challenge-active"); + expect(state.party).toHaveLength(5); + expect(state.party.slice(1).every((member) => member.runProfile)).toBe(true); + expect(Object.values(state.abilityLoadout)).toEqual(drafted.selectedSpellIds); + expect(state.endlessMode).toBe(true); + }); + + it("tracks Paladin and Chronomancer resources independently in mixed spell runs", () => { + finishRpgDrafts(); + useGameStore.getState().dispatchRpgAction({ type: "challenge-start" }); + silenceBosses(); + useGameStore.setState((state) => ({ + abilityLoadout: { + ability1: "paladin-crusader-strike", + ability2: "paladin-word-of-glory", + ability3: "chronomancer-time-anchor", + }, + selectedMemberId: "brann", + party: state.party.map((member) => member.id === "brann" ? { ...member, hp: 40 } : member), + })); + + expect(useGameStore.getState().castAbility("ability1")).toBe(true); + expect(useGameStore.getState().rpgSpellResources.conviction).toBe(1); + expect(useGameStore.getState().healerMechanic.resource).toBe(0); + useGameStore.getState().tick(0.51); + expect(useGameStore.getState().castAbility("ability2")).toBe(true); + expect(useGameStore.getState().rpgSpellResources.conviction).toBe(0); + + useGameStore.getState().tick(0.51); + expect(useGameStore.getState().castAbility("ability3")).toBe(true); + useGameStore.getState().tick(0.51); + useGameStore.setState((state) => ({ + party: state.party.map((member) => member.id === "brann" ? { ...member, hp: 30 } : member), + })); + expect(useGameStore.getState().castAbility("ability3")).toBe(true); + expect(useGameStore.getState().rpgSpellResources.chronoshards).toBe(1); + }); + + it("applies public in-combat RPG actions without rebuilding the encounter", () => { + finishRpgDrafts(); + useGameStore.getState().dispatchRpgAction({ type: "challenge-start" }); + const member = useGameStore.getState().rpgRun!.roster[0]; + useGameStore.setState((state) => ({ + time: 12, + mana: 37, + boss: { ...state.boss, hp: state.boss.maxHp * 0.5 }, + })); + const bossHp = useGameStore.getState().boss.hp; + + useGameStore.getState().dispatchRpgAction({ + type: "party-vitals", + vitals: [{ instanceId: member.instanceId, hp: member.hp - 1 }], + }); + + const state = useGameStore.getState(); + expect(state.time).toBe(12); + expect(state.mana).toBe(37); + expect(state.boss.hp).toBe(bossHp); + expect(state.rpgRun?.roster.find((candidate) => candidate.instanceId === member.instanceId)?.hp).toBe(member.hp - 1); + }); + + it("turns challenge failure into a boss briefing instead of ending the run", () => { + finishRpgDrafts(); + useGameStore.getState().dispatchRpgAction({ type: "challenge-start" }); + silenceBosses(); + useGameStore.setState((state) => ({ party: state.party.map((member) => ({ ...member, hp: 0 })) })); + + useGameStore.getState().tick(0.1); + + const state = useGameStore.getState(); + expect(state.rpgRun?.phase).toBe("boss-briefing"); + expect(state.rpgRun?.lastChallengeResult?.succeeded).toBe(false); + expect(state.phase).toBe("briefing"); + expect(state.endlessMode).toBe(false); + }); + + it("ends the run when the healer falls, even if companions finish the boss", () => { + finishRpgDrafts(); + useGameStore.getState().dispatchRpgAction({ type: "challenge-start" }); + const objective = useGameStore.getState().rpgRun!.currentChallenge!.objective; + useGameStore.setState((state) => ({ + endlessBossKills: objective.metric === "bossKills" ? objective.target : state.endlessBossKills, + blockbreaker: objective.metric === "bricksBroken" + ? { ...state.blockbreaker, bricksBroken: objective.target } + : state.blockbreaker, + aetherAssault: objective.metric === "kills" + ? { ...state.aetherAssault, kills: objective.target } + : state.aetherAssault, + })); + silenceBosses(); + useGameStore.getState().tick(0.1); + useGameStore.getState().dispatchRpgAction({ type: "boss-start" }); + silenceBosses(); + useGameStore.setState((state) => ({ + boss: { ...state.boss, hp: 0 }, + party: state.party.map((member) => member.id === "aelia" ? { ...member, hp: 0 } : member), + })); + + useGameStore.getState().tick(0.1); + + const state = useGameStore.getState(); + expect(state.phase).toBe("defeat"); + expect(state.rpgRun?.phase).toBe("defeat"); + expect(state.rpgRun?.playerHp).toBe(0); + expect(state.rpgRun?.bossesDefeated).toBe(0); + }); + + it("ends the run when every companion falls, even if the healer and boss remain", () => { + finishRpgDrafts(); + useGameStore.getState().dispatchRpgAction({ type: "challenge-start" }); + completeCurrentChallenge(); + useGameStore.getState().dispatchRpgAction({ type: "boss-start" }); + silenceBosses(); + useGameStore.setState((state) => ({ + party: state.party.map((member) => member.id === "aelia" ? member : { ...member, hp: 0 }), + })); + + useGameStore.getState().tick(0.1); + + expect(useGameStore.getState().phase).toBe("defeat"); + expect(useGameStore.getState().rpgRun?.phase).toBe("defeat"); + expect(useGameStore.getState().rpgRun?.bossesDefeated).toBe(0); + }); + + it("targets a living companion at room start and blocks casts after the boss is cleared", () => { + finishRpgDrafts(); + useGameStore.getState().dispatchRpgAction({ type: "challenge-start" }); + completeCurrentChallenge(); + const first = useGameStore.getState().rpgRun!.roster[0]; + useGameStore.getState().dispatchRpgAction({ + type: "party-vitals", + vitals: [{ instanceId: first.instanceId, hp: 0 }], + }); + useGameStore.getState().dispatchRpgAction({ type: "boss-start" }); + expect(useGameStore.getState().selectedMemberId).not.toBe("brann"); + silenceBosses(); + useGameStore.setState((state) => ({ boss: { ...state.boss, hp: 0 } })); + useGameStore.getState().tick(0.1); + + const before = useGameStore.getState(); + expect(before.rpgRun?.phase).toBe("boss-cleared"); + expect(before.castAbility("ability1")).toBe(false); + expect(useGameStore.getState().mana).toBe(before.mana); + }); + + it("unlocks the north portal after a boss and opens exactly one run chest", () => { + finishRpgDrafts(); + useGameStore.getState().dispatchRpgAction({ type: "challenge-start" }); + const objective = useGameStore.getState().rpgRun!.currentChallenge!.objective; + useGameStore.setState((state) => ({ + endlessBossKills: objective.metric === "bossKills" ? objective.target : state.endlessBossKills, + blockbreaker: objective.metric === "bricksBroken" + ? { ...state.blockbreaker, bricksBroken: objective.target } + : state.blockbreaker, + aetherAssault: objective.metric === "kills" + ? { ...state.aetherAssault, kills: objective.target } + : state.aetherAssault, + })); + silenceBosses(); + useGameStore.getState().tick(0.1); + expect(useGameStore.getState().rpgRun?.phase).toBe("boss-briefing"); + + useGameStore.getState().dispatchRpgAction({ type: "boss-start" }); + silenceBosses(); + const companion = useGameStore.getState().party[1]; + const companionInstanceId = companion.runProfile!.instanceId; + useGameStore.setState((state) => ({ + boss: { ...state.boss, hp: 0 }, + party: state.party.map((member) => member.id === companion.id ? { ...member, hp: member.maxHp * 0.25 } : member), + })); + useGameStore.getState().tick(0.1); + expect(useGameStore.getState().rpgRun?.phase).toBe("boss-cleared"); + expect(useGameStore.getState().phase).toBe("combat"); + const runMaxHp = useGameStore.getState().rpgRun!.roster + .find((member) => member.instanceId === companionInstanceId)!.stats.maxHp; + expect(useGameStore.getState().rpgRun!.roster.find((member) => member.instanceId === companionInstanceId)!.hp) + .toBeCloseTo(runMaxHp * 0.25); + + useGameStore.setState((state) => ({ + party: state.party.map((member) => member.id === companion.id ? { ...member, hp: member.maxHp } : member), + })); + + useGameStore.getState().setPlayerPosition([ARENA_CENTER[0], ARENA_CENTER[1] - ARENA_WALL_RADIUS - 0.7]); + const reward = useGameStore.getState().rpgRun; + expect(reward?.phase).toBe("reward"); + expect(reward?.roster.find((member) => member.instanceId === companionInstanceId)?.hp).toBe(runMaxHp); + expect(reward?.pendingReward?.choices).toHaveLength(3); + const chestId = reward?.pendingReward?.id; + useGameStore.getState().setPlayerPosition([ARENA_CENTER[0], ARENA_CENTER[1] - ARENA_WALL_RADIUS - 0.8]); + expect(useGameStore.getState().rpgRun?.pendingReward?.id).toBe(chestId); + }); +}); diff --git a/src/game/runModes.ts b/src/game/runModes.ts new file mode 100644 index 0000000..4c9ad20 --- /dev/null +++ b/src/game/runModes.ts @@ -0,0 +1,13 @@ +import type { GameplayActivity, RunMode } from "./types"; + +/** Competitive runs always use normalized combat stats, regardless of saved gear. */ +export function isPvpRunMode(runMode: RunMode): boolean { + return runMode.endsWith("-pvp"); +} + +export function defaultGameplayActivity(runMode: RunMode): GameplayActivity { + if (runMode === "hockey-healing" || runMode === "hockey-healing-pvp" || runMode === "blockbreaker" || runMode === "aether-assault") { + return runMode; + } + return "boss"; +} diff --git a/src/game/staffCastGlow.test.ts b/src/game/staffCastGlow.test.ts new file mode 100644 index 0000000..167072b --- /dev/null +++ b/src/game/staffCastGlow.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import { + STAFF_CAST_AFTERGLOW_SECONDS, + STAFF_CAST_GLOW_PROFILES, + isHealerPulseKind, + staffCastGlowStrength, +} from "./staffCastGlow"; + +describe("staff cast glow", () => { + it("uses a distinct color for every healer class", () => { + expect(STAFF_CAST_GLOW_PROFILES.priest.color).toBe("#ffd66b"); + expect(STAFF_CAST_GLOW_PROFILES.druid.color).toBe("#72df72"); + expect(STAFF_CAST_GLOW_PROFILES.shaman.color).toBe("#62bdff"); + expect(STAFF_CAST_GLOW_PROFILES.paladin.color).toBe("#f2a65a"); + expect(STAFF_CAST_GLOW_PROFILES.chronomancer.color).toBe("#62d4d9"); + expect(new Set(Object.values(STAFF_CAST_GLOW_PROFILES).map((profile) => profile.color)).size).toBe(5); + }); + + it("accepts every healer cast pulse and rejects encounter pulses", () => { + for (const kind of ["direct-heal", "periodic-heal", "protective", "cleanse", "group-heal", "field"] as const) { + expect(isHealerPulseKind(kind)).toBe(true); + } + expect(isHealerPulseKind("boss")).toBe(false); + expect(isHealerPulseKind("venom")).toBe(false); + }); + + it("ramps during a cast, flashes on completion, then fades out", () => { + const castStart = staffCastGlowStrength({ castingProgress: 0, afterglowAge: null }); + const castEnd = staffCastGlowStrength({ castingProgress: 1, afterglowAge: null }); + const completed = staffCastGlowStrength({ castingProgress: null, afterglowAge: 0 }); + const fading = staffCastGlowStrength({ castingProgress: null, afterglowAge: STAFF_CAST_AFTERGLOW_SECONDS / 2 }); + const finished = staffCastGlowStrength({ castingProgress: null, afterglowAge: STAFF_CAST_AFTERGLOW_SECONDS }); + + expect(castStart).toBeGreaterThan(0); + expect(castEnd).toBeGreaterThan(castStart); + expect(completed).toBe(1); + expect(fading).toBeGreaterThan(0); + expect(fading).toBeLessThan(completed); + expect(finished).toBe(0); + }); + + it("stays dark without an active or completed cast", () => { + expect(staffCastGlowStrength({ castingProgress: null, afterglowAge: null })).toBe(0); + }); +}); diff --git a/src/game/staffCastGlow.ts b/src/game/staffCastGlow.ts new file mode 100644 index 0000000..f92ae6d --- /dev/null +++ b/src/game/staffCastGlow.ts @@ -0,0 +1,45 @@ +import type { HealerClassId, HealerPulseKind, PulseKind } from "./types"; + +export interface StaffCastGlowProfile { + color: string; +} + +export const STAFF_CAST_GLOW_PROFILES: Record = { + priest: { color: "#ffd66b" }, + druid: { color: "#72df72" }, + shaman: { color: "#62bdff" }, + paladin: { color: "#f2a65a" }, + chronomancer: { color: "#62d4d9" }, +}; + +export const STAFF_CAST_AFTERGLOW_SECONDS = 0.85; + +export function isHealerPulseKind(kind: PulseKind): kind is HealerPulseKind { + return kind === "direct-heal" + || kind === "periodic-heal" + || kind === "protective" + || kind === "cleanse" + || kind === "group-heal" + || kind === "field"; +} + +function clamp01(value: number) { + return Math.max(0, Math.min(1, value)); +} + +export function staffCastGlowStrength({ + castingProgress, + afterglowAge, +}: { + castingProgress: number | null; + afterglowAge: number | null; +}) { + const progress = castingProgress === null ? 0 : clamp01(castingProgress); + const smoothProgress = progress * progress * (3 - 2 * progress); + const castingStrength = castingProgress === null ? 0 : 0.42 + smoothProgress * 0.58; + const afterglowProgress = afterglowAge === null + ? 1 + : clamp01(afterglowAge / STAFF_CAST_AFTERGLOW_SECONDS); + const afterglowStrength = afterglowAge === null ? 0 : (1 - afterglowProgress) ** 2; + return Math.max(castingStrength, afterglowStrength); +} diff --git a/src/game/store.test.ts b/src/game/store.test.ts index fc47a48..85be685 100644 --- a/src/game/store.test.ts +++ b/src/game/store.test.ts @@ -3,6 +3,7 @@ import { BULL_CHARGE } from "./bossMechanics"; import { distance, pointToSegmentDistance } from "./geometry"; import { RUN_BUFF_INPUT_LOCK_MS, barrierProtects, useGameStore } from "./store"; import { createClassInventory, HEALER_CLASSES } from "./healers"; +import { healingEffect } from "./healerEffects"; import { dropVenomPool, VENOM_PURGE } from "./bosses/mechanicPool"; import { ARENA_CENTER, isInsideArena } from "./arena"; import { BOSS_DEFINITIONS } from "./bossCatalog"; @@ -37,7 +38,7 @@ describe("Disc Priest combat simulation", () => { useGameStore.setState({ party }); useGameStore.getState().selectMember("nia"); - expect(useGameStore.getState().castAbility("mend")).toBe(true); + expect(useGameStore.getState().castAbility("ability1")).toBe(true); useGameStore.getState().tick(0.4); expect(useGameStore.getState().party.find((member) => member.id === "nia")?.hp).toBe(40); useGameStore.getState().tick(0.11); @@ -45,28 +46,28 @@ describe("Disc Priest combat simulation", () => { const state = useGameStore.getState(); expect(state.party.find((member) => member.id === "nia")?.hp).toBe(78); expect(state.activeCast).toBeNull(); - expect(state.cooldowns.mend).toBe(0); - expect(state.castAbility("mend")).toBe(true); + expect(state.cooldowns.ability1).toBe(0); + expect(state.castAbility("ability1")).toBe(true); }); it("uses the rebalanced Priest mana costs", () => { expect(Object.fromEntries(Object.entries(HEALER_CLASSES.priest.abilities).map(([id, ability]) => [id, ability.mana]))).toEqual({ - mend: 5, - renew: 7, - shield: 8, - purify: 5, - radiance: 12, - barrier: 10, + ability1: 5, + ability2: 7, + ability3: 8, + ability4: 5, + ability5: 12, + ability6: 10, }); }); it("uses a three-second Purify cooldown for every healer class", () => { - expect(HEALER_CLASSES.priest.abilities.purify.cooldown).toBe(3); - expect(HEALER_CLASSES.druid.abilities.purify.cooldown).toBe(3); - expect(HEALER_CLASSES.shaman.abilities.purify.cooldown).toBe(3); + expect(HEALER_CLASSES.priest.abilities.ability4.cooldown).toBe(3); + expect(HEALER_CLASSES.druid.abilities.ability4.cooldown).toBe(3); + expect(HEALER_CLASSES.shaman.abilities.ability4.cooldown).toBe(3); }); - it("configures placeholder healer kits and class-owned inventory", () => { + it("configures distinct healer kits and class-owned inventory", () => { const inventory = createClassInventory("druid"); useGameStore.getState().configureHealer("druid", "Aelia", inventory); @@ -75,8 +76,8 @@ describe("Disc Priest combat simulation", () => { expect(state.party[0].className).toBe("Restoration Druid"); expect(state.party[0].name).toBe("Aelia"); expect(state.inventory).toEqual(inventory); - expect(HEALER_CLASSES.druid.abilities.mend.name).toBe("Healing Touch"); - expect(HEALER_CLASSES.shaman.abilities.radiance.name).toBe("Chain Heal"); + expect(HEALER_CLASSES.druid.abilities.ability1.name).toBe("Regrowth"); + expect(HEALER_CLASSES.shaman.abilities.ability5.name).toBe("Chain Heal"); }); it("ticks Renew once per second for eight seconds", () => { @@ -84,37 +85,37 @@ describe("Disc Priest combat simulation", () => { member.id === "brann" ? { ...member, hp: 70 } : member, ); useGameStore.setState({ party }); - useGameStore.getState().castAbility("renew"); + useGameStore.getState().castAbility("ability2"); for (let index = 0; index < 8; index += 1) useGameStore.getState().tick(1); const brann = useGameStore.getState().party.find((member) => member.id === "brann")!; - expect(brann.renewExpiresAt).toBe(0); + expect(healingEffect(brann, "renew")).toBeUndefined(); expect(brann.hp).toBeGreaterThan(70); }); it("gives Renew no individual cooldown", () => { - expect(useGameStore.getState().castAbility("renew")).toBe(true); - expect(useGameStore.getState().cooldowns.renew).toBe(0); + expect(useGameStore.getState().castAbility("ability2")).toBe(true); + expect(useGameStore.getState().cooldowns.ability2).toBe(0); useGameStore.getState().tick(0.5); - expect(useGameStore.getState().castAbility("renew")).toBe(true); - expect(useGameStore.getState().cooldowns.renew).toBe(0); + expect(useGameStore.getState().castAbility("ability2")).toBe(true); + expect(useGameStore.getState().cooldowns.ability2).toBe(0); }); it("blocks all abilities during the shared 0.5 second global cooldown", () => { - expect(useGameStore.getState().castAbility("renew")).toBe(true); - expect(useGameStore.getState().castAbility("shield")).toBe(false); + expect(useGameStore.getState().castAbility("ability2")).toBe(true); + expect(useGameStore.getState().castAbility("ability3")).toBe(false); useGameStore.getState().tick(0.49); - expect(useGameStore.getState().castAbility("shield")).toBe(false); + expect(useGameStore.getState().castAbility("ability3")).toBe(false); useGameStore.getState().tick(0.02); - expect(useGameStore.getState().castAbility("shield")).toBe(true); + expect(useGameStore.getState().castAbility("ability3")).toBe(true); }); it("uses blue absorption before health", () => { - useGameStore.getState().castAbility("shield"); + useGameStore.getState().castAbility("ability3"); useGameStore.getState().tick(2); const brann = useGameStore.getState().party.find((member) => member.id === "brann")!; @@ -131,7 +132,7 @@ describe("Disc Priest combat simulation", () => { expect(branded.debuffs).toHaveLength(1); useGameStore.getState().selectMember(branded.id); - expect(useGameStore.getState().castAbility("purify")).toBe(true); + expect(useGameStore.getState().castAbility("ability4")).toBe(true); expect(useGameStore.getState().party.find((member) => member.id === branded.id)?.debuffs).toHaveLength(0); }); @@ -139,7 +140,7 @@ describe("Disc Priest combat simulation", () => { useGameStore.setState({ party: useGameStore.getState().party.map((member) => ({ ...member, hp: member.hp - 30 })), }); - useGameStore.getState().castAbility("radiance"); + useGameStore.getState().castAbility("ability5"); for (const member of useGameStore.getState().party) { expect(member.hp).toBe(member.maxHp - 8); @@ -151,17 +152,17 @@ describe("Disc Priest combat simulation", () => { useGameStore.setState((state) => ({ boss: { ...state.boss }, })); - expect(useGameStore.getState().castAbility("barrier")).toBe(true); + expect(useGameStore.getState().castAbility("ability6")).toBe(true); useGameStore.getState().tick(2); const state = useGameStore.getState(); expect(state.party.find((member) => member.id === "brann")?.hp).toBeCloseTo(139.5, 4); expect(barrierProtects(state.partyPositions.brann, state.barrier, state.time)).toBe(true); - expect(state.cooldowns.barrier).toBe(60); + expect(state.cooldowns.ability6).toBe(60); }); it("expires Barrier after eight seconds", () => { - useGameStore.getState().castAbility("barrier"); + useGameStore.getState().castAbility("ability6"); const barrier = useGameStore.getState().barrier; expect(barrierProtects(barrier.center, barrier, 7.99)).toBe(true); expect(barrierProtects(barrier.center, barrier, 8)).toBe(false); @@ -190,7 +191,7 @@ describe("Disc Priest combat simulation", () => { expect(paused.time).toBe(before.time); expect(paused.party).toEqual(before.party); expect(paused.boss.hp).toBe(before.boss.hp); - expect(paused.castAbility("renew")).toBe(false); + expect(paused.castAbility("ability2")).toBe(false); }); it("cancels an active cast and blocks new abilities when the healer falls", () => { @@ -200,7 +201,7 @@ describe("Disc Priest combat simulation", () => { party: state.party.map((member) => member.id === "nia" ? { ...member, hp: 40 } : member), })); useGameStore.getState().selectMember("nia"); - expect(useGameStore.getState().castAbility("mend")).toBe(true); + expect(useGameStore.getState().castAbility("ability1")).toBe(true); useGameStore.setState((state) => ({ party: state.party.map((member) => member.id === "aelia" ? { ...member, hp: 0 } : member), })); @@ -210,7 +211,7 @@ describe("Disc Priest combat simulation", () => { expect(state.phase).toBe("combat"); expect(state.activeCast).toBeNull(); expect(state.party.find((member) => member.id === "nia")?.hp).toBe(40); - expect(state.castAbility("renew")).toBe(false); + expect(state.castAbility("ability2")).toBe(false); }); it("does not publish unchanged player positions while idle", () => { @@ -423,7 +424,7 @@ describe("Broodfang encounter", () => { expect(poisoned).toBeDefined(); useGameStore.getState().selectMember(poisoned.id); - expect(useGameStore.getState().castAbility("purify")).toBe(true); + expect(useGameStore.getState().castAbility("ability4")).toBe(true); const state = useGameStore.getState(); expect(state.party.find((member) => member.id === poisoned.id)?.debuffs).toEqual([]); expect(state.bossMotion.hazards).toHaveLength(1); @@ -522,7 +523,7 @@ describe("PVE dual-boss encounter", () => { const poisoned = useGameStore.getState().party.find((member) => member.debuffs.some((debuff) => debuff.name === "Widow Venom"))!; useGameStore.getState().selectMember(poisoned.id); - expect(useGameStore.getState().castAbility("purify")).toBe(true); + expect(useGameStore.getState().castAbility("ability4")).toBe(true); expect(useGameStore.getState().bossMotion.hazards.filter((hazard) => hazard.kind === "venom_pool")).toHaveLength(1); expect(useGameStore.getState().additionalBosses[0].motion.hazards.filter((hazard) => hazard.kind === "venom_pool")).toHaveLength(0); }); @@ -539,7 +540,7 @@ describe("Roguelike ability buffs", () => { })); useGameStore.getState().selectMember("brann"); - expect(useGameStore.getState().castAbility("mend")).toBe(true); + expect(useGameStore.getState().castAbility("ability1")).toBe(true); expect(useGameStore.getState().mana).toBe(97); expect(useGameStore.getState().activeCast?.completesAt).toBeCloseTo(0.5 * 0.75 ** 3); useGameStore.getState().tick(0.22); @@ -560,10 +561,10 @@ describe("Roguelike ability buffs", () => { })), })); useGameStore.getState().selectMember("brann"); - expect(useGameStore.getState().castAbility("renew")).toBe(true); + expect(useGameStore.getState().castAbility("ability2")).toBe(true); let party = useGameStore.getState().party; - expect(party.filter((member) => member.renewExpiresAt === 12).map((member) => member.id)).toEqual(["brann", "nia", "orin"]); + expect(party.filter((member) => healingEffect(member, "renew")?.expiresAt === 12).map((member) => member.id)).toEqual(["brann", "nia", "orin"]); useGameStore.getState().tick(1.01); party = useGameStore.getState().party; expect(party.find((member) => member.id === "brann")?.hp).toBeCloseTo(59.8); @@ -580,7 +581,7 @@ describe("Roguelike ability buffs", () => { })), })); useGameStore.getState().selectMember("brann"); - expect(useGameStore.getState().castAbility("shield")).toBe(true); + expect(useGameStore.getState().castAbility("ability3")).toBe(true); const party = useGameStore.getState().party; expect(party.find((member) => member.id === "brann")?.absorb).toBe(54); @@ -600,13 +601,13 @@ describe("Roguelike ability buffs", () => { : member), })); useGameStore.getState().selectMember("brann"); - expect(useGameStore.getState().castAbility("purify")).toBe(true); + expect(useGameStore.getState().castAbility("ability4")).toBe(true); const party = useGameStore.getState().party; for (const id of ["brann", "nia"] as const) { const member = party.find((candidate) => candidate.id === id)!; expect(member.debuffs).toEqual([]); - expect(member.renewExpiresAt).toBe(8); + expect(healingEffect(member, "renew")?.expiresAt).toBe(8); expect(member.absorb).toBe(18); } expect(party.find((member) => member.id === "orin")?.debuffs).toHaveLength(1); @@ -615,11 +616,11 @@ describe("Roguelike ability buffs", () => { it("applies Radiance cooldown, party Renew, and party absorption", () => { startBuffedEncounter({ "radiance-cooldown": 3, "radiance-renew": 1, "radiance-shield": 2 }); useGameStore.setState((state) => ({ party: state.party.map((member) => ({ ...member, hp: Math.max(1, member.hp - 30) })) })); - expect(useGameStore.getState().castAbility("radiance")).toBe(true); + expect(useGameStore.getState().castAbility("ability5")).toBe(true); const state = useGameStore.getState(); - expect(state.cooldowns.radiance).toBeCloseTo(14 * 0.8 ** 3); - expect(state.party.every((member) => member.renewExpiresAt === 8)).toBe(true); + expect(state.cooldowns.ability5).toBeCloseTo(14 * 0.8 ** 3); + expect(state.party.every((member) => healingEffect(member, "renew")?.expiresAt === 8)).toBe(true); expect(state.party.every((member) => member.absorb === 18)).toBe(true); }); @@ -628,8 +629,8 @@ describe("Roguelike ability buffs", () => { useGameStore.setState((state) => ({ party: state.party.map((member) => member.id === "aelia" ? { ...member, hp: 50 } : member), })); - expect(useGameStore.getState().castAbility("barrier")).toBe(true); - expect(useGameStore.getState().cooldowns.barrier).toBeCloseTo(60 * 0.8 ** 3); + expect(useGameStore.getState().castAbility("ability6")).toBe(true); + expect(useGameStore.getState().cooldowns.ability6).toBeCloseTo(60 * 0.8 ** 3); expect(useGameStore.getState().barrier.expiresAt).toBe(14); useGameStore.getState().tick(1.01); expect(useGameStore.getState().party.find((member) => member.id === "aelia")?.hp).toBe(59); diff --git a/src/game/store.ts b/src/game/store.ts index 5e2097d..65ab002 100644 --- a/src/game/store.ts +++ b/src/game/store.ts @@ -7,16 +7,42 @@ import { upcomingMechanic, } from "./bossMechanics"; import { BOSS_DEFINITIONS } from "./bossCatalog"; -import { BOSS_DEATH_DESPAWN_SECONDS } from "./bossDeath"; +import { bossDeathDespawnSeconds } from "./bossDeath"; import { normalizeEncounterBossIds } from "./bossSelection"; -import { clampToArena, constrainBossMotion } from "./arena"; +import { clampToArena, clampToHockeyArena, clampToHockeyEnemyHalf, clampToHockeyHealerHalf, constrainBossMotion } from "./arena"; import { cloneMotion } from "./bosses/shared"; import { freshParty } from "./data"; import { distance } from "./geometry"; -import { createClassInventory, HEALER_CLASSES } from "./healers"; +import { + createClassAbilityLoadout, + createClassInventory, + HEALER_ABILITIES, + HEALER_CLASSES, + resolveSlottedAbility, +} from "./healers"; +import { + advanceHealingEffects, + chainHealIndexes, + equalizeHealthPercentages, + flourishParty, + healingEffect, + removeHealingEffect, + resolveReactiveHealAfterDamage, + setHealingEffect, + triggerHealingEffects, +} from "./healerEffects"; +import { + createHealerMechanicState, + expireHealerMechanics, + healMostInjured, + healTargetAndBeacon, + placeOrRewindTimeAnchor, + resolveTimeLoop, + startTimeLoop, +} from "./healerMechanics"; import { combatFormation, updatePartyPositions } from "./partyBehaviors"; import { advancePartyCombat, createPartyCombatState, tankAuraProtects, type PartyCombatState, type PartyDamageEvent } from "./partyCombat"; -import { isPartyWiped } from "./partyState"; +import { areAllNonHealerAlliesDefeated, isPartyWiped } from "./partyState"; import { RUN_BUFFS, bossHealthMultiplier, @@ -33,19 +59,41 @@ import { type CompiledRunModifiers, } from "./roguelike"; import { createDefaultGearProgress, type GearProgress } from "./progression/gear"; -import { aiCombatModifiers, applyGearHealth, createEncounterGearModifiers, type EncounterGearModifiers } from "./progression/gearEffects"; +import { aiCombatModifiers, applyGearHealth, createBaseEncounterGearModifiers, createEncounterGearModifiers, type EncounterGearModifiers } from "./progression/gearEffects"; import { passiveInfusionUnlocked } from "./progression/infusions"; import { DIFFICULTY_BY_SLUG, normalizeDifficultySlug, type DifficultySlug } from "./progression/loot"; +import { + HOCKEY_DEFAULT_PLAYER_POSITION, + advanceHockeyHealing, + createHockeyHealingState, + setHockeyAim, + type HockeyHealingState, +} from "./hockeyHealing"; +import { + HOCKEY_PVP_GOAL_DAMAGE, + advanceHockeyPvpCpuGoalie, + advanceHockeyPvpPuck, + createHockeyPvpState, + hockeyPvpHealingEffectiveness, + hockeyPvpBossAt, + mirrorHockeyPvpPuck, + type HockeyPvpMatchConfig, + type HockeyPvpRemoteSnapshot, + type HockeyPvpState, +} from "./hockeyHealingPvp"; import type { ActiveCast, - AbilityId, + AbilityLoadout, + AbilitySlotId, BarrierState, BossMotionState, BossState, BossId, BottomTab, GamePhase, + GameplayActivity, HealerClassId, + HealerMechanicState, InventoryItem, MemberId, PartyMember, @@ -55,6 +103,41 @@ import type { ScenePulse, WorldPosition, } from "./types"; +import { defaultGameplayActivity, isPvpRunMode } from "./runModes"; +import { + BLOCKBREAKER_BREACH_DAMAGE, + advanceBlockbreaker, + createBlockbreakerSeed, + createBlockbreakerState, + setBlockbreakerAim, + type BlockbreakerState, +} from "./blockbreaker"; +import { + advanceAetherAssault, + createAetherAssaultSeed, + createAetherAssaultState, + type AetherAssaultState, +} from "./aetherAssault"; +import { + assignRosterToCombatSlots, + BOSSES_PER_ACT, + createRpgRoguelikeRun, + reduceRpgRoguelikeRun, + selectCurrentBossId, + type ChallengeMetrics, + type RpgRoguelikeAction, + type RpgRoguelikeRunState, +} from "./rpgRoguelike"; +import { + extractRpgPlayerVital, + extractRpgPartyVitals, + projectRpgCombat, + spellRankCooldownMultiplier, + spellRankPowerMultiplier, + type RpgPartyDamageProfiles, +} from "./rpgRoguelike/combatAdapter"; +import { CLOSED_BOSS_ARENA_PORTALS, NORTH_OPEN_BOSS_ARENA_PORTALS, clampToBossArenaWithPortals, detectBossArenaExit } from "./rpgRoguelike/playSpace"; +import { moveRpgFocus, normalizeRpgFocusId, rpgFocusItems, type RpgFocusDirection } from "./rpgRoguelike/uiModel"; export interface CombatLogEntry { id: number; @@ -69,21 +152,49 @@ export interface AdditionalBossState { motion: BossMotionState; } +export interface HockeyPvpOpponentState { + party: PartyMember[]; + partyPositions: Record; + boss: BossState; + bossMotion: BossMotionState; + partyCombat: PartyCombatState; +} + +export interface RpgSpellResources { + verdancy: number; + tidalSurge: number; + conviction: number; + chronoshards: number; +} + export interface GameState { bossId: BossId; bossInstanceId: string; paused: boolean; pauseSelection: "resume" | "exit"; healerClassId: HealerClassId; + abilityLoadout: AbilityLoadout; playerName: string; phase: GamePhase; runMode: RunMode; + activityMode: GameplayActivity; + /** Authoritative, serializable state for the modular RPG Roguelike mode. */ + rpgRun: RpgRoguelikeRunState | null; + /** Semantic focus target shared by both displays; never depends on DOM focus. */ + rpgFocusId: string | null; + rpgDamageProfiles: RpgPartyDamageProfiles | null; + rpgSpellResources: RpgSpellResources; round: number; seenBossIds: BossId[]; endlessMode: boolean; endlessBossKills: number; endlessSpawnSequence: number; endlessChoiceSelection: "continue" | "quit"; + hockey: HockeyHealingState; + blockbreaker: BlockbreakerState; + aetherAssault: AetherAssaultState; + hockeyPvp: HockeyPvpState; + hockeyPvpOpponent: HockeyPvpOpponentState; runBuffRanks: RunBuffRanks; draftBuffIds: RunBuffId[]; selectedRunBuffId: RunBuffId | null; @@ -106,7 +217,7 @@ export interface GameState { mana: number; maxMana: number; selectedMemberId: MemberId; - cooldowns: Record; + cooldowns: Record; globalCooldownUntil: number; activeTab: BottomTab; selectedItemId: string; @@ -116,16 +227,19 @@ export interface GameState { playerPosition: [number, number]; activeCast: ActiveCast | null; barrier: BarrierState; - configureHealer: (classId: HealerClassId, playerName: string, inventory: InventoryItem[], bossIds?: BossId | readonly BossId[], runMode?: RunMode, gearProgress?: GearProgress, difficultySlug?: DifficultySlug) => void; + healerMechanic: HealerMechanicState; + configureHealer: (classId: HealerClassId, playerName: string, inventory: InventoryItem[], bossIds?: BossId | readonly BossId[], runMode?: RunMode, gearProgress?: GearProgress, difficultySlug?: DifficultySlug, hockeyPvpMatch?: HockeyPvpMatchConfig) => void; startEncounter: () => void; restart: () => void; tick: (delta: number) => void; - castAbility: (abilityId: AbilityId) => boolean; + castAbility: (abilitySlotId: AbilitySlotId) => boolean; selectMember: (memberId: MemberId) => void; cycleMember: (direction: 1 | -1) => void; setActiveTab: (tab: BottomTab) => void; selectItem: (itemId: string) => void; setPlayerPosition: (position: [number, number]) => void; + setHockeyAimDirection: (direction: [number, number]) => void; + applyHockeyPvpRemoteSnapshot: (snapshot: HockeyPvpRemoteSnapshot, hostPuck?: HockeyPvpRemoteSnapshot["puck"]) => void; setPaused: (paused: boolean) => void; togglePause: () => void; setPauseSelection: (selection: "resume" | "exit") => void; @@ -134,15 +248,20 @@ export interface GameState { continueRoguelikeRound: () => boolean; startRogueTrialsEndless: () => boolean; setEndlessChoiceSelection: (selection: "continue" | "quit") => void; + setAbilityLoadout: (loadout: AbilityLoadout) => void; + dispatchRpgAction: (action: RpgRoguelikeAction) => boolean; + setRpgFocusId: (focusId: string) => void; + cycleRpgFocus: (direction: 1 | -1) => void; + moveRpgFocus: (direction: RpgFocusDirection) => void; } -const emptyCooldowns = (): Record => ({ - mend: 0, - renew: 0, - shield: 0, - purify: 0, - radiance: 0, - barrier: 0, +const emptyCooldowns = (): Record => ({ + ability1: 0, + ability2: 0, + ability3: 0, + ability4: 0, + ability5: 0, + ability6: 0, }); export const GLOBAL_COOLDOWN_SECONDS = 0.5; @@ -156,7 +275,15 @@ const normalizeBossIds = (bossIds: BossId | readonly BossId[] = "bulldrome"): Bo return normalizeEncounterBossIds(requested); }; -function createEncounterMotion(bossId: BossId, index: number, count: number, startsAt = 0): BossMotionState { +type EncounterLayout = "standard" | "hockey"; + +function createEncounterMotion( + bossId: BossId, + index: number, + count: number, + startsAt = 0, + layout: EncounterLayout = "standard", +): BossMotionState { const motion = cloneMotion(createBossMotionState(bossId)); const offset = count === 3 ? [-3.7, 0, 3.7][index] : count === 2 ? [-2.65, 2.65][index] : 0; motion.formationOffsetX = offset; @@ -164,21 +291,46 @@ function createEncounterMotion(bossId: BossId, index: number, count: number, sta motion.chargeStart[0] += offset; motion.chargeEnd[0] += offset; motion.pounceCenter[0] += offset; + if (layout === "hockey") { + const anchor: WorldPosition = index === 0 ? [-2.8, -10] : index === 1 ? [2.8, -6.5] : [0, -8]; + const dx = anchor[0] - motion.position[0]; + const dz = anchor[1] - motion.position[1]; + motion.position = [...anchor]; + motion.chargeStart = [motion.chargeStart[0] + dx, motion.chargeStart[1] + dz]; + motion.chargeEnd = [motion.chargeEnd[0] + dx, motion.chargeEnd[1] + dz]; + motion.pounceCenter = [motion.pounceCenter[0] + dx, motion.pounceCenter[1] + dz]; + } const stagger = index * 2.4; if (Number.isFinite(motion.nextMechanicAt)) motion.nextMechanicAt += startsAt + stagger; return constrainBossMotion(motion); } -function createEncounterBoss(bossId: BossId, index: number, count: number, healthMultiplier: number, startsAt = 0): AdditionalBossState { +function createEncounterBoss( + bossId: BossId, + index: number, + count: number, + healthMultiplier: number, + startsAt = 0, + layout: EncounterLayout = "standard", +): AdditionalBossState { const boss = createBossState(bossId); boss.maxHp = Math.round(boss.maxHp * healthMultiplier); boss.hp = boss.maxHp; const stagger = index * 0.8; if (Number.isFinite(boss.nextMeleeAt)) boss.nextMeleeAt += startsAt + stagger; - return { instanceId: `boss-${index}-${bossId}`, boss, motion: createEncounterMotion(bossId, index, count, startsAt) }; + return { instanceId: `boss-${index}-${bossId}`, boss, motion: createEncounterMotion(bossId, index, count, startsAt, layout) }; } -const freshPartyPositions = (bossIds: readonly BossId[]): Record => { +const freshPartyPositions = (bossIds: readonly BossId[], layout: EncounterLayout = "standard"): Record => { + if (layout === "hockey") { + return { + aelia: [...HOCKEY_DEFAULT_PLAYER_POSITION], + brann: [-1.2, -2.8], + nia: [-4.2, -1.6], + orin: [4.2, -1.6], + vale: [1.1, -8.4], + }; + } const bossPosition = createBossMotionState(bossIds[0]).position; if (bossIds.length > 1) bossPosition[0] = 0; const formation = combatFormation(bossPosition); @@ -205,8 +357,20 @@ export function healMember(member: PartyMember, amount: number): PartyMember { return { ...member, hp: Math.min(member.maxHp, member.hp + amount) }; } +function effectiveHealingMultiplier(state: Pick): number { + if (state.runMode !== "hockey-healing-pvp") return state.healingMultiplier; + return state.healingMultiplier * hockeyPvpHealingEffectiveness( + state.endlessBossKills, + state.hockeyPvp.opponentBossKills, + ); +} + export function barrierProtects(position: WorldPosition, barrier: BarrierState, time: number) { - return barrier.expiresAt > time && distance(position, barrier.center) <= BARRIER_RADIUS; + return barrier.kind === "barrier" && healerFieldContains(position, barrier, time); +} + +export function healerFieldContains(position: WorldPosition, barrier: BarrierState, time: number) { + return barrier.kind !== null && barrier.expiresAt > time && distance(position, barrier.center) <= BARRIER_RADIUS; } function lowestHealthIndexes( @@ -223,12 +387,83 @@ function lowestHealthIndexes( .map(({ index }) => index); } -function applyRenewAt(party: PartyMember[], index: number, time: number, modifiers: CompiledRunModifiers) { +function applyRenewAt(party: PartyMember[], index: number, time: number, modifiers: CompiledRunModifiers, power = 1) { + if (party[index].hp <= 0) return; + party[index] = setHealingEffect(party[index], { + id: "renew", + expiresAt: time + 8 + modifiers.renewDurationBonus, + nextTickAt: time + 1, + tickInterval: 1, + healingPerTick: 7 * modifiers.renewHealingMultiplier * power, + stacks: 1, + }); +} + +function applyRejuvenationAt(party: PartyMember[], index: number, time: number, modifiers: CompiledRunModifiers, power = 1) { + if (party[index].hp <= 0) return; + party[index] = setHealingEffect(party[index], { + id: "rejuvenation", + expiresAt: time + 8 + modifiers.renewDurationBonus, + nextTickAt: time + 1, + tickInterval: 1, + healingPerTick: 6 * modifiers.renewHealingMultiplier * power, + stacks: 1, + }); +} + +function applyRiptideAt(party: PartyMember[], index: number, time: number, modifiers: CompiledRunModifiers, power = 1) { + if (party[index].hp <= 0) return; + party[index] = setHealingEffect(party[index], { + id: "riptide", + expiresAt: time + 6 + modifiers.renewDurationBonus, + nextTickAt: time + 1, + tickInterval: 1, + healingPerTick: 5 * modifiers.renewHealingMultiplier * power, + stacks: 1, + }); +} + +function applyLifebloomAt( + party: PartyMember[], + index: number, + time: number, + modifiers: CompiledRunModifiers, + stacksToAdd = 1, + power = 1, +) { + if (party[index].hp <= 0) return 0; + const current = healingEffect(party[index], "lifebloom"); + const stacks = Math.min(3, (current?.stacks ?? 0) + stacksToAdd); + party[index] = setHealingEffect(party[index], { + id: "lifebloom", + expiresAt: time + 7, + nextTickAt: current?.nextTickAt && current.expiresAt > time ? current.nextTickAt : time + 1, + tickInterval: 1, + healingPerTick: 2 * modifiers.shieldAbsorbMultiplier * power, + stacks, + expirationHealingPerStack: 10 * modifiers.shieldAbsorbMultiplier * power, + }); + return stacks; +} + +function applyEarthShieldAt( + party: PartyMember[], + index: number, + time: number, + modifiers: CompiledRunModifiers, + charges = 6, + power = 1, +) { if (party[index].hp <= 0) return; party[index] = { ...party[index], - renewExpiresAt: time + 8 + modifiers.renewDurationBonus, - renewNextTickAt: time + 1, + reactiveHeal: { + id: "earth-shield", + charges, + expiresAt: time + 30, + nextTriggerAt: time, + healingPerTrigger: 9 * modifiers.shieldAbsorbMultiplier * power, + }, }; } @@ -260,6 +495,7 @@ function damageMemberAt( gearModifiers?: EncounterGearModifiers, kind: "direct" | "hazard" = "direct", shieldDamageTakenMultiplier = 1, + onReactiveHeal?: () => void, ) { amount *= incomingDamageMultiplier; if (kind === "hazard") amount *= gearModifiers?.[member.id].hazardDamageTaken ?? 1; @@ -270,8 +506,15 @@ function damageMemberAt( barrierProtects(position, barrier, time) ? BARRIER_DAMAGE_REDUCTION : 0, protectedByTank ? partyCombat?.tankAura.damageReduction ?? 0 : 0, ); - const shieldMultiplier = member.absorb > 0 ? shieldDamageTakenMultiplier : 1; - return damageMember(member, amount * (1 - reduction) * shieldMultiplier); + const lifebloom = healingEffect(member, "lifebloom"); + const warded = member.absorb > 0 + || Boolean(member.reactiveHeal && member.reactiveHeal.expiresAt > time) + || Boolean(lifebloom && lifebloom.expiresAt > time); + const shieldMultiplier = warded ? shieldDamageTakenMultiplier : 1; + const damaged = damageMember(member, amount * (1 - reduction) * shieldMultiplier); + const reactive = resolveReactiveHealAfterDamage(member, damaged, time, gearModifiers?.aelia.healingPower ?? 1); + if (reactive.triggered) onReactiveHeal?.(); + return reactive.member; } function addLog( @@ -296,22 +539,48 @@ function initialState( gearProgress: GearProgress = createDefaultGearProgress(), requestedDifficultySlug: DifficultySlug = "initiate", seenBossIds: readonly BossId[] = [], + hockeyPvpMatch?: HockeyPvpMatchConfig, + requestedAbilityLoadout?: AbilityLoadout, ) { const difficultySlug = normalizeDifficultySlug(requestedDifficultySlug); const difficulty = DIFFICULTY_BY_SLUG[difficultySlug]; const bossIds = normalizeBossIds(requestedBossIds); + const activityMode = defaultGameplayActivity(runMode); + const hockeyLayout = activityMode !== "boss"; + const layout: EncounterLayout = hockeyLayout ? "hockey" : "standard"; const encounterBosses = bossIds.map((bossId, index) => createEncounterBoss( bossId, index, bossIds.length, bossHealthMultiplier(round) * difficulty.healthMultiplier, + 0, + layout, )); const primary = encounterBosses[0]; - const gearModifiers = createEncounterGearModifiers(gearProgress, healerClassId); - const passiveInfusionId = passiveInfusionUnlocked(gearProgress) ? gearProgress[healerClassId].passiveInfusionId : null; + const normalizedExternalGear = isPvpRunMode(runMode) || runMode === "rpg-roguelike"; + const gearModifiers = normalizedExternalGear + ? createBaseEncounterGearModifiers() + : createEncounterGearModifiers(gearProgress, healerClassId); + const passiveInfusionId = !normalizedExternalGear && passiveInfusionUnlocked(gearProgress) + ? gearProgress[healerClassId].passiveInfusionId + : null; const runModifiers = compileRunModifiers(runBuffRanks, passiveInfusionId); - const draftBuffIds = runMode !== "encounter" ? selectRunBuffDraft(runBuffRanks, passiveInfusionId) : []; + const draftBuffIds = runMode === "roguelike" || runMode === "rogue-trials" + ? selectRunBuffDraft(runBuffRanks, passiveInfusionId) + : []; const party = applyGearHealth(freshParty(healerClassId, playerName), gearModifiers); + const opponentParty = applyGearHealth( + freshParty(healerClassId, hockeyPvpMatch?.opponentName ?? "CPU Willow"), + gearModifiers, + ); + const opponentBoss = createEncounterBoss( + primary.boss.id, + 0, + 1, + difficulty.healthMultiplier, + 0, + "hockey", + ); const maxMana = 100; return { bossId: primary.boss.id, @@ -319,15 +588,38 @@ function initialState( paused: false, pauseSelection: "resume" as const, healerClassId, + abilityLoadout: { ...(requestedAbilityLoadout ?? createClassAbilityLoadout(healerClassId)) }, playerName, phase: "briefing" as GamePhase, runMode, + activityMode, + rpgRun: null as RpgRoguelikeRunState | null, + rpgFocusId: null as string | null, + rpgDamageProfiles: null as RpgPartyDamageProfiles | null, + rpgSpellResources: { verdancy: 0, tidalSurge: 0, conviction: 0, chronoshards: 0 } as RpgSpellResources, round, seenBossIds: [...new Set([...seenBossIds, ...bossIds])], - endlessMode: false, + endlessMode: hockeyLayout, endlessBossKills: 0, - endlessSpawnSequence: 0, + endlessSpawnSequence: hockeyLayout ? encounterBosses.length : 0, endlessChoiceSelection: "continue" as const, + hockey: createHockeyHealingState(activityMode === "hockey-healing"), + blockbreaker: createBlockbreakerState( + activityMode === "blockbreaker", + activityMode === "blockbreaker" ? createBlockbreakerSeed() : 1, + ), + aetherAssault: createAetherAssaultState( + activityMode === "aether-assault", + activityMode === "aether-assault" ? createAetherAssaultSeed() : 1, + ), + hockeyPvp: createHockeyPvpState(activityMode === "hockey-healing-pvp" ? hockeyPvpMatch : undefined), + hockeyPvpOpponent: { + party: opponentParty, + partyPositions: freshPartyPositions([primary.boss.id], "hockey"), + boss: opponentBoss.boss, + bossMotion: opponentBoss.motion, + partyCombat: createPartyCombatState(opponentParty), + } as HockeyPvpOpponentState, runBuffRanks: { ...runBuffRanks }, draftBuffIds, selectedRunBuffId: draftBuffIds[0] ?? null, @@ -343,7 +635,7 @@ function initialState( party, boss: primary.boss, additionalBosses: encounterBosses.slice(1), - partyPositions: freshPartyPositions(bossIds), + partyPositions: freshPartyPositions(bossIds, layout), bossMotion: primary.motion, partyCombat: createPartyCombatState(party), partyDamageEvents: [] as PartyDamageEvent[], @@ -356,25 +648,190 @@ function initialState( selectedItemId: inventory[0]?.id ?? "", inventory: structuredClone(inventory), combatLog: [] as CombatLogEntry[], - scenePulse: { id: 0, kind: "mend" as const }, - playerPosition: [0, 4.5] as [number, number], + scenePulse: { id: 0, kind: "direct-heal" as const }, + playerPosition: (hockeyLayout ? [...HOCKEY_DEFAULT_PLAYER_POSITION] : [0, 4.5]) as [number, number], activeCast: null as ActiveCast | null, - barrier: { center: [0, 4.5], expiresAt: 0, nextHealAt: 0 } as BarrierState, + barrier: { kind: null, center: hockeyLayout ? [...HOCKEY_DEFAULT_PLAYER_POSITION] : [0, 4.5], expiresAt: 0, nextHealAt: 0 } as BarrierState, + healerMechanic: createHealerMechanicState(healerClassId), + }; +} + +function rpgActivityForRun(run: RpgRoguelikeRunState): GameplayActivity { + if (run.phase !== "challenge-active" && run.phase !== "challenge-briefing") return "boss"; + const challengeId = run.currentChallenge?.objective.challengeId; + if (challengeId === "blockbreaker") return "blockbreaker"; + if (challengeId === "hockey") return "hockey-healing"; + return "aether-assault"; +} + +function gamePhaseForRpgRun(run: RpgRoguelikeRunState): GamePhase { + if (run.phase === "challenge-active" || run.phase === "boss-combat" || run.phase === "boss-cleared") return "combat"; + if (run.phase === "reward" || run.phase === "shop") return "intermission"; + if (run.phase === "victory") return "victory"; + if (run.phase === "defeat") return "defeat"; + return "briefing"; +} + +function rpgRunSeed(): number { + // One non-deterministic seed is captured at run creation. Every subsequent + // offer, route, reward, and shop draw advances the serialized RNG state. + return Date.now() ^ Math.floor(Math.random() * 0x7fffffff); +} + +function rpgChallengeMetrics( + activityMode: GameplayActivity, + blockbreaker: BlockbreakerState, + endlessBossKills: number, + aetherAssault: AetherAssaultState, +): ChallengeMetrics { + return { + bricksBroken: activityMode === "blockbreaker" ? blockbreaker.bricksBroken : 0, + bossKills: activityMode === "hockey-healing" ? endlessBossKills : 0, + kills: activityMode === "aether-assault" ? aetherAssault.kills : 0, + }; +} + +function createRpgCombatState(state: GameState, run: RpgRoguelikeRunState): Partial { + const activityMode = rpgActivityForRun(run); + const challenge = run.phase === "challenge-active"; + const currentBossId = selectCurrentBossId(run) ?? state.boss.id; + const challengePartner = run.bossRoute[(run.bossIndex + 1) % run.bossRoute.length] ?? currentBossId; + const bossIds = challenge && challengePartner !== currentBossId + ? [currentBossId, challengePartner] + : [currentBossId]; + const layout: EncounterLayout = challenge ? "hockey" : "standard"; + const act = Math.floor(run.bossIndex / BOSSES_PER_ACT); + const healthMultiplier = (challenge ? 0.82 + act * 0.08 : 1 + run.bossIndex * 0.14) + * DIFFICULTY_BY_SLUG[state.difficultySlug].healthMultiplier; + const encounterBosses = bossIds.map((bossId, index) => createEncounterBoss( + bossId, + index, + bossIds.length, + healthMultiplier, + 0, + layout, + )); + const primary = encounterBosses[0]; + const projection = projectRpgCombat(run, { + healerClassId: state.healerClassId, + playerName: state.playerName, + }); + const healerRatio = Math.max(0, Math.min(1, run.playerHp / 100)); + const party = projection.party.map((member) => member.id === "aelia" + ? { ...member, hp: member.maxHp * healerRatio } + : member); + const deterministicSeed = (run.random.state ^ ((run.bossIndex + 1) * 0x9e3779b9)) >>> 0; + const maxMana = 100; + + return { + rpgRun: run, + rpgFocusId: normalizeRpgFocusId(run, state.rpgFocusId), + rpgDamageProfiles: projection.damageProfiles, + rpgSpellResources: { verdancy: 0, tidalSurge: 0, conviction: 0, chronoshards: 0 }, + abilityLoadout: { ...run.abilityLoadout }, + activityMode, + phase: "combat", + round: run.bossIndex + 1, + endlessMode: challenge, + endlessBossKills: 0, + endlessSpawnSequence: encounterBosses.length, + bossId: primary.boss.id, + bossInstanceId: `rpg-${challenge ? "challenge" : "boss"}-${run.bossIndex}-${primary.boss.id}`, + boss: primary.boss, + bossMotion: primary.motion, + additionalBosses: encounterBosses.slice(1).map((entry, index) => ({ + ...entry, + instanceId: `rpg-${challenge ? "challenge" : "boss"}-${run.bossIndex}-${index + 1}-${entry.boss.id}`, + })), + seenBossIds: [...new Set([...state.seenBossIds, ...bossIds])], + party, + gearModifiers: projection.gearModifiers, + healingMultiplier: projection.gearModifiers.aelia.healingPower, + partyCombat: createPartyCombatState(party), + partyDamageEvents: [], + partyPositions: freshPartyPositions(bossIds, layout), + playerPosition: challenge ? [...HOCKEY_DEFAULT_PLAYER_POSITION] : [0, 4.5], + hockey: createHockeyHealingState(activityMode === "hockey-healing"), + blockbreaker: createBlockbreakerState(activityMode === "blockbreaker", deterministicSeed || 1), + aetherAssault: createAetherAssaultState(activityMode === "aether-assault", deterministicSeed || 1), + mana: maxMana, + maxMana, + cooldowns: emptyCooldowns(), + globalCooldownUntil: 0, + activeCast: null, + activeTab: "combat", + selectedMemberId: party.find((member) => member.id === "brann" && member.hp > 0)?.id + ?? party.find((member) => member.id !== "aelia" && member.hp > 0)?.id + ?? party.find((member) => member.hp > 0)?.id + ?? "aelia", + barrier: { + kind: null, + center: challenge ? [...HOCKEY_DEFAULT_PLAYER_POSITION] : [0, 4.5], + expiresAt: 0, + nextHealAt: 0, + }, + healerMechanic: createHealerMechanicState(state.healerClassId), + time: 0, + combatLog: [{ + id: Date.now(), + time: 0, + message: challenge + ? `${run.currentChallenge?.objective.name ?? "Hallway challenge"} begins.` + : `${primary.boss.name} guards room ${run.bossIndex + 1}.`, + tone: challenge ? "neutral" : "danger", + }], }; } export const useGameStore = create((set, get) => ({ ...initialState(), - configureHealer: (healerClassId, playerName, inventory, bossIds = "bulldrome", runMode = "encounter", gearProgress = createDefaultGearProgress(), difficultySlug = "initiate") => { - set(initialState(healerClassId, playerName, inventory, bossIds, runMode, 1, {}, gearProgress, difficultySlug)); + configureHealer: (healerClassId, playerName, inventory, bossIds = "bulldrome", runMode = "encounter", gearProgress = createDefaultGearProgress(), difficultySlug = "initiate", hockeyPvpMatch) => { + const base = initialState(healerClassId, playerName, inventory, bossIds, runMode, 1, {}, gearProgress, difficultySlug, [], hockeyPvpMatch); + if (runMode !== "rpg-roguelike") { + set(base); + return; + } + const rpgRun = createRpgRoguelikeRun({ seed: rpgRunSeed() }); + set({ + ...base, + rpgRun, + rpgFocusId: normalizeRpgFocusId(rpgRun, null), + rpgDamageProfiles: null, + abilityLoadout: {}, + phase: "briefing", + activityMode: "boss", + endlessMode: false, + }); }, startEncounter: () => { - const { healerClassId, playerName, inventory, boss, additionalBosses, runMode, round, runBuffRanks, gearProgress, difficultySlug, seenBossIds } = get(); + const current = get(); + if (current.runMode === "rpg-roguelike" && current.rpgRun) { + if (current.rpgRun.phase === "challenge-briefing") current.dispatchRpgAction({ type: "challenge-start" }); + else if (current.rpgRun.phase === "boss-briefing") current.dispatchRpgAction({ type: "boss-start" }); + else if (current.rpgRun.phase === "boss-cleared") current.dispatchRpgAction({ type: "reward-open" }); + return; + } + const { healerClassId, abilityLoadout, playerName, inventory, boss, additionalBosses, runMode, round, runBuffRanks, gearProgress, difficultySlug, seenBossIds, hockeyPvp } = get(); const bossIds = [boss.id, ...additionalBosses.map((entry) => entry.boss.id)]; set({ - ...initialState(healerClassId, playerName, inventory, bossIds, runMode, round, runBuffRanks, gearProgress, difficultySlug, seenBossIds), + ...initialState( + healerClassId, + playerName, + inventory, + bossIds, + runMode, + round, + runBuffRanks, + gearProgress, + difficultySlug, + seenBossIds, + runMode === "hockey-healing-pvp" + ? { matchId: hockeyPvp.matchId, seed: hockeyPvp.seed, opponentName: hockeyPvp.opponentName, role: hockeyPvp.role } + : undefined, + abilityLoadout, + ), phase: "combat", activeTab: "combat", combatLog: [{ id: Date.now(), time: 0, message: `${bossIds.map((bossId) => BOSS_DEFINITIONS[bossId].name).join(" and ")} engaged.`, tone: "danger" }], @@ -382,11 +839,52 @@ export const useGameStore = create((set, get) => ({ }, restart: () => { - const { healerClassId, playerName, inventory, boss, additionalBosses, runMode, gearProgress, difficultySlug } = get(); - const bossIds = runMode === "rogue-trials" + const { healerClassId, abilityLoadout, playerName, inventory, boss, additionalBosses, runMode, gearProgress, difficultySlug, hockeyPvp } = get(); + if (runMode === "rpg-roguelike") { + const base = initialState( + healerClassId, + playerName, + inventory, + boss.id, + runMode, + 1, + {}, + gearProgress, + difficultySlug, + [], + undefined, + {}, + ); + const rpgRun = createRpgRoguelikeRun({ seed: rpgRunSeed() }); + set({ + ...base, + rpgRun, + rpgFocusId: normalizeRpgFocusId(rpgRun, null), + abilityLoadout: {}, + }); + return; + } + const bossIds = runMode === "rogue-trials" || runMode === "hockey-healing" || runMode === "blockbreaker" || runMode === "aether-assault" ? selectRandomBossPair() + : runMode === "hockey-healing-pvp" + ? [hockeyPvpBossAt(hockeyPvp.seed, 0)] : [boss.id, ...additionalBosses.map((entry) => entry.boss.id)]; - set(initialState(healerClassId, playerName, inventory, bossIds, runMode, 1, {}, gearProgress, difficultySlug)); + set(initialState( + healerClassId, + playerName, + inventory, + bossIds, + runMode, + 1, + {}, + gearProgress, + difficultySlug, + [], + runMode === "hockey-healing-pvp" + ? { matchId: hockeyPvp.matchId, seed: hockeyPvp.seed, opponentName: hockeyPvp.opponentName, role: hockeyPvp.role } + : undefined, + abilityLoadout, + )); }, selectMember: (selectedMemberId) => set({ selectedMemberId }), @@ -421,7 +919,7 @@ export const useGameStore = create((set, get) => ({ const bossIds = state.runMode === "rogue-trials" ? selectRogueTrialsBosses(round, state.seenBossIds) : selectRandomBossPair(previousBossIds); - const abilityName = HEALER_CLASSES[state.healerClassId].abilities[RUN_BUFFS[buffId].abilityId].name; + const abilityName = HEALER_CLASSES[state.healerClassId].abilities[RUN_BUFFS[buffId].abilitySlotId].name; set({ ...initialState(state.healerClassId, state.playerName, state.inventory, bossIds, state.runMode, round, runBuffRanks, state.gearProgress, state.difficultySlug, state.seenBossIds), phase: "combat", @@ -496,8 +994,89 @@ export const useGameStore = create((set, get) => ({ return true; }, setEndlessChoiceSelection: (endlessChoiceSelection) => set({ endlessChoiceSelection }), + setAbilityLoadout: (abilityLoadout) => set({ abilityLoadout: { ...abilityLoadout }, cooldowns: emptyCooldowns(), activeCast: null }), + dispatchRpgAction: (action) => { + const state = get(); + if (state.runMode !== "rpg-roguelike" || !state.rpgRun) return false; + const actionRun = action.type === "reward-open" && state.rpgRun.phase === "boss-cleared" + ? reduceRpgRoguelikeRun(state.rpgRun, { + type: "party-vitals", + vitals: extractRpgPartyVitals(state.party, assignRosterToCombatSlots(state.rpgRun.roster)), + playerHp: extractRpgPlayerVital(state.party), + }) + : state.rpgRun; + const nextRun = reduceRpgRoguelikeRun(actionRun, action); + if (nextRun === state.rpgRun) return false; + + const enteringCombat = state.rpgRun.phase !== nextRun.phase + && (nextRun.phase === "challenge-active" || nextRun.phase === "boss-combat"); + if (enteringCombat) { + set(createRpgCombatState(state, nextRun)); + return true; + } + if (nextRun.phase === "challenge-active" || nextRun.phase === "boss-combat") { + set({ rpgRun: nextRun }); + return true; + } + + set({ + rpgRun: nextRun, + rpgFocusId: normalizeRpgFocusId(nextRun, state.rpgFocusId), + abilityLoadout: { ...nextRun.abilityLoadout }, + activityMode: rpgActivityForRun(nextRun), + phase: gamePhaseForRpgRun(nextRun), + endlessMode: false, + activeCast: null, + activeTab: "combat", + }); + return true; + }, + setRpgFocusId: (requestedFocusId) => set((state) => { + if (!state.rpgRun) return state; + const rpgFocusId = normalizeRpgFocusId(state.rpgRun, requestedFocusId); + return rpgFocusId === state.rpgFocusId ? state : { rpgFocusId }; + }), + cycleRpgFocus: (direction) => set((state) => { + if (!state.rpgRun) return state; + const items = rpgFocusItems(state.rpgRun); + if (!items.length) return state; + const currentIndex = items.findIndex((item) => item.id === state.rpgFocusId); + const startIndex = currentIndex >= 0 ? currentIndex : 0; + const rpgFocusId = items[(startIndex + direction + items.length) % items.length].id; + return rpgFocusId === state.rpgFocusId ? state : { rpgFocusId }; + }), + moveRpgFocus: (direction) => set((state) => { + if (!state.rpgRun) return state; + const rpgFocusId = moveRpgFocus(state.rpgRun, state.rpgFocusId, direction); + return rpgFocusId === state.rpgFocusId ? state : { rpgFocusId }; + }), setPlayerPosition: (playerPosition) => set((state) => { - playerPosition = clampToArena(playerPosition); + if (state.runMode === "rpg-roguelike" && state.activityMode === "boss" && state.rpgRun) { + const portals = state.rpgRun.phase === "boss-cleared" ? NORTH_OPEN_BOSS_ARENA_PORTALS : CLOSED_BOSS_ARENA_PORTALS; + playerPosition = clampToBossArenaWithPortals(playerPosition, portals); + if (state.rpgRun.phase === "boss-cleared" && detectBossArenaExit(playerPosition, portals) === "north") { + const syncedRun = reduceRpgRoguelikeRun(state.rpgRun, { + type: "party-vitals", + vitals: extractRpgPartyVitals(state.party, assignRosterToCombatSlots(state.rpgRun.roster)), + playerHp: extractRpgPlayerVital(state.party), + }); + const rpgRun = reduceRpgRoguelikeRun(syncedRun, { type: "reward-open" }); + return { + playerPosition, + partyPositions: { ...state.partyPositions, aelia: [...playerPosition] }, + rpgRun, + rpgFocusId: normalizeRpgFocusId(rpgRun, null), + phase: gamePhaseForRpgRun(rpgRun), + activeCast: null, + }; + } + } else { + playerPosition = state.activityMode === "aether-assault" + ? clampToHockeyArena(playerPosition, 0.65) + : state.activityMode === "hockey-healing" || state.activityMode === "hockey-healing-pvp" || state.activityMode === "blockbreaker" + ? clampToHockeyHealerHalf(playerPosition, 0.65) + : clampToArena(playerPosition); + } const current = state.playerPosition; const partyCurrent = state.partyPositions.aelia; if (current[0] === playerPosition[0] @@ -511,40 +1090,133 @@ export const useGameStore = create((set, get) => ({ partyPositions: { ...state.partyPositions, aelia: [...playerPosition] }, }; }), + setHockeyAimDirection: (direction) => set((state) => { + if (state.activityMode === "hockey-healing-pvp") { + if (state.hockeyPvp.aimDirection[0] === direction[0] && state.hockeyPvp.aimDirection[1] === direction[1]) return state; + return { hockeyPvp: { ...state.hockeyPvp, aimDirection: [...direction] } }; + } + if (state.activityMode === "blockbreaker") { + const blockbreaker = setBlockbreakerAim(state.blockbreaker, direction); + return blockbreaker === state.blockbreaker ? state : { blockbreaker }; + } + if (state.activityMode !== "hockey-healing") return state; + const hockey = setHockeyAim(state.hockey, direction); + return hockey === state.hockey ? state : { hockey }; + }), + applyHockeyPvpRemoteSnapshot: (snapshot, hostPuck) => set((state) => { + if (state.runMode !== "hockey-healing-pvp" || state.hockeyPvp.role === "cpu") return state; + const authoritativePuck = state.hockeyPvp.role === "guest" && hostPuck + ? mirrorHockeyPvpPuck(hostPuck) + : undefined; + const previousLocalGoals = state.hockeyPvp.localGoalsConceded; + const nextLocalGoals = authoritativePuck?.localGoalsConceded ?? previousLocalGoals; + const newGoals = Math.max(0, nextLocalGoals - previousLocalGoals); + let reactiveTriggers = 0; + const party = newGoals > 0 + ? state.party.map((member) => { + const damaged = damageMember(member, HOCKEY_PVP_GOAL_DAMAGE * newGoals); + const reactive = resolveReactiveHealAfterDamage(member, damaged, state.time, state.gearModifiers.aelia.healingPower); + if (reactive.triggered) reactiveTriggers += 1; + return reactive.member; + }) + : state.party; + const opponentParty = snapshot.party.map((member) => ({ ...member, debuffs: [...member.debuffs] })); + const opponentWiped = isPartyWiped(opponentParty); + const localWiped = isPartyWiped(party); + const phase = localWiped ? "defeat" : opponentWiped ? "victory" : state.phase; + return { + party, + healerMechanic: state.healerClassId === "shaman" && reactiveTriggers > 0 + ? { ...state.healerMechanic, resource: Math.min(state.healerMechanic.maxResource, state.healerMechanic.resource + reactiveTriggers) } + : state.healerMechanic, + phase, + hockeyPvp: { + ...state.hockeyPvp, + ...(authoritativePuck ?? {}), + opponentBossKills: snapshot.bossKills, + opponentPlayerPosition: [...snapshot.playerPosition], + opponentAimDirection: [...snapshot.aimDirection], + networkSequence: Math.max(state.hockeyPvp.networkSequence, snapshot.sequence), + appliedGoalSequence: authoritativePuck?.goalSequence ?? state.hockeyPvp.appliedGoalSequence, + status: localWiped ? "lost" : opponentWiped ? "won" : state.hockeyPvp.status, + }, + hockeyPvpOpponent: { + ...state.hockeyPvpOpponent, + party: opponentParty, + partyPositions: structuredClone(snapshot.partyPositions), + boss: { + ...state.hockeyPvpOpponent.boss, + ...snapshot.boss, + nextMeleeAt: state.hockeyPvpOpponent.boss.nextMeleeAt, + }, + bossMotion: { + ...state.hockeyPvpOpponent.bossMotion, + bossId: snapshot.boss.id, + position: [...snapshot.bossPosition], + mode: snapshot.bossMode, + }, + }, + }; + }), - castAbility: (abilityId) => { + castAbility: (abilitySlotId) => { const state = get(); if (state.phase !== "combat") return false; + if (state.rpgRun && state.rpgRun.phase !== "challenge-active" && state.rpgRun.phase !== "boss-combat") return false; if (state.paused) return false; if (state.activeCast) return false; const healer = state.party.find((member) => member.id === "aelia"); if (!healer || healer.hp <= 0) return false; - const ability = HEALER_CLASSES[state.healerClassId].abilities[abilityId]; - const manaCost = runAbilityManaCost(abilityId, ability.mana, state.runModifiers); + const ability = resolveSlottedAbility(state.abilityLoadout, abilitySlotId); + if (!ability) return false; + const spellPower = state.rpgRun + ? spellRankPowerMultiplier(state.rpgRun.spellRanks, ability.id) + : 1; + const manaCost = runAbilityManaCost(abilitySlotId, ability.mana, state.runModifiers); const selectedIndex = state.party.findIndex((member) => member.id === state.selectedMemberId); const selected = state.party[selectedIndex]; - if (state.cooldowns[abilityId] > state.time + 0.01) return false; + if (state.cooldowns[abilitySlotId] > state.time + 0.01) return false; if (state.globalCooldownUntil > state.time + 0.001) return false; if (state.mana < manaCost) { - set({ combatLog: addLog(state.combatLog, state.time, "Not enough mana.", "danger") }); + set({ combatLog: addLog(state.combatLog, state.time, `Not enough ${HEALER_CLASSES[state.healerClassId].resourceName.toLowerCase()}.`, "danger") }); return false; } if (ability.targeting === "ally" && (!selected || selected.hp <= 0)) return false; - if (abilityId === "purify" && selected.debuffs.length === 0) { + if (ability.targeting === "enemy" && state.boss.hp <= 0 && state.additionalBosses.every((entry) => entry.boss.hp <= 0)) return false; + if (ability.pulseKind === "cleanse" && selected.debuffs.length === 0) { set({ combatLog: addLog(state.combatLog, state.time, `${selected.name} has nothing to ${ability.name}.`) }); return false; } - if (abilityId === "mend") { + if (ability.castTime) { + let resourceSpent = 0; + let castTime = runAbilityCastTime(abilitySlotId, ability.castTime, state.runModifiers); + const rpgSpellResources = { ...state.rpgSpellResources }; + if (ability.id === "druid-regrowth") { + resourceSpent = Math.min(3, state.rpgRun ? rpgSpellResources.verdancy : state.healerMechanic.resource); + if (state.rpgRun) rpgSpellResources.verdancy -= resourceSpent; + } + const availableSurge = state.rpgRun ? rpgSpellResources.tidalSurge : state.healerMechanic.resource; + if (ability.id === "shaman-healing-wave" && availableSurge > 0) { + resourceSpent = 1; + castTime *= 0.5; + if (state.rpgRun) rpgSpellResources.tidalSurge -= 1; + } set({ activeCast: { - abilityId: "mend", + slotId: abilitySlotId, + abilityId: ability.id, targetId: selected.id, startedAt: state.time, - completesAt: state.time + runAbilityCastTime("mend", ability.castTime ?? 0.5, state.runModifiers), + completesAt: state.time + castTime, + resourceSpent, }, + healerMechanic: resourceSpent > 0 && !state.rpgRun + ? { ...state.healerMechanic, resource: state.healerMechanic.resource - resourceSpent } + : state.healerMechanic, + rpgSpellResources, mana: Math.max(0, state.mana - manaCost), globalCooldownUntil: state.time + GLOBAL_COOLDOWN_SECONDS, combatLog: addLog(state.combatLog, state.time, `Casting ${ability.name} on ${selected.name}...`), @@ -552,29 +1224,43 @@ export const useGameStore = create((set, get) => ({ return true; } - let party = state.party.map((member) => ({ ...member, debuffs: [...member.debuffs] })); + let party = state.party.map((member) => ({ + ...member, + debuffs: [...member.debuffs], + healingEffects: member.healingEffects.map((effect) => ({ ...effect })), + reactiveHeal: member.reactiveHeal ? { ...member.reactiveHeal } : null, + })); let message = ability.name; + const healingMultiplier = effectiveHealingMultiplier(state) * spellPower; let barrier = state.barrier; + let boss = state.boss; let bossMotion = state.bossMotion; let additionalBosses = state.additionalBosses; + let healerMechanic = { ...state.healerMechanic }; + const rpgSpellResources = { ...state.rpgSpellResources }; + const cooldowns = { ...state.cooldowns }; - switch (abilityId) { - case "renew": - applyRenewAt(party, selectedIndex, state.time, state.runModifiers); + switch (ability.id) { + case "priest-renew": + applyRenewAt(party, selectedIndex, state.time, state.runModifiers, spellPower); for (const index of lowestHealthIndexes(party, selectedIndex, state.runModifiers.renewExtraTargets)) { - applyRenewAt(party, index, state.time, state.runModifiers); + applyRenewAt(party, index, state.time, state.runModifiers, spellPower); } message = `${ability.name} placed on ${selected.name}.`; break; - case "shield": { - const amount = addHealerAbsorb(party, selectedIndex, 36, state.gearModifiers.aelia.healingPower, state.runModifiers); + case "priest-aegis-shield": { + const amount = addHealerAbsorb(party, selectedIndex, 36, state.gearModifiers.aelia.healingPower * spellPower, state.runModifiers); for (const index of lowestHealthIndexes(party, selectedIndex, state.runModifiers.shieldExtraTargets)) { - addHealerAbsorb(party, index, 18, state.gearModifiers.aelia.healingPower, state.runModifiers); + addHealerAbsorb(party, index, 18, state.gearModifiers.aelia.healingPower * spellPower, state.runModifiers); } message = `${selected.name} gains ${Math.round(amount)} absorption.`; break; } - case "purify": { + case "priest-purify": + case "druid-natures-cure": + case "shaman-cleanse-spirit": + case "paladin-cleanse-light": + case "chronomancer-erase-affliction": { const cleanseIndexes = [ selectedIndex, ...lowestHealthIndexes(party, selectedIndex, state.runModifiers.purifyExtraTargets, (member) => member.debuffs.length > 0), @@ -590,55 +1276,262 @@ export const useGameStore = create((set, get) => ({ return { ...entry, motion: dispel.motion }; }); party[index] = { ...party[index], debuffs: [] }; - if (state.runModifiers.purifyAppliesRenew) applyRenewAt(party, index, state.time, state.runModifiers); - if (state.runModifiers.purifyAppliesShield) { - addHealerAbsorb(party, index, 18, state.gearModifiers.aelia.healingPower, state.runModifiers); + if (ability.id === "druid-natures-cure") { + party[index] = triggerHealingEffects(party[index], healingMultiplier).member; + if (state.runModifiers.purifyAppliesRenew) applyRejuvenationAt(party, index, state.time, state.runModifiers, spellPower); + if (state.runModifiers.purifyAppliesShield) applyLifebloomAt(party, index, state.time, state.runModifiers, 1, spellPower); + } else if (ability.id === "shaman-cleanse-spirit") { + if (state.runModifiers.purifyAppliesRenew) applyRiptideAt(party, index, state.time, state.runModifiers, spellPower); + if (state.runModifiers.purifyAppliesShield) applyEarthShieldAt(party, index, state.time, state.runModifiers, 3, spellPower); + } else { + if (state.runModifiers.purifyAppliesRenew) applyRenewAt(party, index, state.time, state.runModifiers, spellPower); + if (state.runModifiers.purifyAppliesShield) { + addHealerAbsorb(party, index, 18, state.gearModifiers.aelia.healingPower * spellPower, state.runModifiers); + } } } + if (ability.id === "shaman-cleanse-spirit") { + if (state.rpgRun) rpgSpellResources.tidalSurge = Math.min(2, rpgSpellResources.tidalSurge + 1); + else healerMechanic.resource = Math.min(healerMechanic.maxResource, healerMechanic.resource + 1); + } else if (ability.id === "chronomancer-erase-affliction") { + if (state.rpgRun) rpgSpellResources.chronoshards = Math.min(3, rpgSpellResources.chronoshards + 1); + else healerMechanic.resource = Math.min(healerMechanic.maxResource, healerMechanic.resource + 1); + } else if (ability.id === "paladin-cleanse-light") { + const result = healTargetAndBeacon(party, selectedIndex, 10 * healingMultiplier, healerMechanic, state.time); + party = result.party; + } message = primaryNames.includes("Widow Venom") ? "Widow Venom purged. A venom pool forms where the target stood." : `${primaryNames.join(", ") || "Harmful magic"} removed from ${selected.name}.`; break; } - case "radiance": - party = party.map((member) => healMember(member, 22 * state.healingMultiplier)); + case "priest-radiance": + party = party.map((member) => healMember(member, 22 * healingMultiplier)); if (state.runModifiers.radianceAppliesRenew) { - for (let index = 0; index < party.length; index += 1) applyRenewAt(party, index, state.time, state.runModifiers); + for (let index = 0; index < party.length; index += 1) applyRenewAt(party, index, state.time, state.runModifiers, spellPower); } if (state.runModifiers.radianceAbsorb > 0) { for (let index = 0; index < party.length; index += 1) { - addHealerAbsorb(party, index, state.runModifiers.radianceAbsorb, state.gearModifiers.aelia.healingPower, state.runModifiers); + addHealerAbsorb(party, index, state.runModifiers.radianceAbsorb, state.gearModifiers.aelia.healingPower * spellPower, state.runModifiers); } } message = `${ability.name} heals the full party.`; break; - case "barrier": + case "priest-barrier": barrier = { + kind: "barrier", center: [...state.partyPositions.aelia], expiresAt: state.time + 8 + state.runModifiers.barrierDurationBonus, nextHealAt: state.time + 1, }; message = `${ability.name} protects a 3m circle for ${8 + state.runModifiers.barrierDurationBonus} seconds.`; break; + case "druid-rejuvenation": + applyRejuvenationAt(party, selectedIndex, state.time, state.runModifiers, spellPower); + for (const index of lowestHealthIndexes(party, selectedIndex, state.runModifiers.renewExtraTargets)) { + applyRejuvenationAt(party, index, state.time, state.runModifiers, spellPower); + } + message = `${ability.name} begins restoring ${selected.name}.`; + break; + case "druid-lifebloom": { + const current = healingEffect(party[selectedIndex], "lifebloom"); + const verdancy = state.rpgRun ? rpgSpellResources.verdancy : healerMechanic.resource; + if (current?.stacks === 3 && verdancy >= 2) { + const bloom = 10 * current.stacks * state.runModifiers.shieldAbsorbMultiplier * healingMultiplier; + party[selectedIndex] = healMember(removeHealingEffect(party[selectedIndex], "lifebloom"), bloom); + if (state.rpgRun) rpgSpellResources.verdancy -= 2; + else healerMechanic.resource -= 2; + message = `${ability.name} blooms on ${selected.name} for ${Math.round(bloom)}.`; + } else { + const stacks = applyLifebloomAt(party, selectedIndex, state.time, state.runModifiers, 1, spellPower); + for (const index of lowestHealthIndexes(party, selectedIndex, state.runModifiers.shieldExtraTargets)) { + applyLifebloomAt(party, index, state.time, state.runModifiers, 1, spellPower); + } + message = `${ability.name} grows to ${stacks} ${stacks === 1 ? "stack" : "stacks"} on ${selected.name}.`; + } + break; + } + case "druid-wild-growth": { + const targets = lowestHealthIndexes(party, -1, 3); + for (const index of targets) { + party[index] = setHealingEffect(party[index], { + id: "wild-growth", + expiresAt: state.time + 6, + nextTickAt: state.time + 1, + tickInterval: 1, + healingPerTick: 5 * spellPower, + stacks: 1, + }); + } + if (state.runModifiers.radianceAppliesRenew) { + for (let index = 0; index < party.length; index += 1) applyRejuvenationAt(party, index, state.time, state.runModifiers, spellPower); + } + if (state.runModifiers.radianceAbsorb > 0) { + party = party.map((member) => healMember(member, state.runModifiers.radianceAbsorb * healingMultiplier)); + } + message = `${ability.name} spreads to ${targets.length} injured allies.`; + break; + } + case "druid-flourish": + party = flourishParty(party, state.time, 5 + state.runModifiers.barrierDurationBonus); + healerMechanic.flourishExpiresAt = state.time + 6 + state.runModifiers.barrierDurationBonus; + healerMechanic.nextPulseAt = state.time + 1; + message = `${ability.name} accelerates and extends every active growth.`; + break; + case "shaman-riptide": + party[selectedIndex] = healMember(party[selectedIndex], 12 * healingMultiplier); + applyRiptideAt(party, selectedIndex, state.time, state.runModifiers, spellPower); + for (const index of lowestHealthIndexes(party, selectedIndex, state.runModifiers.renewExtraTargets)) { + party[index] = healMember(party[index], 6 * healingMultiplier); + applyRiptideAt(party, index, state.time, state.runModifiers, spellPower); + } + if (state.rpgRun) rpgSpellResources.tidalSurge = Math.min(2, rpgSpellResources.tidalSurge + 1); + else healerMechanic.resource = Math.min(healerMechanic.maxResource, healerMechanic.resource + 1); + message = `${ability.name} marks ${selected.name} as a chain anchor.`; + break; + case "shaman-earth-shield": + applyEarthShieldAt(party, selectedIndex, state.time, state.runModifiers, 6, spellPower); + for (const index of lowestHealthIndexes(party, selectedIndex, state.runModifiers.shieldExtraTargets)) { + applyEarthShieldAt(party, index, state.time, state.runModifiers, 3, spellPower); + } + message = `${selected.name} gains 6 ${ability.name} charges.`; + break; + case "shaman-chain-heal": { + const surgeSpent = state.rpgRun ? rpgSpellResources.tidalSurge : healerMechanic.resource; + if (state.rpgRun) rpgSpellResources.tidalSurge = 0; + else healerMechanic.resource = 0; + const targets = chainHealIndexes(party, state.partyPositions, selectedIndex, 3 + surgeSpent); + for (let order = 0; order < targets.length; order += 1) { + const index = targets[order]; + const riptideBonus = order === 0 && healingEffect(party[index], "riptide") ? 1.25 : 1; + party[index] = healMember(party[index], 30 * 0.75 ** order * riptideBonus * healingMultiplier); + } + if (state.runModifiers.radianceAppliesRenew) { + for (let index = 0; index < party.length; index += 1) applyRiptideAt(party, index, state.time, state.runModifiers, spellPower); + } + if (state.runModifiers.radianceAbsorb > 0) { + party = party.map((member) => healMember(member, state.runModifiers.radianceAbsorb * healingMultiplier)); + } + message = `${ability.name} reaches ${targets.length} ${targets.length === 1 ? "ally" : "allies"}${surgeSpent ? ` using ${surgeSpent} Tidal Surge` : ""}.`; + break; + } + case "shaman-spirit-link": + barrier = { + kind: "spirit-link", + center: [...state.partyPositions.aelia], + expiresAt: state.time + 8 + state.runModifiers.barrierDurationBonus, + nextHealAt: state.time + 1, + }; + message = `${ability.name} links allies inside a 3m circle.`; + break; + case "paladin-crusader-strike": { + const requestedDamage = 18 * spellPower; + let damage = 0; + if (boss.hp > 0) { + damage = Math.min(boss.hp, requestedDamage); + boss = { ...boss, hp: boss.hp - damage }; + } else { + const targetIndex = additionalBosses.findIndex((entry) => entry.boss.hp > 0); + const target = additionalBosses[targetIndex]; + if (target) { + damage = Math.min(target.boss.hp, requestedDamage); + additionalBosses = additionalBosses.map((entry, index) => index === targetIndex + ? { ...entry, boss: { ...entry.boss, hp: entry.boss.hp - damage } } + : entry); + } + } + const result = healMostInjured(party, 18 * healingMultiplier); + party = result.party; + if (state.rpgRun) rpgSpellResources.conviction = Math.min(3, rpgSpellResources.conviction + 1); + else healerMechanic.resource = Math.min(healerMechanic.maxResource, healerMechanic.resource + 1); + message = result.targetId + ? `${ability.name} deals ${Math.round(damage)} and restores ${Math.round(result.healing)} health.` + : `${ability.name} deals ${Math.round(damage)} damage.`; + break; + } + case "paladin-beacon-of-light": + healerMechanic.beaconTargetId = selected.id; + healerMechanic.beaconExpiresAt = state.time + 30; + message = `${selected.name} becomes your Beacon of Light.`; + break; + case "paladin-word-of-glory": { + const conviction = state.rpgRun ? rpgSpellResources.conviction : healerMechanic.resource; + if (state.rpgRun) rpgSpellResources.conviction = 0; + else healerMechanic.resource = 0; + const amount = (18 + conviction * 14) * healingMultiplier; + const result = healTargetAndBeacon(party, selectedIndex, amount, healerMechanic, state.time); + party = result.party; + if (conviction === 3) party = party.map((member) => healMember(member, 8 * healingMultiplier)); + message = `${ability.name} spends ${conviction} Conviction and restores ${Math.round(result.directHealing)} health.`; + break; + } + case "paladin-avenging-crusader": + healerMechanic.avengingCrusaderExpiresAt = state.time + 10 + state.runModifiers.barrierDurationBonus; + message = `${ability.name} converts party damage into healing for ${10 + state.runModifiers.barrierDurationBonus} seconds.`; + break; + case "chronomancer-time-anchor": { + const result = placeOrRewindTimeAnchor(party, selectedIndex, healerMechanic, state.time); + party = result.party; + healerMechanic = result.mechanic; + if (result.restored > 0) { + if (state.rpgRun) rpgSpellResources.chronoshards = Math.min(3, rpgSpellResources.chronoshards + 1); + else healerMechanic.resource = Math.min(healerMechanic.maxResource, healerMechanic.resource + 1); + } + message = result.anchored + ? `${selected.name}'s timeline is anchored for 6 seconds.` + : `${selected.name} rewinds ${Math.round(result.restored)} health.`; + break; + } + case "chronomancer-echo-of-tomorrow": + party[selectedIndex] = setHealingEffect(healMember(party[selectedIndex], 10 * healingMultiplier), { + id: "temporal-echo", + expiresAt: state.time + 3, + nextTickAt: state.time + 3, + tickInterval: 3, + healingPerTick: 28 * spellPower, + stacks: 1, + }); + message = `${selected.name} receives healing now and again in 3 seconds.`; + break; + case "chronomancer-accelerate": { + const chronoshards = state.rpgRun ? rpgSpellResources.chronoshards : healerMechanic.resource; + if (state.rpgRun) rpgSpellResources.chronoshards = 0; + else healerMechanic.resource = 0; + party = party.map((member) => healMember(member, (10 + chronoshards * 8) * healingMultiplier)); + if (chronoshards > 0) { + for (const slotId of Object.keys(cooldowns) as AbilitySlotId[]) { + cooldowns[slotId] = Math.max(state.time, cooldowns[slotId] - chronoshards); + } + } + message = `${ability.name} spends ${chronoshards} Chronoshards and advances active cooldowns.`; + break; + } + case "chronomancer-time-loop": + healerMechanic = startTimeLoop(party, healerMechanic, state.time + 6); + message = `${ability.name} records every living ally for 6 seconds.`; + break; } - const cooldowns = { - ...state.cooldowns, - [abilityId]: ability.cooldown > 0 - ? state.time + runAbilityCooldown(abilityId, ability.cooldown, state.runModifiers) * state.gearModifiers.aelia.cooldown - : 0, - }; + cooldowns[abilitySlotId] = ability.cooldown > 0 + ? state.time + + runAbilityCooldown(abilitySlotId, ability.cooldown, state.runModifiers) + * (state.rpgRun ? spellRankCooldownMultiplier(state.rpgRun.spellRanks, ability.id) : 1) + * state.gearModifiers.aelia.cooldown + : 0; const pulse: ScenePulse = { id: state.scenePulse.id + 1, - kind: abilityId, - targetId: abilityId === "barrier" ? "aelia" : selected?.id, + kind: ability.pulseKind, + targetId: ability.targeting === "party" ? "aelia" : ability.targeting === "ally" ? selected?.id : undefined, }; set({ party, + boss, cooldowns, globalCooldownUntil: state.time + GLOBAL_COOLDOWN_SECONDS, barrier, + healerMechanic, + rpgSpellResources, bossMotion, additionalBosses, mana: Math.max(0, state.mana - manaCost), @@ -654,9 +1547,21 @@ export const useGameStore = create((set, get) => ({ const oldTime = state.time; const time = oldTime + Math.min(delta, 2); + const healingMultiplier = effectiveHealingMultiplier(state); + let reactiveResourceGains = 0; + const damageLocalMember = (member: PartyMember, amount: number, at = time) => { + const damaged = damageMember(member, amount); + const reactive = resolveReactiveHealAfterDamage(member, damaged, at, state.gearModifiers.aelia.healingPower); + if (reactive.triggered) reactiveResourceGains += 1; + return reactive.member; + }; // Debuffs are copied only by the periodic-debuff pass below. Copying them here // as well doubled short-lived allocations for every simulation step. - let party = state.party.map((member) => ({ ...member })); + let party = state.party.map((member) => ({ + ...member, + healingEffects: member.healingEffects.map((effect) => ({ ...effect })), + reactiveHeal: member.reactiveHeal ? { ...member.reactiveHeal } : null, + })); let boss = { ...state.boss }; let bossInstanceId = state.bossInstanceId; let bossMotion = { ...state.bossMotion }; @@ -669,15 +1574,146 @@ export const useGameStore = create((set, get) => ({ let combatLog = state.combatLog; let pulse = state.scenePulse; let activeCast = state.activeCast ? { ...state.activeCast } : null; + let healerMechanic = { ...state.healerMechanic }; + let rpgSpellResources = { ...state.rpgSpellResources }; let partyCombat = state.partyCombat; let partyDamageEvents = state.partyDamageEvents; let barrier = { ...state.barrier }; + let rpgRun = state.rpgRun; + let rpgFocusId = state.rpgFocusId; + let activityMode = state.activityMode; + let endlessMode = state.endlessMode; let endlessBossKills = state.endlessBossKills; let endlessSpawnSequence = state.endlessSpawnSequence; + let hockeyPvp = { ...state.hockeyPvp }; + let hockeyPvpOpponent: HockeyPvpOpponentState = { + party: state.hockeyPvpOpponent.party.map((member) => ({ ...member, debuffs: [...member.debuffs] })), + partyPositions: structuredClone(state.hockeyPvpOpponent.partyPositions), + boss: { ...state.hockeyPvpOpponent.boss }, + bossMotion: cloneMotion(state.hockeyPvpOpponent.bossMotion), + partyCombat: state.hockeyPvpOpponent.partyCombat, + }; + let hockey = state.activityMode === "hockey-healing" + ? advanceHockeyHealing(state.hockey, { + delta: time - oldTime, + time, + playerPosition: state.partyPositions.aelia, + }) + : state.hockey; + let blockbreaker = state.activityMode === "blockbreaker" + ? advanceBlockbreaker(state.blockbreaker, { + delta: time - oldTime, + time, + playerPosition: state.partyPositions.aelia, + }) + : state.blockbreaker; + const aetherAdvance = state.activityMode === "aether-assault" + ? advanceAetherAssault(state.aetherAssault, { + delta: time - oldTime, + time, + playerPosition: state.partyPositions.aelia, + canAutoFire: party[0].hp > 0 && party[0].knockedUntil <= time, + }) + : { state: state.aetherAssault, playerDamage: 0 }; + let aetherAssault = aetherAdvance.state; + if (aetherAdvance.playerDamage > 0 && party[0].hp > 0) { + party[0] = damageMemberAt( + party[0], + aetherAdvance.playerDamage, + state.partyPositions.aelia, + barrier, + time, + partyCombat, + state.partyPositions[partyCombat.tankAura.sourceId], + state.difficultyDamageMultiplier, + state.gearModifiers, + "hazard", + state.runModifiers.shieldDamageTakenMultiplier, + () => { reactiveResourceGains += 1; }, + ); + combatLog = addLog(combatLog, time, `Aether strike hits ${state.playerName} for ${aetherAdvance.playerDamage} base damage.`, "danger"); + } + const blockbreakerBreachEvents = state.activityMode === "blockbreaker" + ? Math.max(0, blockbreaker.breaches - state.blockbreaker.breaches) + : 0; + if (blockbreakerBreachEvents > 0) { + const breachDamage = BLOCKBREAKER_BREACH_DAMAGE * blockbreakerBreachEvents; + party = party.map((member) => damageLocalMember(member, breachDamage)); + combatLog = addLog( + combatLog, + time, + `Brick breach — ${breachDamage} damage to every party member.`, + "danger", + ); + } + + if (state.activityMode === "hockey-healing-pvp") { + if (hockeyPvp.role === "cpu") { + const goalie = advanceHockeyPvpCpuGoalie( + hockeyPvp.opponentPlayerPosition, + hockeyPvp.puckPosition, + time - oldTime, + ); + hockeyPvp.opponentPlayerPosition = goalie.position; + hockeyPvp.opponentAimDirection = goalie.aimDirection; + hockeyPvpOpponent.partyPositions.aelia = [...goalie.position]; + } + hockeyPvp = advanceHockeyPvpPuck(hockeyPvp, { + delta: time - oldTime, + localPlayerPosition: state.partyPositions.aelia, + localAimDirection: hockeyPvp.aimDirection, + opponentPlayerPosition: hockeyPvp.opponentPlayerPosition, + opponentAimDirection: hockeyPvp.opponentAimDirection, + }); + const localGoals = Math.max(0, hockeyPvp.localGoalsConceded - state.hockeyPvp.localGoalsConceded); + if (localGoals > 0) { + party = party.map((member) => damageLocalMember(member, HOCKEY_PVP_GOAL_DAMAGE * localGoals)); + combatLog = addLog(combatLog, time, `Goal conceded — ${HOCKEY_PVP_GOAL_DAMAGE} partywide damage.`, "danger"); + } + const opponentGoals = Math.max(0, hockeyPvp.opponentGoalsConceded - state.hockeyPvp.opponentGoalsConceded); + if (opponentGoals > 0 && hockeyPvp.role === "cpu") { + hockeyPvpOpponent.party = hockeyPvpOpponent.party.map((member) => damageMember(member, HOCKEY_PVP_GOAL_DAMAGE * opponentGoals)); + combatLog = addLog(combatLog, time, `Goal scored — rival party takes ${HOCKEY_PVP_GOAL_DAMAGE} damage.`, "good"); + } + hockeyPvp.appliedGoalSequence = hockeyPvp.goalSequence; + } if (!party.some((member) => member.id === "aelia" && member.hp > 0)) activeCast = null; - if (state.endlessMode) { + if (state.activityMode === "hockey-healing-pvp" && boss.hp <= 0) { + const replacementId = hockeyPvpBossAt(hockeyPvp.seed, endlessBossKills); + endlessSpawnSequence += 1; + const replacement = createEncounterBoss( + replacementId, + 0, + 1, + DIFFICULTY_BY_SLUG[state.difficultySlug].healthMultiplier, + time, + "hockey", + ); + boss = replacement.boss; + bossMotion = replacement.motion; + bossInstanceId = `hockey-pvp-${endlessSpawnSequence}-${replacementId}`; + combatLog = addLog(combatLog, time, `${replacement.boss.name} enters both boss lanes.`, "danger"); + } + + if (state.activityMode === "hockey-healing-pvp" + && hockeyPvp.role === "cpu" + && hockeyPvpOpponent.boss.hp <= 0) { + const replacementId = hockeyPvpBossAt(hockeyPvp.seed, hockeyPvp.opponentBossKills); + const replacement = createEncounterBoss( + replacementId, + 0, + 1, + DIFFICULTY_BY_SLUG[state.difficultySlug].healthMultiplier, + time, + "hockey", + ); + hockeyPvpOpponent.boss = replacement.boss; + hockeyPvpOpponent.bossMotion = replacement.motion; + } + + if (state.endlessMode && state.runMode !== "hockey-healing-pvp") { const slots: AdditionalBossState[] = [ { instanceId: bossInstanceId, boss, motion: bossMotion }, ...additionalBosses, @@ -686,21 +1722,34 @@ export const useGameStore = create((set, get) => ({ if (slots[index].boss.hp > 0) continue; const defeatedAt = slots[index].boss.defeatedAt ?? oldTime; slots[index].boss.defeatedAt = defeatedAt; - if (time < defeatedAt + BOSS_DEATH_DESPAWN_SECONDS) continue; + if (time < defeatedAt + bossDeathDespawnSeconds(slots[index].boss.id)) continue; const activeBossIds = slots .filter((entry, slotIndex) => slotIndex !== index && entry.boss.hp > 0) .map((entry) => entry.boss.id); - const replacementId = selectUnseenBosses(1, [slots[index].boss.id, ...activeBossIds], Math.random, activeBossIds)[0]; + const replacementId = state.runMode === "rpg-roguelike" && rpgRun + ? rpgRun.bossRoute[(rpgRun.bossIndex + endlessSpawnSequence + index + 1) % rpgRun.bossRoute.length] + : selectUnseenBosses(1, [slots[index].boss.id, ...activeBossIds], Math.random, activeBossIds)[0]; endlessSpawnSequence += 1; const difficulty = DIFFICULTY_BY_SLUG[state.difficultySlug]; + const replacementHealthMultiplier = state.activityMode === "hockey-healing" || state.activityMode === "blockbreaker" || state.activityMode === "aether-assault" + ? difficulty.healthMultiplier + : bossHealthMultiplier(ROGUE_TRIALS_TRIO_ROUND) * difficulty.healthMultiplier; const replacement = createEncounterBoss( replacementId, index, slots.length, - bossHealthMultiplier(ROGUE_TRIALS_TRIO_ROUND) * difficulty.healthMultiplier, + replacementHealthMultiplier, time, + state.activityMode === "hockey-healing" || state.activityMode === "blockbreaker" || state.activityMode === "aether-assault" ? "hockey" : "standard", ); - slots[index] = { ...replacement, instanceId: `endless-${endlessSpawnSequence}-${replacementId}` }; + const instancePrefix = state.activityMode === "hockey-healing" + ? "hockey" + : state.activityMode === "blockbreaker" + ? "blockbreaker" + : state.activityMode === "aether-assault" + ? "aether-assault" + : "endless"; + slots[index] = { ...replacement, instanceId: `${instancePrefix}-${endlessSpawnSequence}-${replacementId}` }; combatLog = addLog(combatLog, time, `${replacement.boss.name} replaces the fallen boss.`, "danger"); } boss = slots[0].boss; @@ -709,43 +1758,87 @@ export const useGameStore = create((set, get) => ({ additionalBosses = slots.slice(1); } + healerMechanic = expireHealerMechanics(healerMechanic, time); + const timeLoopResult = resolveTimeLoop(party, healerMechanic, time); + party = timeLoopResult.party; + healerMechanic = timeLoopResult.mechanic; + if (timeLoopResult.restored > 0) { + combatLog = addLog(combatLog, time, `Time Loop restores ${Math.round(timeLoopResult.restored)} party health.`, "good"); + pulse = { id: pulse.id + 1, kind: "group-heal", targetId: "aelia" }; + } + if (activeCast && activeCast.completesAt <= time) { const targetIndex = party.findIndex((member) => member.id === activeCast!.targetId); const target = party[targetIndex]; if (target?.hp > 0) { - const healing = 38 * state.healingMultiplier; - party[targetIndex] = healMember(target, healing); + const castSpellPower = rpgRun + ? spellRankPowerMultiplier(rpgRun.spellRanks, activeCast.abilityId) + : 1; + let healing = 0; + if (activeCast.abilityId === "priest-mend") { + healing = 38 * healingMultiplier * castSpellPower; + party[targetIndex] = healMember(target, healing); + } else if (activeCast.abilityId === "druid-regrowth") { + healing = (24 + activeCast.resourceSpent * 7) * healingMultiplier * castSpellPower; + party[targetIndex] = setHealingEffect(healMember(target, healing), { + id: "regrowth", + expiresAt: activeCast.completesAt + 6, + nextTickAt: activeCast.completesAt + 1, + tickInterval: 1, + healingPerTick: 4 * castSpellPower, + stacks: 1, + }); + } else if (activeCast.abilityId === "shaman-healing-wave") { + const lowHealthBonus = target.hp / target.maxHp < 0.5 ? 1.35 : 1; + healing = (34 * lowHealthBonus + activeCast.resourceSpent * 10) * healingMultiplier * castSpellPower; + party[targetIndex] = healMember(target, healing); + } else if (activeCast.abilityId === "paladin-holy-light") { + healing = 34 * healingMultiplier * castSpellPower; + party = healTargetAndBeacon(party, targetIndex, healing, healerMechanic, activeCast.completesAt).party; + } else if (activeCast.abilityId === "chronomancer-mend-timeline") { + healing = 30 * healingMultiplier * castSpellPower; + party[targetIndex] = healMember(target, healing); + } for (const index of lowestHealthIndexes(party, targetIndex, state.runModifiers.mendExtraTargets)) { party[index] = healMember(party[index], healing * 0.5); + if (activeCast.abilityId === "druid-regrowth") { + party[index] = setHealingEffect(party[index], { + id: "regrowth", + expiresAt: activeCast.completesAt + 6, + nextTickAt: activeCast.completesAt + 1, + tickInterval: 1, + healingPerTick: 2 * castSpellPower, + stacks: 1, + }); + } } - const abilityName = HEALER_CLASSES[state.healerClassId].abilities.mend.name; + const ability = HEALER_ABILITIES[activeCast.abilityId]; + const abilityName = ability.name; combatLog = addLog(combatLog, activeCast.completesAt, `${abilityName} restores ${target.name} for ${Math.round(healing)}.`, "good"); - pulse = { id: pulse.id + 1, kind: "mend", targetId: target.id }; + pulse = { id: pulse.id + 1, kind: ability.pulseKind, targetId: target.id }; } activeCast = null; } party = party.map((member) => { - let next = member; - if (next.renewExpiresAt > oldTime && next.renewNextTickAt <= time) { - let tickAt = next.renewNextTickAt; - const lastTickAt = Math.min(time, next.renewExpiresAt); - while (tickAt <= lastTickAt + 0.001) { - next = healMember(next, 7 * state.healingMultiplier * state.runModifiers.renewHealingMultiplier); - tickAt += 1; - } - next = { - ...next, - renewExpiresAt: time >= next.renewExpiresAt ? 0 : next.renewExpiresAt, - renewNextTickAt: time >= next.renewExpiresAt ? 0 : tickAt, - }; + const advanced = advanceHealingEffects(member, oldTime, time, healingMultiplier, healerMechanic.flourishExpiresAt); + let next = advanced.member; + if (rpgRun) { + const verdancyTicks = (advanced.tickCounts.regrowth ?? 0) + + (advanced.tickCounts.rejuvenation ?? 0) + + (advanced.tickCounts.lifebloom ?? 0) + + (advanced.tickCounts["wild-growth"] ?? 0); + if (verdancyTicks > 0) rpgSpellResources.verdancy = Math.min(5, rpgSpellResources.verdancy + verdancyTicks); + } else if (state.healerClassId === "druid" && advanced.tickCount > 0) { + healerMechanic.resource = Math.min(healerMechanic.maxResource, healerMechanic.resource + advanced.tickCount); } + if (next.reactiveHeal && next.reactiveHeal.expiresAt <= time) next = { ...next, reactiveHeal: null }; const activeDebuffs = next.debuffs .map((debuff) => { let updated = { ...debuff }; while (updated.nextTickAt <= time && updated.nextTickAt < updated.expiresAt) { - next = damageMemberAt(next, updated.tickDamage, state.partyPositions[next.id], barrier, updated.nextTickAt, partyCombat, state.partyPositions.brann, state.difficultyDamageMultiplier, state.gearModifiers, "direct", state.runModifiers.shieldDamageTakenMultiplier); + next = damageMemberAt(next, updated.tickDamage, state.partyPositions[next.id], barrier, updated.nextTickAt, partyCombat, state.partyPositions[partyCombat.tankAura.sourceId], state.difficultyDamageMultiplier, state.gearModifiers, "direct", state.runModifiers.shieldDamageTakenMultiplier, () => { reactiveResourceGains += 1; }); updated.nextTickAt += 1; } return updated; @@ -754,17 +1847,30 @@ export const useGameStore = create((set, get) => ({ return { ...next, debuffs: activeDebuffs }; }); - if (state.runModifiers.barrierHealingPerSecond > 0) { + if (barrier.kind && barrier.expiresAt > oldTime) { while (barrier.nextHealAt <= time && barrier.nextHealAt < barrier.expiresAt) { const pulseAt = barrier.nextHealAt; - const healing = state.runModifiers.barrierHealingPerSecond * state.healingMultiplier; - party = party.map((member) => barrierProtects(state.partyPositions[member.id], barrier, pulseAt) - ? healMember(member, healing) - : member); + if (barrier.kind === "spirit-link") { + party = equalizeHealthPercentages(party, state.partyPositions, barrier.center, BARRIER_RADIUS); + } + if (state.runModifiers.barrierHealingPerSecond > 0) { + const healing = state.runModifiers.barrierHealingPerSecond * healingMultiplier; + party = party.map((member) => distance(state.partyPositions[member.id], barrier.center) <= BARRIER_RADIUS + ? healMember(member, healing) + : member); + } barrier.nextHealAt += 1; } } + if (state.healerClassId === "druid" && healerMechanic.flourishExpiresAt > oldTime + && state.runModifiers.barrierHealingPerSecond > 0) { + while (healerMechanic.nextPulseAt <= time && healerMechanic.nextPulseAt < healerMechanic.flourishExpiresAt) { + party = party.map((member) => healMember(member, state.runModifiers.barrierHealingPerSecond * healingMultiplier)); + healerMechanic.nextPulseAt += 1; + } + } + const livingMotions = [ ...(boss.hp > 0 ? [bossMotion] : []), ...additionalBosses.filter((entry) => entry.boss.hp > 0).map((entry) => entry.motion), @@ -775,6 +1881,23 @@ export const useGameStore = create((set, get) => ({ orin: state.gearModifiers.orin.moveSpeed, vale: state.gearModifiers.vale.moveSpeed, }); + if (state.activityMode === "aether-assault") { + partyPositions = { + aelia: clampToHockeyArena(partyPositions.aelia, 0.65), + brann: clampToHockeyEnemyHalf(partyPositions.brann, 0.55), + nia: clampToHockeyEnemyHalf(partyPositions.nia, 0.55), + orin: clampToHockeyEnemyHalf(partyPositions.orin, 0.55), + vale: clampToHockeyEnemyHalf(partyPositions.vale, 0.55), + }; + } else if (state.activityMode === "hockey-healing" || state.activityMode === "hockey-healing-pvp" || state.activityMode === "blockbreaker") { + partyPositions = { + aelia: clampToHockeyHealerHalf(partyPositions.aelia, 0.65), + brann: clampToHockeyEnemyHalf(partyPositions.brann, 0.55), + nia: clampToHockeyEnemyHalf(partyPositions.nia, 0.55), + orin: clampToHockeyEnemyHalf(partyPositions.orin, 0.55), + vale: clampToHockeyEnemyHalf(partyPositions.vale, 0.55), + }; + } const encounterBosses: AdditionalBossState[] = [ { instanceId: bossInstanceId, boss, motion: bossMotion }, @@ -785,13 +1908,20 @@ export const useGameStore = create((set, get) => ({ if (encounterBoss.boss.hp <= 0) continue; const partyBeforeMechanic = party; const mechanicResult = advanceBossMechanics({ + arenaLayout: state.activityMode === "aether-assault" + ? "aether-assault" + : state.activityMode === "hockey-healing" || state.activityMode === "blockbreaker" + ? "hockey-healing" + : state.activityMode === "hockey-healing-pvp" + ? "hockey-healing-pvp" + : "standard", boss: encounterBoss.boss, motion: encounterBoss.motion, party, partyPositions, time, delta: time - oldTime, - damageMember: (member, amount, position, at, kind) => damageMemberAt(member, amount, position, barrier, at, partyCombat, partyPositions.brann, state.difficultyDamageMultiplier, state.gearModifiers, kind, state.runModifiers.shieldDamageTakenMultiplier), + damageMember: (member, amount, position, at, kind) => damageMemberAt(member, amount, position, barrier, at, partyCombat, partyPositions[partyCombat.tankAura.sourceId], state.difficultyDamageMultiplier, state.gearModifiers, kind, state.runModifiers.shieldDamageTakenMultiplier, () => { reactiveResourceGains += 1; }), }); encounterBosses[index] = { ...encounterBoss, boss: mechanicResult.boss, motion: constrainBossMotion(mechanicResult.motion) }; party = mechanicResult.party.map((member) => { @@ -819,12 +1949,17 @@ export const useGameStore = create((set, get) => ({ targets: encounterBosses, upcomingMechanicRemaining: mechanicRemaining.length ? Math.min(...mechanicRemaining) : Number.POSITIVE_INFINITY, gearModifiers: aiCombatModifiers(state.gearModifiers), + damageProfiles: state.rpgDamageProfiles ?? undefined, }); partyCombat = partyCombatResult.state; partyDamageEvents = [...partyCombatResult.events].reverse().concat(partyDamageEvents).slice(0, 24); for (const event of partyCombatResult.events) { const target = encounterBosses.find((entry) => entry.instanceId === event.targetInstanceId); if (target) target.boss.hp = Math.max(0, target.boss.hp - event.amount); + if (healerMechanic.avengingCrusaderExpiresAt > event.at) { + const crusaderHeal = healMostInjured(party, event.amount * 0.2 * healingMultiplier); + party = crusaderHeal.party; + } } for (const entry of encounterBosses) { if (entry.boss.hp <= 0 && entry.boss.defeatedAt === undefined) entry.boss.defeatedAt = time; @@ -833,9 +1968,115 @@ export const useGameStore = create((set, get) => ({ bossInstanceId = encounterBosses[0].instanceId; bossMotion = encounterBosses[0].motion; additionalBosses = encounterBosses.slice(1); + + if (state.activityMode === "hockey-healing-pvp" && hockeyPvp.role === "cpu") { + while (hockeyPvp.nextCpuHealAt <= time) { + const living = hockeyPvpOpponent.party.filter((member) => member.hp > 0); + const lowest = living.reduce((current, member) => + !current || member.hp / member.maxHp < current.hp / current.maxHp ? member : current, null); + if (lowest) { + const averageHealth = living.reduce((total, member) => total + member.hp / member.maxHp, 0) / Math.max(1, living.length); + hockeyPvpOpponent.party = hockeyPvpOpponent.party.map((member) => member.hp <= 0 + ? member + : member.id === lowest.id + ? healMember(member, (averageHealth < 0.55 ? 42 : 32) * healingMultiplier) + : averageHealth < 0.55 + ? healMember(member, 12 * healingMultiplier) + : member); + } + hockeyPvp.nextCpuHealAt += 1.1; + } + + hockeyPvpOpponent.partyPositions = updatePartyPositions( + hockeyPvpOpponent.partyPositions, + hockeyPvpOpponent.boss.hp > 0 ? [hockeyPvpOpponent.bossMotion] : [], + hockeyPvpOpponent.party, + time, + time - oldTime, + undefined, + { + brann: state.gearModifiers.brann.moveSpeed, + nia: state.gearModifiers.nia.moveSpeed, + orin: state.gearModifiers.orin.moveSpeed, + vale: state.gearModifiers.vale.moveSpeed, + }, + ); + hockeyPvpOpponent.partyPositions = { + aelia: clampToHockeyHealerHalf(hockeyPvp.opponentPlayerPosition, 0.65), + brann: clampToHockeyEnemyHalf(hockeyPvpOpponent.partyPositions.brann, 0.55), + nia: clampToHockeyEnemyHalf(hockeyPvpOpponent.partyPositions.nia, 0.55), + orin: clampToHockeyEnemyHalf(hockeyPvpOpponent.partyPositions.orin, 0.55), + vale: clampToHockeyEnemyHalf(hockeyPvpOpponent.partyPositions.vale, 0.55), + }; + + if (hockeyPvpOpponent.boss.hp > 0) { + const opponentBeforeMechanic = hockeyPvpOpponent.party; + const opponentMechanic = advanceBossMechanics({ + arenaLayout: "hockey-healing-pvp", + boss: hockeyPvpOpponent.boss, + motion: hockeyPvpOpponent.bossMotion, + party: hockeyPvpOpponent.party, + partyPositions: hockeyPvpOpponent.partyPositions, + time, + delta: time - oldTime, + damageMember: (member, amount, position, at, kind) => damageMemberAt( + member, + amount, + position, + { kind: null, center: [0, 0], expiresAt: 0, nextHealAt: 0 }, + at, + hockeyPvpOpponent.partyCombat, + hockeyPvpOpponent.partyPositions[hockeyPvpOpponent.partyCombat.tankAura.sourceId], + state.difficultyDamageMultiplier, + state.gearModifiers, + kind, + state.runModifiers.shieldDamageTakenMultiplier, + ), + }); + hockeyPvpOpponent.boss = opponentMechanic.boss; + hockeyPvpOpponent.bossMotion = constrainBossMotion(opponentMechanic.motion); + hockeyPvpOpponent.party = opponentMechanic.party.map((member) => { + const previous = opponentBeforeMechanic.find((candidate) => candidate.id === member.id); + if (!previous || member.knockedUntil <= previous.knockedUntil || member.knockedUntil <= time) return member; + return { ...member, knockedUntil: time + (member.knockedUntil - time) * state.gearModifiers[member.id].stunDuration }; + }); + + const opponentCombat = advancePartyCombat(hockeyPvpOpponent.partyCombat, { + oldTime, + time, + party: hockeyPvpOpponent.party, + oldPositions: state.hockeyPvpOpponent.partyPositions, + positions: hockeyPvpOpponent.partyPositions, + targets: [{ instanceId: `hockey-pvp-opponent-${hockeyPvp.opponentBossKills}`, boss: hockeyPvpOpponent.boss, motion: hockeyPvpOpponent.bossMotion }], + upcomingMechanicRemaining: upcomingMechanic(hockeyPvpOpponent.boss, hockeyPvpOpponent.bossMotion, time).remaining, + gearModifiers: aiCombatModifiers(state.gearModifiers), + }); + hockeyPvpOpponent.partyCombat = opponentCombat.state; + for (const event of opponentCombat.events) { + hockeyPvpOpponent.boss.hp = Math.max(0, hockeyPvpOpponent.boss.hp - event.amount); + } + if (hockeyPvpOpponent.boss.hp <= 0 && state.hockeyPvpOpponent.boss.hp > 0) { + hockeyPvpOpponent.boss.defeatedAt = time; + hockeyPvp.opponentBossKills += 1; + } + } + } + const healer = party.find((member) => member.id === "aelia")!; - if (healer.hp <= 0) activeCast = null; + const healerDefeated = healer.hp <= 0; + if (healerDefeated) activeCast = null; const partyWiped = isPartyWiped(party); + const allCompanionsDefeated = areAllNonHealerAlliesDefeated(party); + const blockbreakerAlliesDefeated = state.activityMode === "blockbreaker" && allCompanionsDefeated; + if (blockbreakerAlliesDefeated && blockbreaker.status !== "lost") { + blockbreaker = { + ...blockbreaker, + status: "lost", + puckVelocity: [0, 0], + reServeAt: null, + lostAt: time, + }; + } let phase: GamePhase = state.phase; let runBuffInputUnlockAt = state.runBuffInputUnlockAt; let newlyDefeatedBossCount = 0; @@ -847,13 +2088,129 @@ export const useGameStore = create((set, get) => ({ : state.additionalBosses[index - 1]; if (current.boss.hp > 0 || previous?.instanceId !== current.instanceId || previous.boss.hp <= 0) continue; newlyDefeatedBossCount += 1; - combatLog = addLog(combatLog, time, `${current.boss.name} falls. Endless kill ${endlessBossKills + newlyDefeatedBossCount}.`, "good"); + const killLabel = state.activityMode === "hockey-healing-pvp" + ? "PVP boss kill" + : state.activityMode === "hockey-healing" + ? "Hockey kill" + : state.activityMode === "blockbreaker" + ? "Blockbreaker kill" + : state.activityMode === "aether-assault" + ? "Aether Assault kill" + : "Endless kill"; + combatLog = addLog(combatLog, time, `${current.boss.name} falls. ${killLabel} ${endlessBossKills + newlyDefeatedBossCount}.`, "good"); } } endlessBossKills += newlyDefeatedBossCount; - if (state.endlessMode && partyWiped) { + const hockeyLost = state.activityMode === "hockey-healing" && hockey.status === "lost"; + const blockbreakerLost = state.activityMode === "blockbreaker" && blockbreaker.status === "lost"; + const pvpMode = state.activityMode === "hockey-healing-pvp"; + const opponentWiped = pvpMode && isPartyWiped(hockeyPvpOpponent.party); + const rpgChallengeActive = rpgRun?.phase === "challenge-active"; + const rpgBossActive = rpgRun?.phase === "boss-combat"; + if (rpgChallengeActive && rpgRun) { + const metrics = rpgChallengeMetrics(state.activityMode, blockbreaker, endlessBossKills, aetherAssault); + const previousMetrics = rpgRun.currentChallenge?.metrics; + if (!previousMetrics + || previousMetrics.bricksBroken !== metrics.bricksBroken + || previousMetrics.bossKills !== metrics.bossKills + || previousMetrics.kills !== metrics.kills) { + rpgRun = reduceRpgRoguelikeRun(rpgRun, { type: "challenge-progress", metrics }); + } + const objective = rpgRun.currentChallenge?.objective; + const succeeded = Boolean(objective && metrics[objective.metric] >= objective.target); + const failed = healerDefeated || allCompanionsDefeated || partyWiped || hockeyLost || blockbreakerLost; + if (succeeded || failed) { + rpgRun = reduceRpgRoguelikeRun(rpgRun, { type: "challenge-complete", metrics, forcedFailure: failed }); + const cleared = rpgRun.lastChallengeResult?.succeeded ?? false; + phase = gamePhaseForRpgRun(rpgRun); + activityMode = "boss"; + endlessMode = false; + rpgFocusId = normalizeRpgFocusId(rpgRun, null); + activeCast = null; + combatLog = addLog( + combatLog, + time, + cleared ? "Hallway challenge cleared. Bonus chest quality secured." : "Hallway challenge failed. Boss route remains open.", + cleared ? "good" : "danger", + ); + } else { + phase = "combat"; + } + } else if (rpgBossActive && rpgRun) { + if (healerDefeated) { + const vitals = extractRpgPartyVitals(party, assignRosterToCombatSlots(rpgRun.roster)); + rpgRun = reduceRpgRoguelikeRun(rpgRun, { + type: "boss-lost", + vitals, + playerHp: 0, + }); + phase = "defeat"; + endlessMode = false; + rpgFocusId = normalizeRpgFocusId(rpgRun, null); + combatLog = addLog(combatLog, time, "The healer falls. The expedition ends.", "danger"); + } else if (allCompanionsDefeated) { + const vitals = extractRpgPartyVitals(party, assignRosterToCombatSlots(rpgRun.roster)); + rpgRun = reduceRpgRoguelikeRun(rpgRun, { + type: "boss-lost", + vitals, + playerHp: extractRpgPlayerVital(party), + }); + phase = "defeat"; + endlessMode = false; + rpgFocusId = normalizeRpgFocusId(rpgRun, null); + combatLog = addLog(combatLog, time, "Every companion falls. The expedition ends.", "danger"); + } else if (encounterBosses.every((entry) => entry.boss.hp <= 0)) { + const vitals = extractRpgPartyVitals(party, assignRosterToCombatSlots(rpgRun.roster)); + rpgRun = reduceRpgRoguelikeRun(rpgRun, { + type: "boss-won", + vitals, + playerHp: extractRpgPlayerVital(party), + }); + phase = "combat"; + endlessMode = false; + rpgFocusId = normalizeRpgFocusId(rpgRun, null); + combatLog = addLog(combatLog, time, `${boss.name} falls. North door unlocked.`, "good"); + } else if (partyWiped) { + const vitals = extractRpgPartyVitals(party, assignRosterToCombatSlots(rpgRun.roster)); + rpgRun = reduceRpgRoguelikeRun(rpgRun, { + type: "boss-lost", + vitals, + playerHp: extractRpgPlayerVital(party), + }); + phase = "defeat"; + endlessMode = false; + rpgFocusId = normalizeRpgFocusId(rpgRun, null); + combatLog = addLog(combatLog, time, "Boss room claims the expedition.", "danger"); + } + } else if (rpgRun?.phase === "boss-cleared") { + phase = "combat"; + endlessMode = false; + } else if (pvpMode) { + if (partyWiped) { + phase = "defeat"; + hockeyPvp.status = "lost"; + combatLog = addLog(combatLog, time, `${hockeyPvp.opponentName} wins the rally.`, "danger"); + } else if (opponentWiped) { + phase = "victory"; + hockeyPvp.status = "won"; + combatLog = addLog(combatLog, time, `${hockeyPvp.opponentName}'s party falls. PVP victory.`, "good"); + } else { + phase = "combat"; + } + } else if (state.endlessMode && (partyWiped || hockeyLost || blockbreakerLost)) { phase = "defeat"; - combatLog = addLog(combatLog, time, `${endlessBossKills} endless bosses defeated before the party fell.`, "danger"); + combatLog = addLog( + combatLog, + time, + hockeyLost + ? `Goal breached after ${hockey.returns} puck returns and ${endlessBossKills} boss kills.` + : blockbreakerAlliesDefeated + ? `All four allies fell after ${blockbreaker.bricksBroken} bricks and ${blockbreaker.score} points.` + : state.activityMode === "aether-assault" + ? `Party fell during wave ${aetherAssault.wave} with ${aetherAssault.score} Aether Assault points.` + : `${endlessBossKills} bosses defeated before the party fell.`, + "danger", + ); } else if (state.endlessMode) { phase = "combat"; } else if (encounterBosses.every((entry) => entry.boss.hp <= 0)) { @@ -866,6 +2223,12 @@ export const useGameStore = create((set, get) => ({ combatLog = addLog(combatLog, time, `The party falls. ${boss.name} claims the vault.`, "danger"); } + if (rpgRun && reactiveResourceGains > 0) { + rpgSpellResources.tidalSurge = Math.min(2, rpgSpellResources.tidalSurge + reactiveResourceGains); + } else if (state.healerClassId === "shaman" && reactiveResourceGains > 0) { + healerMechanic.resource = Math.min(healerMechanic.maxResource, healerMechanic.resource + reactiveResourceGains); + } + set({ time, party, @@ -878,14 +2241,26 @@ export const useGameStore = create((set, get) => ({ partyPositions, bossMotion, phase, + activityMode, + endlessMode, + rpgRun, + rpgFocusId, + activeTab: pvpMode && (phase === "victory" || phase === "defeat") ? "combat" : state.activeTab, endlessBossKills, endlessSpawnSequence, + hockey, + blockbreaker, + aetherAssault, + hockeyPvp, + hockeyPvpOpponent, runBuffInputUnlockAt, mana: Math.min(state.maxMana, state.mana + 3.2 * (time - oldTime)), activeCast, combatLog, scenePulse: pulse, barrier, + healerMechanic, + rpgSpellResources, }); }, })); @@ -901,6 +2276,8 @@ export type GameSnapshot = Omit; export function getGameSnapshot(): GameSnapshot { @@ -923,6 +2305,8 @@ export function getGameSnapshot(): GameSnapshot { setActiveTab: _setActiveTab, selectItem: _selectItem, setPlayerPosition: _setPlayerPosition, + setHockeyAimDirection: _setHockeyAimDirection, + applyHockeyPvpRemoteSnapshot: _applyHockeyPvpRemoteSnapshot, setPaused: _setPaused, togglePause: _togglePause, setPauseSelection: _setPauseSelection, @@ -931,13 +2315,47 @@ export function getGameSnapshot(): GameSnapshot { continueRoguelikeRound: _continueRoguelikeRound, startRogueTrialsEndless: _startRogueTrialsEndless, setEndlessChoiceSelection: _setEndlessChoiceSelection, + setAbilityLoadout: _setAbilityLoadout, + dispatchRpgAction: _dispatchRpgAction, + setRpgFocusId: _setRpgFocusId, + cycleRpgFocus: _cycleRpgFocus, + moveRpgFocus: _moveRpgFocus, ...snapshot } = useGameStore.getState(); return snapshot; } -export function abilityRemaining(abilityId: AbilityId, time: number, cooldowns: Record) { - return Math.max(0, cooldowns[abilityId] - time); +export function getHockeyPvpNetworkSnapshot(): HockeyPvpRemoteSnapshot | null { + const state = useGameStore.getState(); + if (state.runMode !== "hockey-healing-pvp" || state.hockeyPvp.role === "cpu") return null; + const puck = state.hockeyPvp.role === "host" ? { + puckPosition: [...state.hockeyPvp.puckPosition] as WorldPosition, + puckVelocity: [...state.hockeyPvp.puckVelocity] as WorldPosition, + localReturns: state.hockeyPvp.localReturns, + opponentReturns: state.hockeyPvp.opponentReturns, + localGoalsConceded: state.hockeyPvp.localGoalsConceded, + opponentGoalsConceded: state.hockeyPvp.opponentGoalsConceded, + goalSequence: state.hockeyPvp.goalSequence, + lastGoalSide: state.hockeyPvp.lastGoalSide, + serveIndex: state.hockeyPvp.serveIndex, + } : undefined; + return { + sequence: Date.now(), + time: state.time, + party: structuredClone(state.party), + partyPositions: structuredClone(state.partyPositions), + boss: { id: state.boss.id, name: state.boss.name, hp: state.boss.hp, maxHp: state.boss.maxHp }, + bossPosition: [...state.bossMotion.position], + bossMode: state.bossMotion.mode, + bossKills: state.endlessBossKills, + playerPosition: [...state.partyPositions.aelia], + aimDirection: [...state.hockeyPvp.aimDirection], + puck, + }; +} + +export function abilityRemaining(abilitySlotId: AbilitySlotId, time: number, cooldowns: Record) { + return Math.max(0, cooldowns[abilitySlotId] - time); } export function isRunBuffInputLocked( @@ -954,5 +2372,8 @@ export function upcomingEncounterMechanic(state: Pick 0 ? [{ boss: state.boss, motion: state.bossMotion }] : []), ...state.additionalBosses.filter((entry) => entry.boss.hp > 0), ].map((entry) => upcomingMechanic(entry.boss, entry.motion, state.time)); + if (!candidates.length) { + return { name: "Replacement incoming", remaining: 0, cycle: 1, urgent: false }; + } return candidates.reduce((next, candidate) => candidate.remaining < next.remaining ? candidate : next, candidates[0]); } diff --git a/src/game/types.ts b/src/game/types.ts index 585d149..4239117 100644 --- a/src/game/types.ts +++ b/src/game/types.ts @@ -1,5 +1,37 @@ export type MemberId = "aelia" | "brann" | "nia" | "orin" | "vale"; -export type AbilityId = "mend" | "renew" | "shield" | "purify" | "radiance" | "barrier"; +export type AbilitySlotId = "ability1" | "ability2" | "ability3" | "ability4" | "ability5" | "ability6"; +export type HealerAbilityId = + | "priest-mend" + | "priest-renew" + | "priest-aegis-shield" + | "priest-purify" + | "priest-radiance" + | "priest-barrier" + | "druid-regrowth" + | "druid-rejuvenation" + | "druid-lifebloom" + | "druid-natures-cure" + | "druid-wild-growth" + | "druid-flourish" + | "shaman-healing-wave" + | "shaman-riptide" + | "shaman-earth-shield" + | "shaman-cleanse-spirit" + | "shaman-chain-heal" + | "shaman-spirit-link" + | "paladin-holy-light" + | "paladin-crusader-strike" + | "paladin-beacon-of-light" + | "paladin-cleanse-light" + | "paladin-word-of-glory" + | "paladin-avenging-crusader" + | "chronomancer-mend-timeline" + | "chronomancer-time-anchor" + | "chronomancer-echo-of-tomorrow" + | "chronomancer-erase-affliction" + | "chronomancer-accelerate" + | "chronomancer-time-loop"; +export type AbilityLoadout = Partial>; export type BossId = | "bulldrome" | "sandglass-scorpion" @@ -62,7 +94,8 @@ export type BossMechanicId = | "soul-siphon"; export type BossAnimationCue = "idle" | "move" | "attack" | "special"; export type GamePhase = "briefing" | "combat" | "intermission" | "victory" | "defeat"; -export type RunMode = "encounter" | "roguelike" | "rogue-trials"; +export type RunMode = "encounter" | "roguelike" | "rpg-roguelike" | "rogue-trials" | "hockey-healing" | "hockey-healing-pvp" | "blockbreaker" | "aether-assault"; +export type GameplayActivity = "boss" | "hockey-healing" | "hockey-healing-pvp" | "blockbreaker" | "aether-assault"; export type RunBuffId = | "mend-echo" | "mend-efficiency" @@ -83,8 +116,9 @@ export type RunBuffId = | "barrier-duration" | "barrier-regen"; export type RunBuffRanks = Partial>; -export type BottomTab = "combat" | "map" | "pack"; -export type PulseKind = AbilityId | "boss" | "debuff" | "charge" | "pounce" | "tether" | "venom" | "breath" | "skyfall" | "slash"; +export type BottomTab = "combat" | "map" | "pack" | "pvp"; +export type HealerPulseKind = "direct-heal" | "periodic-heal" | "protective" | "cleanse" | "group-heal" | "field"; +export type PulseKind = HealerPulseKind | "boss" | "debuff" | "charge" | "pounce" | "tether" | "venom" | "breath" | "skyfall" | "slash"; export type BossMotionMode = | "holding" | "telegraph" @@ -202,6 +236,26 @@ export interface Debuff { sourceBossId?: BossId; } +export type HealingEffectId = "renew" | "regrowth" | "rejuvenation" | "lifebloom" | "wild-growth" | "riptide" | "temporal-echo"; + +export interface HealingOverTimeEffect { + id: HealingEffectId; + expiresAt: number; + nextTickAt: number; + tickInterval: number; + healingPerTick: number; + stacks: number; + expirationHealingPerStack?: number; +} + +export interface ReactiveHealEffect { + id: "earth-shield"; + charges: number; + expiresAt: number; + nextTriggerAt: number; + healingPerTrigger: number; +} + export interface PartyMember { id: MemberId; name: string; @@ -211,12 +265,23 @@ export interface PartyMember { maxHp: number; hp: number; absorb: number; - renewExpiresAt: number; - renewNextTickAt: number; + healingEffects: HealingOverTimeEffect[]; + reactiveHeal: ReactiveHealEffect | null; knockedUntil: number; debuffs: Debuff[]; + /** Optional serializable projection supplied by modular run modes. */ + runProfile?: { + instanceId: string; + combatProfileId: string; + combatKitId: PartyCombatKitId; + tier: string; + visualArchetype: "knight" | "ranger" | "mage" | "rogue" | "druid"; + }; } +/** Renderer-independent companion behavior family; run modes may map any archetype to one. */ +export type PartyCombatKitId = "tank" | "ranged" | "caster" | "melee"; + export interface BossState { id: BossId; name: string; @@ -244,6 +309,8 @@ export interface BossMotionState { nextMechanicAt: number; mechanicCount: number; phaseStartedAt: number; + /** Simulation time of latest basic melee impact, or -1 before first impact. */ + lastMeleeAt: number; mechanicHitIds: MemberId[]; mechanicNextDamageAt: Partial>; tetherIds: MemberId[]; @@ -258,7 +325,9 @@ export interface BossMotionState { } export interface AbilityDefinition { - id: AbilityId; + id: HealerAbilityId; + slot: AbilitySlotId; + pulseKind: HealerPulseKind; name: string; shortName: string; key: string; @@ -273,18 +342,40 @@ export interface AbilityDefinition { } export interface ActiveCast { - abilityId: "mend"; + slotId: AbilitySlotId; + abilityId: HealerAbilityId; targetId: MemberId; startedAt: number; completesAt: number; + resourceSpent: number; } export interface BarrierState { + kind: "barrier" | "spirit-link" | null; center: WorldPosition; expiresAt: number; nextHealAt: number; } +export interface HealerMechanicState { + resource: number; + maxResource: number; + flourishExpiresAt: number; + nextPulseAt: number; + beaconTargetId: MemberId | null; + beaconExpiresAt: number; + avengingCrusaderExpiresAt: number; + temporalAnchor: { + targetId: MemberId; + hp: number; + expiresAt: number; + } | null; + timeLoop: { + restoresAt: number; + partyHp: Record; + } | null; +} + export interface ScenePulse { id: number; kind: PulseKind; @@ -302,7 +393,7 @@ export interface InventoryItem { equipped: boolean; } -export type HealerClassId = "priest" | "druid" | "shaman"; +export type HealerClassId = "priest" | "druid" | "shaman" | "paladin" | "chronomancer"; export interface HealerClassDefinition { id: HealerClassId; @@ -311,6 +402,7 @@ export interface HealerClassDefinition { icon: string; color: string; resourceName: string; + secondaryResourceName?: string; description: string; - abilities: Record; + abilities: Record; } diff --git a/src/game/useGameLoop.ts b/src/game/useGameLoop.ts index 253ade2..baa4237 100644 --- a/src/game/useGameLoop.ts +++ b/src/game/useGameLoop.ts @@ -3,6 +3,8 @@ import { subscribeControllerToken } from "../input/controller"; import { ABILITY_ORDER } from "./data"; import { isRunBuffInputLocked, useGameStore } from "./store"; import { ABILITY_BY_CONTROLLER_BUTTON } from "./controllerBindings"; +import { cycleBottomTab } from "./bottomTabs"; +import { resolveRpgFocusCommand } from "./rpgRoguelike/uiModel"; function cycleRunBuff(direction: 1 | -1) { const store = useGameStore.getState(); @@ -12,6 +14,21 @@ function cycleRunBuff(direction: 1 | -1) { store.setSelectedRunBuff(store.draftBuffIds[nextIndex]); } +function rpgInputIsGated() { + const run = useGameStore.getState().rpgRun; + return Boolean(run && run.phase !== "challenge-active" && run.phase !== "boss-combat"); +} + +function activateRpgFocus(onExit?: () => void) { + const store = useGameStore.getState(); + if (!store.rpgRun) return; + const command = resolveRpgFocusCommand(store.rpgRun, store.rpgFocusId); + if (!command) return; + if (command.type === "run-action") store.dispatchRpgAction(command.action); + else if (command.type === "restart-run") store.restart(); + else onExit?.(); +} + export function useActionBindings(enabled = true, onExit?: () => void) { const exitRef = useRef(onExit); exitRef.current = onExit; @@ -32,6 +49,16 @@ export function useActionBindings(enabled = true, onExit?: () => void) { } return; } + if (store.runMode === "rpg-roguelike" && store.rpgRun && rpgInputIsGated()) { + if (["arrowleft", "arrowup", "arrowright", "arrowdown", "enter", "escape"].includes(key)) event.preventDefault(); + if (key === "arrowleft") store.moveRpgFocus("left"); + if (key === "arrowright") store.moveRpgFocus("right"); + if (key === "arrowup") store.moveRpgFocus("up"); + if (key === "arrowdown") store.moveRpgFocus("down"); + if (key === "enter") activateRpgFocus(exitRef.current); + if (key === "escape") exitRef.current?.(); + return; + } if (store.phase === "intermission") { if (["arrowleft", "arrowup", "arrowright", "arrowdown", "enter"].includes(key)) event.preventDefault(); if (isRunBuffInputLocked(store)) return; @@ -71,7 +98,14 @@ export function useActionBindings(enabled = true, onExit?: () => void) { store.setActiveTab(store.activeTab === "map" ? "combat" : "map"); break; case "i": - store.setActiveTab(store.activeTab === "pack" ? "combat" : "pack"); + if (store.runMode !== "hockey-healing-pvp") { + store.setActiveTab(store.activeTab === "pack" ? "combat" : "pack"); + } + break; + case "p": + if (store.runMode === "hockey-healing-pvp") { + store.setActiveTab(store.activeTab === "pvp" ? "combat" : "pvp"); + } break; case "enter": if (store.phase === "briefing") store.startEncounter(); @@ -101,6 +135,15 @@ export function useActionBindings(enabled = true, onExit?: () => void) { } return; } + if (store.runMode === "rpg-roguelike" && store.rpgRun && rpgInputIsGated()) { + if (token === "Button12" || token === "Axis1-") store.moveRpgFocus("up"); + if (token === "Button13" || token === "Axis1+") store.moveRpgFocus("down"); + if (token === "Button14" || token === "Axis0-") store.moveRpgFocus("left"); + if (token === "Button15" || token === "Axis0+") store.moveRpgFocus("right"); + if (!repeat && token === "Button0") activateRpgFocus(exitRef.current); + if (!repeat && token === "Button1") exitRef.current?.(); + return; + } if (store.phase === "intermission") { if (isRunBuffInputLocked(store)) return; if (["Button12", "Button14", "Axis0-", "Axis1-"].includes(token)) cycleRunBuff(-1); @@ -129,7 +172,7 @@ export function useActionBindings(enabled = true, onExit?: () => void) { } if (token === "Button12") store.cycleMember(-1); if (token === "Button13") store.cycleMember(1); - if (token === "Button8") store.setActiveTab(store.activeTab === "map" ? "combat" : "map"); + if (token === "Button8") store.setActiveTab(cycleBottomTab(store.activeTab, store.runMode)); if (token === "Button9" || (token === "Button0" && store.phase !== "combat")) { if (store.phase === "briefing") store.startEncounter(); else if (store.phase === "victory" || store.phase === "defeat") store.restart(); diff --git a/src/game/weaponCatalog.test.ts b/src/game/weaponCatalog.test.ts new file mode 100644 index 0000000..848abb3 --- /dev/null +++ b/src/game/weaponCatalog.test.ts @@ -0,0 +1,198 @@ +import { readdirSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { getBounds, NodeIO } from "@gltf-transform/core"; +import { ALL_EXTENSIONS } from "@gltf-transform/extensions"; +import { MeshoptDecoder } from "meshoptimizer"; +import { describe, expect, it } from "vitest"; +import { + CHARACTER_BACK_PROP_MODEL_IDS, + CHARACTER_WEAPON_MODEL_IDS, + CLAUDECRAFT_WEAPON_CATALOG, + LEGACY_WEAPON_ID_ALIASES, + TWO_HANDED_WEAPON_IDS, + heldItemForModel, + isWeaponCatalogId, + preferredWeaponAssetFile, + resolveCharacterWeaponModelId, + resolveWeaponCatalogId, + weaponDefinition, + weaponDefinitionsForSlot, + weaponIdsForCategory, + weaponIdsForGrip, + weaponIdsForSlot, + weaponSupportsSlot, + weaponUsesBothHands, +} from "./weaponCatalog"; + +const RUNTIME_DIRECTORY = fileURLToPath( + new URL("../assets/game/models/claudecraft/weapons", import.meta.url), +); + +function glbFiles(directory: string) { + return readdirSync(directory) + .filter((file) => file.endsWith(".glb")) + .sort(); +} + +describe("Claudecraft weapon catalog", () => { + it("catalogs every checked-in canonical asset exactly once", () => { + const ids = CLAUDECRAFT_WEAPON_CATALOG.map((entry) => entry.id); + const files = CLAUDECRAFT_WEAPON_CATALOG.map((entry) => entry.sourceFile); + const canonicalRuntimeFiles = glbFiles(RUNTIME_DIRECTORY) + .filter((file) => !file.endsWith("-uastc.glb")); + + expect(CLAUDECRAFT_WEAPON_CATALOG).toHaveLength(55); + expect(new Set(ids)).toHaveLength(55); + expect(new Set(files)).toHaveLength(55); + expect([...files].sort()).toEqual(canonicalRuntimeFiles); + }); + + it("references every optimized variant exactly once", () => { + const optimizedFiles = CLAUDECRAFT_WEAPON_CATALOG + .map((entry) => entry.optimizedFile) + .filter((file): file is NonNullable => file !== null) + .sort(); + const optimizedRuntimeFiles = glbFiles(RUNTIME_DIRECTORY) + .filter((file) => file.endsWith("-uastc.glb")); + + expect(optimizedFiles).toHaveLength(7); + expect(new Set(optimizedFiles)).toHaveLength(7); + expect(optimizedFiles).toEqual(optimizedRuntimeFiles); + }); + + it("has tracked runtime copies for every referenced source asset", () => { + const referencedFiles = CLAUDECRAFT_WEAPON_CATALOG + .flatMap((entry) => entry.optimizedFile + ? [entry.sourceFile, entry.optimizedFile] + : [entry.sourceFile]) + .sort(); + + expect(glbFiles(RUNTIME_DIRECTORY)).toEqual(referencedFiles); + }); + + it("keeps category coverage stable", () => { + expect(Object.fromEntries([ + "axe", + "crossbow", + "dagger", + "halberd", + "hammer", + "quiver", + "scythe", + "shield", + "spear", + "spellbook", + "staff", + "sword", + "wand", + ].map((category) => [category, weaponIdsForCategory(category as Parameters[0]).length]))).toEqual({ + axe: 9, + crossbow: 3, + dagger: 5, + halberd: 1, + hammer: 4, + quiver: 1, + scythe: 1, + shield: 4, + spear: 1, + spellbook: 1, + staff: 8, + sword: 13, + wand: 4, + }); + }); + + it("derives held items while keeping back-only props separate", () => { + expect(CHARACTER_WEAPON_MODEL_IDS).toHaveLength(54); + expect(CHARACTER_WEAPON_MODEL_IDS).not.toContain("cc/quiver"); + expect(CHARACTER_BACK_PROP_MODEL_IDS).toEqual(["cc/quiver"]); + expect(heldItemForModel("cc/adv_dagger")).toEqual({ + modelId: "cc/adv_dagger", + grip: "dagger", + }); + expect(weaponDefinitionsForSlot("back").map((entry) => entry.id)).toEqual(["cc/quiver"]); + }); + + it("identifies valid two-handed and long-weapon models", () => { + expect(TWO_HANDED_WEAPON_IDS).toHaveLength(18); + expect(new Set(TWO_HANDED_WEAPON_IDS)).toHaveLength(18); + expect(TWO_HANDED_WEAPON_IDS.every((id) => isWeaponCatalogId(id))).toBe(true); + expect(TWO_HANDED_WEAPON_IDS.every((id) => CHARACTER_WEAPON_MODEL_IDS.includes(id))).toBe(true); + expect(weaponUsesBothHands("cc/staff_d")).toBe(true); + expect(weaponUsesBothHands("cc/crossbow_1handed")).toBe(false); + expect(TWO_HANDED_WEAPON_IDS).not.toContain("cc/quiver"); + }); + + it("filters by slot, category, and attachment grip", () => { + expect(weaponSupportsSlot("cc/adv_sword_1handed", "main")).toBe(true); + expect(weaponSupportsSlot("cc/adv_sword_1handed", "off")).toBe(true); + expect(weaponSupportsSlot("cc/adv_sword_2handed", "off")).toBe(false); + expect(weaponIdsForSlot("off")).toContain("cc/shield_round"); + expect(weaponIdsForCategory("scythe")).toEqual(["cc/scythe"]); + expect(weaponIdsForGrip("polearm")).toEqual([ + "cc/halberd", + "cc/scythe", + "cc/spear_a", + ]); + expect(weaponDefinition("cc/wand_b").category).toBe("wand"); + }); + + it("resolves canonical and legacy save IDs without accepting prototype keys", () => { + expect(LEGACY_WEAPON_ID_ALIASES).toEqual({ + "druid-staff": "cc/adv_druid_staff", + sword: "cc/adv_sword_1handed", + shield: "cc/shield_badge", + wand: "cc/adv_wand", + spellbook: "cc/spellbook_open", + }); + expect(resolveWeaponCatalogId("cc/sword_g")).toBe("cc/sword_g"); + expect(resolveWeaponCatalogId("sword")).toBe("cc/adv_sword_1handed"); + expect(resolveCharacterWeaponModelId("spellbook")).toBe("cc/spellbook_open"); + expect(resolveCharacterWeaponModelId("cc/quiver")).toBeNull(); + expect(resolveWeaponCatalogId("toString")).toBeNull(); + expect(isWeaponCatalogId("__proto__")).toBe(false); + expect(resolveWeaponCatalogId(null)).toBeNull(); + }); + + it("prefers UASTC only when a catalog entry provides it", () => { + expect(preferredWeaponAssetFile("cc/adv_wand")).toBe("adv_wand-uastc.glb"); + expect(preferredWeaponAssetFile("cc/adv_wand", false)).toBe("adv_wand.glb"); + expect(preferredWeaponAssetFile("cc/wand_a")).toBe("wand_a.glb"); + }); + + it("keeps every runtime design finite, static, and inside the mobile geometry budget", async () => { + await MeshoptDecoder.ready; + const io = new NodeIO() + .registerExtensions(ALL_EXTENSIONS) + .registerDependencies({ "meshopt.decoder": MeshoptDecoder }); + + for (const definition of CLAUDECRAFT_WEAPON_CATALOG) { + const runtimeFiles = definition.optimizedFile + ? [definition.sourceFile, definition.optimizedFile] + : [definition.sourceFile]; + for (const runtimeFile of runtimeFiles) { + const contractLabel = `${definition.id} (${runtimeFile})`; + const document = await io.read(path.join(RUNTIME_DIRECTORY, runtimeFile)); + const root = document.getRoot(); + const scene = root.listScenes()[0]; + const bounds = getBounds(scene); + const values = [...bounds.min, ...bounds.max]; + const triangleCount = root.listMeshes().reduce((meshTotal, mesh) => meshTotal + + mesh.listPrimitives().reduce((primitiveTotal, primitive) => { + const elementCount = primitive.getIndices()?.getCount() + ?? primitive.getAttribute("POSITION")?.getCount() + ?? 0; + return primitiveTotal + Math.floor(elementCount / 3); + }, 0), 0); + + expect(values.every(Number.isFinite), contractLabel).toBe(true); + expect(bounds.max.some((value, index) => value > bounds.min[index]), contractLabel).toBe(true); + expect(triangleCount, contractLabel).toBeGreaterThan(0); + expect(triangleCount, contractLabel).toBeLessThanOrEqual(2_000); + expect(root.listAnimations(), contractLabel).toHaveLength(0); + expect(root.listSkins(), contractLabel).toHaveLength(0); + } + } + }); +}); diff --git a/src/game/weaponCatalog.ts b/src/game/weaponCatalog.ts new file mode 100644 index 0000000..b96542b --- /dev/null +++ b/src/game/weaponCatalog.ts @@ -0,0 +1,230 @@ +export const WEAPON_SLOTS = ["main", "off", "back"] as const; +export type WeaponSlot = typeof WEAPON_SLOTS[number]; + +export const WEAPON_CATEGORIES = [ + "axe", + "crossbow", + "dagger", + "halberd", + "hammer", + "quiver", + "scythe", + "shield", + "spear", + "spellbook", + "staff", + "sword", + "wand", +] as const; +export type WeaponCategory = typeof WEAPON_CATEGORIES[number]; + +/** Attachment transform profile. Gameplay weapon type remains `category`. */ +export const WEAPON_GRIPS = [ + "upright", + "dagger", + "staff", + "wand", + "crossbow", + "polearm", + "prop", + "back", +] as const; +export type WeaponGrip = typeof WEAPON_GRIPS[number]; + +export interface WeaponCatalogEntry { + id: `cc/${string}`; + label: string; + category: WeaponCategory; + allowedSlots: readonly WeaponSlot[]; + grip: WeaponGrip; + sourceFile: `${string}.glb`; + optimizedFile: `${string}-uastc.glb` | null; +} + +export const CLAUDECRAFT_WEAPON_CATALOG = [ + { id: "cc/adv_axe_1handed", label: "Adventurer One-Handed Axe", category: "axe", allowedSlots: ["main", "off"], grip: "upright", sourceFile: "adv_axe_1handed.glb", optimizedFile: null }, + { id: "cc/adv_axe_2handed", label: "Adventurer Two-Handed Axe", category: "axe", allowedSlots: ["main"], grip: "upright", sourceFile: "adv_axe_2handed.glb", optimizedFile: null }, + { id: "cc/adv_dagger", label: "Adventurer Dagger", category: "dagger", allowedSlots: ["main", "off"], grip: "dagger", sourceFile: "adv_dagger.glb", optimizedFile: "adv_dagger-uastc.glb" }, + { id: "cc/adv_druid_staff", label: "Druid Staff", category: "staff", allowedSlots: ["main"], grip: "staff", sourceFile: "adv_druid_staff.glb", optimizedFile: "adv_druid_staff-uastc.glb" }, + { id: "cc/adv_staff", label: "Adventurer Staff", category: "staff", allowedSlots: ["main"], grip: "staff", sourceFile: "adv_staff.glb", optimizedFile: null }, + { id: "cc/adv_sword_1handed", label: "Adventurer One-Handed Sword", category: "sword", allowedSlots: ["main", "off"], grip: "upright", sourceFile: "adv_sword_1handed.glb", optimizedFile: "adv_sword_1handed-uastc.glb" }, + { id: "cc/adv_sword_2handed", label: "Adventurer Two-Handed Sword", category: "sword", allowedSlots: ["main"], grip: "upright", sourceFile: "adv_sword_2handed.glb", optimizedFile: null }, + { id: "cc/adv_sword_2handed_color", label: "Adventurer Colored Greatsword", category: "sword", allowedSlots: ["main"], grip: "upright", sourceFile: "adv_sword_2handed_color.glb", optimizedFile: null }, + { id: "cc/adv_wand", label: "Adventurer Wand", category: "wand", allowedSlots: ["main"], grip: "wand", sourceFile: "adv_wand.glb", optimizedFile: "adv_wand-uastc.glb" }, + + { id: "cc/axe_1handed", label: "One-Handed Axe", category: "axe", allowedSlots: ["main", "off"], grip: "upright", sourceFile: "axe_1handed.glb", optimizedFile: null }, + { id: "cc/axe_2handed", label: "Two-Handed Axe", category: "axe", allowedSlots: ["main"], grip: "upright", sourceFile: "axe_2handed.glb", optimizedFile: null }, + { id: "cc/axe_a", label: "Axe A", category: "axe", allowedSlots: ["main"], grip: "upright", sourceFile: "axe_a.glb", optimizedFile: null }, + { id: "cc/axe_b", label: "Axe B", category: "axe", allowedSlots: ["main"], grip: "upright", sourceFile: "axe_b.glb", optimizedFile: null }, + { id: "cc/axe_c", label: "Axe C", category: "axe", allowedSlots: ["main"], grip: "upright", sourceFile: "axe_c.glb", optimizedFile: null }, + { id: "cc/axe_d", label: "Axe D", category: "axe", allowedSlots: ["main"], grip: "upright", sourceFile: "axe_d.glb", optimizedFile: null }, + + { id: "cc/crossbow_1handed", label: "One-Handed Crossbow", category: "crossbow", allowedSlots: ["main"], grip: "crossbow", sourceFile: "crossbow_1handed.glb", optimizedFile: null }, + { id: "cc/crossbow_2handed", label: "Two-Handed Crossbow", category: "crossbow", allowedSlots: ["main"], grip: "crossbow", sourceFile: "crossbow_2handed.glb", optimizedFile: "crossbow_2handed-uastc.glb" }, + + { id: "cc/dagger", label: "Dagger", category: "dagger", allowedSlots: ["main", "off"], grip: "dagger", sourceFile: "dagger.glb", optimizedFile: null }, + { id: "cc/dagger_a", label: "Dagger A", category: "dagger", allowedSlots: ["main", "off"], grip: "dagger", sourceFile: "dagger_a.glb", optimizedFile: null }, + { id: "cc/dagger_b", label: "Dagger B", category: "dagger", allowedSlots: ["main", "off"], grip: "dagger", sourceFile: "dagger_b.glb", optimizedFile: null }, + { id: "cc/dagger_c", label: "Dagger C", category: "dagger", allowedSlots: ["main", "off"], grip: "dagger", sourceFile: "dagger_c.glb", optimizedFile: null }, + + { id: "cc/halberd", label: "Halberd", category: "halberd", allowedSlots: ["main"], grip: "polearm", sourceFile: "halberd.glb", optimizedFile: null }, + + { id: "cc/hammer_a", label: "Hammer A", category: "hammer", allowedSlots: ["main"], grip: "upright", sourceFile: "hammer_a.glb", optimizedFile: null }, + { id: "cc/hammer_b", label: "Hammer B", category: "hammer", allowedSlots: ["main"], grip: "upright", sourceFile: "hammer_b.glb", optimizedFile: null }, + { id: "cc/hammer_c", label: "Hammer C", category: "hammer", allowedSlots: ["main"], grip: "upright", sourceFile: "hammer_c.glb", optimizedFile: null }, + { id: "cc/hammer_d", label: "Hammer D", category: "hammer", allowedSlots: ["main"], grip: "upright", sourceFile: "hammer_d.glb", optimizedFile: null }, + + { id: "cc/quiver", label: "Quiver", category: "quiver", allowedSlots: ["back"], grip: "back", sourceFile: "quiver.glb", optimizedFile: null }, + { id: "cc/scythe", label: "Scythe", category: "scythe", allowedSlots: ["main"], grip: "polearm", sourceFile: "scythe.glb", optimizedFile: null }, + + { id: "cc/shield_badge", label: "Badge Shield", category: "shield", allowedSlots: ["off"], grip: "prop", sourceFile: "shield_badge.glb", optimizedFile: "shield_badge-uastc.glb" }, + { id: "cc/shield_round", label: "Round Shield", category: "shield", allowedSlots: ["off"], grip: "prop", sourceFile: "shield_round.glb", optimizedFile: null }, + { id: "cc/shield_square", label: "Square Shield", category: "shield", allowedSlots: ["off"], grip: "prop", sourceFile: "shield_square.glb", optimizedFile: null }, + + { id: "cc/skeleton_axe", label: "Skeleton Axe", category: "axe", allowedSlots: ["main"], grip: "upright", sourceFile: "skeleton_axe.glb", optimizedFile: null }, + { id: "cc/skeleton_blade", label: "Skeleton Blade", category: "sword", allowedSlots: ["main"], grip: "upright", sourceFile: "skeleton_blade.glb", optimizedFile: null }, + { id: "cc/skeleton_crossbow", label: "Skeleton Crossbow", category: "crossbow", allowedSlots: ["main"], grip: "crossbow", sourceFile: "skeleton_crossbow.glb", optimizedFile: null }, + { id: "cc/skeleton_shield_large_a", label: "Skeleton Tower Shield", category: "shield", allowedSlots: ["off"], grip: "prop", sourceFile: "skeleton_shield_large_a.glb", optimizedFile: null }, + { id: "cc/skeleton_staff", label: "Skeleton Staff", category: "staff", allowedSlots: ["main"], grip: "staff", sourceFile: "skeleton_staff.glb", optimizedFile: null }, + + { id: "cc/spear_a", label: "Spear A", category: "spear", allowedSlots: ["main"], grip: "polearm", sourceFile: "spear_a.glb", optimizedFile: null }, + { id: "cc/spellbook_open", label: "Open Spellbook", category: "spellbook", allowedSlots: ["off"], grip: "prop", sourceFile: "spellbook_open.glb", optimizedFile: "spellbook_open-uastc.glb" }, + + { id: "cc/staff", label: "Staff", category: "staff", allowedSlots: ["main"], grip: "staff", sourceFile: "staff.glb", optimizedFile: null }, + { id: "cc/staff_a", label: "Staff A", category: "staff", allowedSlots: ["main"], grip: "staff", sourceFile: "staff_a.glb", optimizedFile: null }, + { id: "cc/staff_b", label: "Staff B", category: "staff", allowedSlots: ["main"], grip: "staff", sourceFile: "staff_b.glb", optimizedFile: null }, + { id: "cc/staff_c", label: "Staff C", category: "staff", allowedSlots: ["main"], grip: "staff", sourceFile: "staff_c.glb", optimizedFile: null }, + { id: "cc/staff_d", label: "Staff D", category: "staff", allowedSlots: ["main"], grip: "staff", sourceFile: "staff_d.glb", optimizedFile: null }, + + { id: "cc/sword_1handed", label: "One-Handed Sword", category: "sword", allowedSlots: ["main", "off"], grip: "upright", sourceFile: "sword_1handed.glb", optimizedFile: null }, + { id: "cc/sword_2handed", label: "Two-Handed Sword", category: "sword", allowedSlots: ["main"], grip: "upright", sourceFile: "sword_2handed.glb", optimizedFile: null }, + { id: "cc/sword_a", label: "Sword A", category: "sword", allowedSlots: ["main"], grip: "upright", sourceFile: "sword_a.glb", optimizedFile: null }, + { id: "cc/sword_b", label: "Sword B", category: "sword", allowedSlots: ["main"], grip: "upright", sourceFile: "sword_b.glb", optimizedFile: null }, + { id: "cc/sword_c", label: "Sword C", category: "sword", allowedSlots: ["main"], grip: "upright", sourceFile: "sword_c.glb", optimizedFile: null }, + { id: "cc/sword_d", label: "Sword D", category: "sword", allowedSlots: ["main"], grip: "upright", sourceFile: "sword_d.glb", optimizedFile: null }, + { id: "cc/sword_e", label: "Sword E", category: "sword", allowedSlots: ["main"], grip: "upright", sourceFile: "sword_e.glb", optimizedFile: null }, + { id: "cc/sword_f", label: "Sword F", category: "sword", allowedSlots: ["main"], grip: "upright", sourceFile: "sword_f.glb", optimizedFile: null }, + { id: "cc/sword_g", label: "Sword G", category: "sword", allowedSlots: ["main"], grip: "upright", sourceFile: "sword_g.glb", optimizedFile: null }, + + { id: "cc/wand", label: "Wand", category: "wand", allowedSlots: ["main"], grip: "wand", sourceFile: "wand.glb", optimizedFile: null }, + { id: "cc/wand_a", label: "Wand A", category: "wand", allowedSlots: ["main"], grip: "wand", sourceFile: "wand_a.glb", optimizedFile: null }, + { id: "cc/wand_b", label: "Wand B", category: "wand", allowedSlots: ["main"], grip: "wand", sourceFile: "wand_b.glb", optimizedFile: null }, +] as const satisfies readonly WeaponCatalogEntry[]; + +export type WeaponCatalogId = typeof CLAUDECRAFT_WEAPON_CATALOG[number]["id"]; +export type WeaponDefinition = typeof CLAUDECRAFT_WEAPON_CATALOG[number]; +export type LegacyWeaponModelId = keyof typeof LEGACY_WEAPON_ID_ALIASES; +export type CharacterBackPropModelId = "cc/quiver"; +export type CharacterWeaponModelId = Exclude; +export type CharacterWeaponGrip = Exclude; +export type CharacterWeaponCategory = Exclude; + +export interface CharacterHeldItemVisual { + modelId: CharacterWeaponModelId; + grip: CharacterWeaponGrip; +} + +const WEAPON_CATALOG_BY_ID = Object.fromEntries( + CLAUDECRAFT_WEAPON_CATALOG.map((entry) => [entry.id, entry]), +) as Record; + +export const LEGACY_WEAPON_ID_ALIASES = { + "druid-staff": "cc/adv_druid_staff", + sword: "cc/adv_sword_1handed", + shield: "cc/shield_badge", + wand: "cc/adv_wand", + spellbook: "cc/spellbook_open", +} as const satisfies Record; + +export const CHARACTER_BACK_PROP_MODEL_IDS = ["cc/quiver"] as const satisfies readonly CharacterBackPropModelId[]; + +export const CHARACTER_WEAPON_MODEL_IDS = CLAUDECRAFT_WEAPON_CATALOG + .filter((entry) => !(entry.allowedSlots as readonly WeaponSlot[]).includes("back")) + .map((entry) => entry.id) as CharacterWeaponModelId[]; + +export const TWO_HANDED_WEAPON_IDS = [ + "cc/adv_axe_2handed", + "cc/adv_druid_staff", + "cc/adv_staff", + "cc/adv_sword_2handed", + "cc/adv_sword_2handed_color", + "cc/axe_2handed", + "cc/crossbow_2handed", + "cc/halberd", + "cc/scythe", + "cc/skeleton_crossbow", + "cc/skeleton_staff", + "cc/spear_a", + "cc/staff", + "cc/staff_a", + "cc/staff_b", + "cc/staff_c", + "cc/staff_d", + "cc/sword_2handed", +] as const satisfies readonly CharacterWeaponModelId[]; + +const TWO_HANDED_WEAPON_ID_SET = new Set(TWO_HANDED_WEAPON_IDS); + +function owns(object: object, key: PropertyKey) { + return Object.prototype.hasOwnProperty.call(object, key); +} + +export function isWeaponCatalogId(value: unknown): value is WeaponCatalogId { + return typeof value === "string" && owns(WEAPON_CATALOG_BY_ID, value); +} + +export function resolveWeaponCatalogId(value: unknown): WeaponCatalogId | null { + if (isWeaponCatalogId(value)) return value; + if (typeof value !== "string" || !owns(LEGACY_WEAPON_ID_ALIASES, value)) return null; + return LEGACY_WEAPON_ID_ALIASES[value as LegacyWeaponModelId]; +} + +export function resolveCharacterWeaponModelId(value: unknown): CharacterWeaponModelId | null { + const id = resolveWeaponCatalogId(value); + return id && id !== "cc/quiver" ? id : null; +} + +export function weaponCatalogEntry(id: WeaponCatalogId) { + return WEAPON_CATALOG_BY_ID[id]; +} + +export const weaponDefinition = weaponCatalogEntry; + +export function weaponDefinitionsForSlot(slot: WeaponSlot): WeaponDefinition[] { + return CLAUDECRAFT_WEAPON_CATALOG + .filter((entry) => (entry.allowedSlots as readonly WeaponSlot[]).includes(slot)); +} + +export function heldItemForModel(modelId: CharacterWeaponModelId): CharacterHeldItemVisual { + const definition = weaponDefinition(modelId); + return { modelId, grip: definition.grip as CharacterWeaponGrip }; +} + +export function weaponUsesBothHands(modelId: CharacterWeaponModelId) { + return TWO_HANDED_WEAPON_ID_SET.has(modelId); +} + +export function weaponSupportsSlot(id: WeaponCatalogId, slot: WeaponSlot) { + return (weaponCatalogEntry(id).allowedSlots as readonly WeaponSlot[]).includes(slot); +} + +export function weaponIdsForSlot(slot: WeaponSlot): WeaponCatalogId[] { + return weaponDefinitionsForSlot(slot).map((entry) => entry.id); +} + +export function weaponIdsForCategory(category: WeaponCategory): WeaponCatalogId[] { + return CLAUDECRAFT_WEAPON_CATALOG + .filter((entry) => entry.category === category) + .map((entry) => entry.id); +} + +export function weaponIdsForGrip(grip: WeaponGrip): WeaponCatalogId[] { + return CLAUDECRAFT_WEAPON_CATALOG + .filter((entry) => entry.grip === grip) + .map((entry) => entry.id); +} + +export function preferredWeaponAssetFile(id: WeaponCatalogId, optimizedAssets = true) { + const entry = weaponCatalogEntry(id); + return optimizedAssets && entry.optimizedFile ? entry.optimizedFile : entry.sourceFile; +} diff --git a/src/input/controller.test.ts b/src/input/controller.test.ts index c12d236..9c1bb8c 100644 --- a/src/input/controller.test.ts +++ b/src/input/controller.test.ts @@ -1,9 +1,11 @@ import { beforeEach, describe, expect, it } from "vitest"; import { + emitControllerToken, getControllerMovement, resetControllerState, setExternalControllerMovement, subscribeControllerMovement, + subscribeControllerToken, } from "./controller"; describe("controller movement normalization", () => { @@ -25,4 +27,18 @@ describe("controller movement normalization", () => { unsubscribe(); }); + + it("routes semantic actions without a DOM focus target", () => { + const received: Array<{ token: string; repeat: boolean }> = []; + const unsubscribe = subscribeControllerToken((event) => received.push(event)); + + emitControllerToken({ token: "Button0", repeat: false }); + emitControllerToken({ token: "Button13", repeat: true }); + + expect(received).toEqual([ + { token: "Button0", repeat: false }, + { token: "Button13", repeat: true }, + ]); + unsubscribe(); + }); }); diff --git a/src/input/nativeControllerFocus.test.ts b/src/input/nativeControllerFocus.test.ts new file mode 100644 index 0000000..cd4df88 --- /dev/null +++ b/src/input/nativeControllerFocus.test.ts @@ -0,0 +1,22 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const bridgeSource = readFileSync(new URL( + "../../android/app/src/main/java/com/phenomrom/iwanttoheal/ControllerBridgeActivity.java", + import.meta.url, +), "utf8"); + +describe("native controller bridge", () => { + it("never requests WebView or window focus for controller input", () => { + expect(bridgeSource).not.toMatch(/requestFocus\s*\(/); + expect(bridgeSource).not.toMatch(/setFocusable/); + expect(bridgeSource).not.toMatch(/onWindowFocusChanged/); + expect(bridgeSource).not.toMatch(/dispatchTouchEvent/); + }); + + it("routes controller input from activity-level key and motion dispatch", () => { + expect(bridgeSource).toMatch(/boolean dispatchKeyEvent\s*\(/); + expect(bridgeSource).toMatch(/boolean dispatchGenericMotionEvent\s*\(/); + expect(bridgeSource).toMatch(/evaluateJavascript\s*\(/); + }); +}); diff --git a/src/input/useMenuController.ts b/src/input/useMenuController.ts index e45ac25..e147ee3 100644 --- a/src/input/useMenuController.ts +++ b/src/input/useMenuController.ts @@ -10,6 +10,7 @@ export interface MenuAction { interface MenuControllerOptions { columns?: number; + initialId?: string; onBack?: () => void; } @@ -21,8 +22,11 @@ function nextIndex(index: number, direction: Direction, count: number, columns: } export function useMenuController(actions: MenuAction[], options: MenuControllerOptions = {}) { + // App-owned semantic cursor. Never coupled to document.activeElement or DOM focus. const enabled = actions.filter((action) => action.enabled !== false); - const [focusedId, setFocusedId] = useState(enabled[0]?.id ?? ""); + const [selectedId, setSelectedId] = useState(() => enabled.some((action) => action.id === options.initialId) + ? options.initialId! + : enabled[0]?.id ?? ""); const actionsRef = useRef(enabled); const backRef = useRef(options.onBack); const columnsRef = useRef(options.columns ?? 1); @@ -31,26 +35,26 @@ export function useMenuController(actions: MenuAction[], options: MenuController columnsRef.current = options.columns ?? 1; useEffect(() => { - if (!actionsRef.current.some((action) => action.id === focusedId)) { - setFocusedId(actionsRef.current[0]?.id ?? ""); + if (!actionsRef.current.some((action) => action.id === selectedId)) { + setSelectedId(actionsRef.current[0]?.id ?? ""); } - }, [actions, focusedId]); + }, [actions, selectedId]); const move = useCallback((direction: Direction) => { const current = actionsRef.current; if (!current.length) return; - const index = Math.max(0, current.findIndex((action) => action.id === focusedId)); + const index = Math.max(0, current.findIndex((action) => action.id === selectedId)); const neighborId = current[index]?.neighbors?.[direction]; if (neighborId && current.some((action) => action.id === neighborId)) { - setFocusedId(neighborId); + setSelectedId(neighborId); return; } - setFocusedId(current[nextIndex(index, direction, current.length, columnsRef.current)].id); - }, [focusedId]); + setSelectedId(current[nextIndex(index, direction, current.length, columnsRef.current)].id); + }, [selectedId]); const confirm = useCallback(() => { - actionsRef.current.find((action) => action.id === focusedId)?.run(); - }, [focusedId]); + actionsRef.current.find((action) => action.id === selectedId)?.run(); + }, [selectedId]); useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { @@ -81,8 +85,8 @@ export function useMenuController(actions: MenuAction[], options: MenuController }), [confirm, move]); return { - focusedId, - focus: setFocusedId, - isFocused: (id: string) => id === focusedId, + selectedId, + select: setSelectedId, + isSelected: (id: string) => id === selectedId, }; } diff --git a/src/platform/BottomDisplayApp.tsx b/src/platform/BottomDisplayApp.tsx index 5cbd593..e7ad92c 100644 --- a/src/platform/BottomDisplayApp.tsx +++ b/src/platform/BottomDisplayApp.tsx @@ -10,6 +10,7 @@ import type { DifficultySlug } from "../game/progression/loot"; import { useForcedThorDisplays } from "./useThorDualScreen"; import { createRateLimitedPublisher } from "./rateLimitedPublisher"; import { DEFAULT_CONTROLLER_GLYPHS } from "../input/controllerGlyphs"; +import type { HockeyPvpMatchConfig } from "../game/hockeyHealingPvp"; const BottomScreen = lazy(() => import("../components/BottomScreen").then((module) => ({ default: module.BottomScreen }))); const CONTROLLER_MOTION_SYNC_INTERVAL_MS = 33; @@ -21,6 +22,7 @@ function screenTitle(screen: AppScreen) { case "home": return "Choose expedition"; case "profile": return "Hunter profile"; case "gear": return "Gear upgrade"; + case "appearance": return "Appearance Lab"; case "settings": return "Field settings"; case "mode": return "Prepare encounter"; case "game": return "Field console"; @@ -67,8 +69,8 @@ export function BottomDisplayApp() { channelRef.current?.postMessage({ type: "frontend-command", command } satisfies DualScreenMessage); }, []); - const launchGame = useCallback((bossIds: readonly BossId[], difficultySlug?: DifficultySlug) => { - postFrontendCommand({ name: "launchGame", bossIds, difficultySlug }); + const launchGame = useCallback((bossIds: readonly BossId[], difficultySlug?: DifficultySlug, hockeyPvpMatch?: HockeyPvpMatchConfig) => { + postFrontendCommand({ name: "launchGame", bossIds, difficultySlug, hockeyPvpMatch }); }, [postFrontendCommand]); useEffect(() => { @@ -117,7 +119,7 @@ export function BottomDisplayApp() { copySlot: (sourceId, targetId) => postFrontend({ name: "copySlot", sourceId, targetId }), uploadSlot: (slotId) => { postFrontend({ name: "uploadSlot", slotId }); - return Promise.resolve(); + return Promise.resolve(false); }, downloadSlot: (slotId) => { postFrontend({ name: "downloadSlot", slotId }); @@ -132,6 +134,20 @@ export function BottomDisplayApp() { selectInfusion: (infusionId) => postFrontend({ name: "selectInfusion", infusionId }), selectPassiveAbility: (abilityId) => postFrontend({ name: "selectPassiveAbility", abilityId }), selectPassiveInfusion: (passiveId) => postFrontend({ name: "selectPassiveInfusion", passiveId }), + selectProfileCollectionView: (view) => postFrontend({ name: "selectProfileCollectionView", view }), + selectProfileGroup: (groupId) => postFrontend({ name: "selectProfileGroup", groupId }), + selectProfileStat: (statId) => postFrontend({ name: "selectProfileStat", statId }), + openAppearanceLab: () => postFrontend({ name: "openAppearanceLab" }), + selectAppearanceClass: (classId) => postFrontend({ name: "selectAppearanceClass", classId }), + updateAppearanceDraft: (appearance) => postFrontend({ name: "updateAppearanceDraft", appearance }), + resetAppearanceDraft: () => postFrontend({ name: "resetAppearanceDraft" }), + saveAppearanceDraft: () => { + postFrontend({ name: "saveAppearanceDraft" }); + return false; + }, + closeAppearanceLab: () => postFrontend({ name: "closeAppearanceLab" }), + setAppearancePreviewMode: (mode) => postFrontend({ name: "setAppearancePreviewMode", mode }), + setAppearancePreviewAnimation: (animation) => postFrontend({ name: "setAppearancePreviewAnimation", animation }), upgradeSelectedGear: () => { postFrontend({ name: "upgradeSelectedGear" }); return false; @@ -174,6 +190,13 @@ export function BottomDisplayApp() { return false; }, setEndlessChoiceSelection: (selection) => postCommand({ name: "setEndlessChoiceSelection", selection }), + dispatchRpgAction: (action) => { + postCommand({ name: "dispatchRpgAction", action }); + return false; + }, + setRpgFocusId: (focusId) => postCommand({ name: "setRpgFocusId", focusId }), + cycleRpgFocus: (direction) => postCommand({ name: "cycleRpgFocus", direction }), + moveRpgFocus: (direction) => postCommand({ name: "moveRpgFocus", direction }), }); channel.onmessage = (event: MessageEvent) => { if (event.data.type === "authoritative-ready") { diff --git a/src/platform/dualScreenSync.test.ts b/src/platform/dualScreenSync.test.ts index 2e52147..dc9ea48 100644 --- a/src/platform/dualScreenSync.test.ts +++ b/src/platform/dualScreenSync.test.ts @@ -1,16 +1,32 @@ import { describe, expect, it } from "vitest"; +import { createClassAbilityLoadout } from "../game/healers"; import { diffBottomGameSnapshot, executeFrontendCommand, executeGameCommand, type BottomGameSnapshot } from "./dualScreenSync"; import { useGameStore } from "../game/store"; -import { useFrontendStore } from "../frontend/store"; +import { getFrontendSnapshot, useFrontendStore } from "../frontend/store"; +import { createHockeyHealingState } from "../game/hockeyHealing"; +import { createBlockbreakerState } from "../game/blockbreaker"; +import { createAetherAssaultState } from "../game/aetherAssault"; +import { createHockeyPvpState } from "../game/hockeyHealingPvp"; +import { freshParty } from "../game/data"; +import { createBossMotionState, createBossState } from "../game/bossMechanics"; +import { createPartyCombatState } from "../game/partyCombat"; +import { createDefaultHealerAppearance } from "../game/healerVisuals"; +import { heldItemForModel } from "../game/weaponCatalog"; function snapshot(): BottomGameSnapshot { return { bossId: "bulldrome", bossInstanceId: "boss-0-bulldrome", paused: false, - healerClassId: "priest", + healerClassId: "priest", + abilityLoadout: createClassAbilityLoadout("priest"), phase: "combat", round: 1, + runMode: "encounter", + activityMode: "boss", + rpgRun: null, + rpgFocusId: null, + rpgSpellResources: { verdancy: 0, tidalSurge: 0, conviction: 0, chronoshards: 0 }, endlessMode: false, endlessBossKills: 0, endlessChoiceSelection: "continue", @@ -30,7 +46,7 @@ function snapshot(): BottomGameSnapshot { bossMotion: { bossId: "bulldrome", activeMechanicId: null, formationOffsetX: 0, mode: "holding", position: [0, 0], chargeStart: [0, 0], chargeEnd: [0, 0], chargeTargetId: "aelia", chargeHitIds: [], phaseEndsAt: 0, chargeCount: 0, pounceTargetId: "aelia", pounceCenter: [0, 0], - nextMechanicAt: Infinity, mechanicCount: 0, phaseStartedAt: 0, mechanicHitIds: [], mechanicNextDamageAt: {}, tetherIds: [], tetherBreakDistance: 0, + nextMechanicAt: Infinity, mechanicCount: 0, phaseStartedAt: 0, lastMeleeAt: -1, mechanicHitIds: [], mechanicNextDamageAt: {}, tetherIds: [], tetherBreakDistance: 0, breathAngle: 0, breathStartAngle: 0, breathEndAngle: 0, hazards: [], slashLanes: [], poolMechanicCount: 0, poolTelegraphs: [], }, partyCombat: { @@ -40,20 +56,42 @@ function snapshot(): BottomGameSnapshot { orin: { id: "orin", readyAt: 0, resource: 0, points: 0, cooldowns: {}, activeAction: null, visualAction: null, overchargeStacks: 0, bladeFlurryUntil: 0, revengeReadyUntil: 0, lastHp: 100, damageDone: 0 }, vale: { id: "vale", readyAt: 0, resource: 0, points: 0, cooldowns: {}, activeAction: null, visualAction: null, overchargeStacks: 0, bladeFlurryUntil: 0, revengeReadyUntil: 0, lastHp: 100, damageDone: 0 }, }, - tankAura: { expiresAt: 0, radius: 3, damageReduction: 0.3 }, + tankAura: { expiresAt: 0, radius: 3, damageReduction: 0.3, sourceId: "brann" }, nextEventId: 1, }, mana: 100, maxMana: 100, selectedMemberId: "brann", - cooldowns: { mend: 0, renew: 0, shield: 0, purify: 0, radiance: 0, barrier: 0 }, + cooldowns: { ability1: 0, ability2: 0, ability3: 0, ability4: 0, ability5: 0, ability6: 0 }, globalCooldownUntil: 0, activeTab: "combat", selectedItemId: "", inventory: [], playerPosition: [0, 0], activeCast: null, - barrier: { center: [0, 0], expiresAt: 0, nextHealAt: 0 }, + barrier: { kind: null, center: [0, 0], expiresAt: 0, nextHealAt: 0 }, + healerMechanic: { + resource: 0, + maxResource: 0, + flourishExpiresAt: 0, + nextPulseAt: 0, + beaconTargetId: null, + beaconExpiresAt: 0, + avengingCrusaderExpiresAt: 0, + temporalAnchor: null, + timeLoop: null, + }, + hockey: createHockeyHealingState(false), + blockbreaker: createBlockbreakerState(false), + aetherAssault: createAetherAssaultState(false), + hockeyPvp: createHockeyPvpState(), + hockeyPvpOpponent: { + party: freshParty(), + partyPositions: { aelia: [0, 4.5], brann: [0, 0], nia: [-3, 2], orin: [3, 2], vale: [0, -2] }, + boss: createBossState("bulldrome"), + bossMotion: createBossMotionState("bulldrome"), + partyCombat: createPartyCombatState(freshParty()), + }, }; } @@ -67,6 +105,14 @@ describe("dual-screen game snapshots", () => { const next = { ...clonedWithoutChanges, time: 10.1, mana: 97 }; expect(diffBottomGameSnapshot(clonedWithoutChanges, next)).toEqual({ time: 10.1, mana: 97 }); + + const hockey = createHockeyHealingState(true); + const hockeyNext = { ...next, runMode: "hockey-healing" as const, hockey }; + expect(diffBottomGameSnapshot(next, hockeyNext)).toEqual({ runMode: "hockey-healing", hockey }); + + const aetherAssault = createAetherAssaultState(true, 42); + const aetherNext = { ...hockeyNext, runMode: "aether-assault" as const, aetherAssault }; + expect(diffBottomGameSnapshot(hockeyNext, aetherNext)).toEqual({ runMode: "aether-assault", aetherAssault }); }); it("routes maxed-run continuation and passive filter commands", () => { @@ -74,6 +120,9 @@ describe("dual-screen game snapshots", () => { const originalStartEndless = useGameStore.getState().startRogueTrialsEndless; const originalSelectAbility = useFrontendStore.getState().selectPassiveAbility; const originalSelectPassive = useFrontendStore.getState().selectPassiveInfusion; + const originalSelectProfileView = useFrontendStore.getState().selectProfileCollectionView; + const originalSelectProfileGroup = useFrontendStore.getState().selectProfileGroup; + const originalSelectProfileStat = useFrontendStore.getState().selectProfileStat; const calls: string[] = []; useGameStore.setState({ continueRoguelikeRound: () => { calls.push("continue"); return true; }, @@ -82,15 +131,105 @@ describe("dual-screen game snapshots", () => { useFrontendStore.setState({ selectPassiveAbility: (abilityId) => { calls.push(`ability:${abilityId}`); }, selectPassiveInfusion: (passiveId) => { calls.push(`passive:${passiveId}`); }, + selectProfileCollectionView: (view) => { calls.push(`profile-view:${view}`); }, + selectProfileGroup: (groupId) => { calls.push(`profile-group:${groupId}`); }, + selectProfileStat: (statId) => { calls.push(`profile-stat:${statId}`); }, }); executeGameCommand({ name: "continueRoguelikeRound" }); executeGameCommand({ name: "startRogueTrialsEndless" }); - executeFrontendCommand({ name: "selectPassiveAbility", abilityId: "shield" }); + executeFrontendCommand({ name: "selectPassiveAbility", abilityId: "ability3" }); executeFrontendCommand({ name: "selectPassiveInfusion", passiveId: "shield-guard" }); - expect(calls).toEqual(["continue", "endless", "ability:shield", "passive:shield-guard"]); + executeFrontendCommand({ name: "selectProfileCollectionView", view: "stats" }); + executeFrontendCommand({ name: "selectProfileGroup", groupId: "bind-venom" }); + executeFrontendCommand({ name: "selectProfileStat", statId: "broodfang-spider" }); + expect(calls).toEqual([ + "continue", + "endless", + "ability:ability3", + "passive:shield-guard", + "profile-view:stats", + "profile-group:bind-venom", + "profile-stat:broodfang-spider", + ]); useGameStore.setState({ continueRoguelikeRound: originalContinue, startRogueTrialsEndless: originalStartEndless }); - useFrontendStore.setState({ selectPassiveAbility: originalSelectAbility, selectPassiveInfusion: originalSelectPassive }); + useFrontendStore.setState({ + selectPassiveAbility: originalSelectAbility, + selectPassiveInfusion: originalSelectPassive, + selectProfileCollectionView: originalSelectProfileView, + selectProfileGroup: originalSelectProfileGroup, + selectProfileStat: originalSelectProfileStat, + }); + }); + + it("includes authoritative profile selection in companion snapshots", () => { + const original = useFrontendStore.getState(); + original.selectProfileCollectionView("stats"); + original.selectProfileGroup("bind-venom"); + original.selectProfileStat("broodfang-spider"); + + const snapshot = getFrontendSnapshot(); + expect(snapshot.profileCollectionView).toBe("stats"); + expect(snapshot.selectedProfileGroupId).toBe("bind-venom"); + expect(snapshot.selectedProfileStatId).toBe("broodfang-spider"); + expect("selectProfileCollectionView" in snapshot).toBe(false); + + useFrontendStore.setState({ + profileCollectionView: original.profileCollectionView, + selectedProfileGroupId: original.selectedProfileGroupId, + selectedProfileStatId: original.selectedProfileStatId, + }); + }); + + it("routes Appearance Lab commands to the authoritative frontend store", () => { + const original = useFrontendStore.getState(); + const calls: string[] = []; + const appearance = { + ...createDefaultHealerAppearance("priest"), + headPartId: "rogue-head" as const, + mainHand: heldItemForModel("cc/staff_d"), + }; + useFrontendStore.setState({ + openAppearanceLab: () => { calls.push("open"); }, + selectAppearanceClass: (classId) => { calls.push(`class:${classId}`); }, + updateAppearanceDraft: (draft) => { calls.push(`draft:${draft.headPartId}:${draft.mainHand.modelId}`); }, + resetAppearanceDraft: () => { calls.push("reset"); }, + saveAppearanceDraft: () => { calls.push("save"); return true; }, + closeAppearanceLab: () => { calls.push("close"); }, + setAppearancePreviewMode: (mode) => { calls.push(`mode:${mode}`); }, + setAppearancePreviewAnimation: (animation) => { calls.push(`animation:${animation}`); }, + }); + + executeFrontendCommand({ name: "openAppearanceLab" }); + executeFrontendCommand({ name: "selectAppearanceClass", classId: "chronomancer" }); + executeFrontendCommand({ name: "updateAppearanceDraft", appearance }); + executeFrontendCommand({ name: "resetAppearanceDraft" }); + executeFrontendCommand({ name: "saveAppearanceDraft" }); + executeFrontendCommand({ name: "setAppearancePreviewMode", mode: "legacy" }); + executeFrontendCommand({ name: "setAppearancePreviewAnimation", animation: "cast" }); + executeFrontendCommand({ name: "closeAppearanceLab" }); + + expect(calls).toEqual([ + "open", + "class:chronomancer", + "draft:rogue-head:cc/staff_d", + "reset", + "save", + "mode:legacy", + "animation:cast", + "close", + ]); + + useFrontendStore.setState({ + openAppearanceLab: original.openAppearanceLab, + selectAppearanceClass: original.selectAppearanceClass, + updateAppearanceDraft: original.updateAppearanceDraft, + resetAppearanceDraft: original.resetAppearanceDraft, + saveAppearanceDraft: original.saveAppearanceDraft, + closeAppearanceLab: original.closeAppearanceLab, + setAppearancePreviewMode: original.setAppearancePreviewMode, + setAppearancePreviewAnimation: original.setAppearancePreviewAnimation, + }); }); }); diff --git a/src/platform/dualScreenSync.ts b/src/platform/dualScreenSync.ts index 8691ed7..2d1bde7 100644 --- a/src/platform/dualScreenSync.ts +++ b/src/platform/dualScreenSync.ts @@ -1,20 +1,24 @@ -import type { AppScreen } from "../frontend/types"; -import type { FrontendSnapshot } from "../frontend/store"; +import type { AppScreen, ProfileCollectionView, ProfileStatId } from "../frontend/types"; +import type { AppearancePreviewAnimation, FrontendSnapshot } from "../frontend/store"; import { useFrontendStore } from "../frontend/store"; import { emitControllerToken, setExternalControllerMovement, type ControllerMovement, type ControllerTokenEvent } from "../input/controller"; import { type GameState, useGameStore } from "../game/store"; -import type { AbilityId, BottomTab, MemberId, RunBuffId } from "../game/types"; +import type { AbilitySlotId, BottomTab, MemberId, RunBuffId } from "../game/types"; import type { BossId, HealerClassId } from "../game/types"; import type { GameModeId, GameSettings, SaveSlotId } from "../frontend/types"; import type { GearOwnerId, GearSlotId } from "../game/progression/gear"; import type { DifficultySlug } from "../game/progression/loot"; +import type { BossGroupId } from "../game/bossCatalog"; +import type { HockeyPvpMatchConfig } from "../game/hockeyHealingPvp"; +import type { RpgFocusDirection, RpgRoguelikeAction } from "../game/rpgRoguelike"; +import type { CharacterAppearanceV1, CharacterModelMode } from "../game/characterAppearance"; const CHANNEL_NAME = "i-want-to-heal:thor-dual-screen:v1"; export type GameCommand = | { name: "startEncounter" } | { name: "restart" } - | { name: "castAbility"; abilityId: AbilityId } + | { name: "castAbility"; abilityId: AbilitySlotId } | { name: "selectMember"; memberId: MemberId } | { name: "cycleMember"; direction: 1 | -1 } | { name: "setActiveTab"; tab: BottomTab } @@ -25,7 +29,11 @@ export type GameCommand = | { name: "chooseRunBuff"; buffId: RunBuffId } | { name: "continueRoguelikeRound" } | { name: "startRogueTrialsEndless" } - | { name: "setEndlessChoiceSelection"; selection: "continue" | "quit" }; + | { name: "setEndlessChoiceSelection"; selection: "continue" | "quit" } + | { name: "dispatchRpgAction"; action: RpgRoguelikeAction } + | { name: "setRpgFocusId"; focusId: string } + | { name: "cycleRpgFocus"; direction: 1 | -1 } + | { name: "moveRpgFocus"; direction: RpgFocusDirection }; export type FrontendCommand = | { name: "signIn"; username: string; password: string } @@ -47,15 +55,26 @@ export type FrontendCommand = | { name: "selectGearSlot"; slotId: GearSlotId } | { name: "selectGearWorkshopMode"; mode: "upgrade" | "infusion" } | { name: "selectInfusion"; infusionId: string } - | { name: "selectPassiveAbility"; abilityId: AbilityId } + | { name: "selectPassiveAbility"; abilityId: AbilitySlotId } | { name: "selectPassiveInfusion"; passiveId: RunBuffId } + | { name: "selectProfileCollectionView"; view: ProfileCollectionView } + | { name: "selectProfileGroup"; groupId: BossGroupId } + | { name: "selectProfileStat"; statId: ProfileStatId } + | { name: "openAppearanceLab" } + | { name: "selectAppearanceClass"; classId: HealerClassId } + | { name: "updateAppearanceDraft"; appearance: CharacterAppearanceV1 } + | { name: "resetAppearanceDraft" } + | { name: "saveAppearanceDraft" } + | { name: "closeAppearanceLab" } + | { name: "setAppearancePreviewMode"; mode: CharacterModelMode } + | { name: "setAppearancePreviewAnimation"; animation: AppearancePreviewAnimation } | { name: "upgradeSelectedGear" } | { name: "equipSelectedInfusion" } | { name: "equipPassiveInfusion"; passiveId: RunBuffId } | { name: "selectHealerClass"; classId: HealerClassId } | { name: "updateSetting"; key: keyof GameSettings; value: GameSettings[keyof GameSettings] } | { name: "exitGame" } - | { name: "launchGame"; bossIds: readonly BossId[]; difficultySlug?: DifficultySlug }; + | { name: "launchGame"; bossIds: readonly BossId[]; difficultySlug?: DifficultySlug; hockeyPvpMatch?: HockeyPvpMatchConfig }; export const DUAL_SCREEN_LAUNCH_EVENT = "iwt:dual-screen-launch-game"; export const DUAL_SCREEN_EXIT_EVENT = "iwt:dual-screen-exit-game"; @@ -92,6 +111,10 @@ export function executeGameCommand(command: GameCommand) { case "continueRoguelikeRound": game.continueRoguelikeRound(); break; case "startRogueTrialsEndless": game.startRogueTrialsEndless(); break; case "setEndlessChoiceSelection": game.setEndlessChoiceSelection(command.selection); break; + case "dispatchRpgAction": game.dispatchRpgAction(command.action); break; + case "setRpgFocusId": game.setRpgFocusId(command.focusId); break; + case "cycleRpgFocus": game.cycleRpgFocus(command.direction); break; + case "moveRpgFocus": game.moveRpgFocus(command.direction); break; } } @@ -119,13 +142,24 @@ export function executeFrontendCommand(command: FrontendCommand) { case "selectInfusion": frontend.selectInfusion(command.infusionId); break; case "selectPassiveAbility": frontend.selectPassiveAbility(command.abilityId); break; case "selectPassiveInfusion": frontend.selectPassiveInfusion(command.passiveId); break; + case "selectProfileCollectionView": frontend.selectProfileCollectionView(command.view); break; + case "selectProfileGroup": frontend.selectProfileGroup(command.groupId); break; + case "selectProfileStat": frontend.selectProfileStat(command.statId); break; + case "openAppearanceLab": frontend.openAppearanceLab(); break; + case "selectAppearanceClass": frontend.selectAppearanceClass(command.classId); break; + case "updateAppearanceDraft": frontend.updateAppearanceDraft(command.appearance); break; + case "resetAppearanceDraft": frontend.resetAppearanceDraft(); break; + case "saveAppearanceDraft": frontend.saveAppearanceDraft(); break; + case "closeAppearanceLab": frontend.closeAppearanceLab(); break; + case "setAppearancePreviewMode": frontend.setAppearancePreviewMode(command.mode); break; + case "setAppearancePreviewAnimation": frontend.setAppearancePreviewAnimation(command.animation); break; case "upgradeSelectedGear": frontend.upgradeSelectedGear(); break; case "equipSelectedInfusion": frontend.equipSelectedInfusion(); break; case "equipPassiveInfusion": frontend.equipPassiveInfusion(command.passiveId); break; case "selectHealerClass": frontend.selectHealerClass(command.classId); break; case "updateSetting": frontend.updateSetting(command.key, command.value); break; case "exitGame": window.dispatchEvent(new Event(DUAL_SCREEN_EXIT_EVENT)); break; - case "launchGame": window.dispatchEvent(new CustomEvent(DUAL_SCREEN_LAUNCH_EVENT, { detail: { bossIds: command.bossIds, difficultySlug: command.difficultySlug } })); break; + case "launchGame": window.dispatchEvent(new CustomEvent(DUAL_SCREEN_LAUNCH_EVENT, { detail: { bossIds: command.bossIds, difficultySlug: command.difficultySlug, hockeyPvpMatch: command.hockeyPvpMatch } })); break; } } @@ -142,8 +176,14 @@ export type BottomGameSnapshot = Pick; const BOTTOM_GAME_SNAPSHOT_KEYS: readonly (keyof BottomGameSnapshot)[] = [ - "bossId", "bossInstanceId", "paused", "healerClassId", "phase", "round", "endlessMode", "endlessBossKills", "endlessChoiceSelection", "runModifiers", "time", "party", "boss", "additionalBosses", + "bossId", "bossInstanceId", "paused", "healerClassId", "abilityLoadout", "phase", "round", "runMode", "activityMode", "rpgRun", "rpgFocusId", "rpgSpellResources", "endlessMode", "endlessBossKills", "endlessChoiceSelection", "runModifiers", "time", "party", "boss", "additionalBosses", "partyPositions", "bossMotion", "partyCombat", "mana", "maxMana", "selectedMemberId", "cooldowns", - "globalCooldownUntil", "activeTab", "selectedItemId", "inventory", "playerPosition", "activeCast", "barrier", + "globalCooldownUntil", "activeTab", "selectedItemId", "inventory", "playerPosition", "activeCast", "barrier", "healerMechanic", "hockey", "blockbreaker", "aetherAssault", "hockeyPvp", "hockeyPvpOpponent", ]; function structurallyEqual(left: unknown, right: unknown): boolean { @@ -198,8 +244,14 @@ export function currentBottomGameSnapshot(): BottomGameSnapshot { bossInstanceId: state.bossInstanceId, paused: state.paused, healerClassId: state.healerClassId, + abilityLoadout: state.abilityLoadout, phase: state.phase, round: state.round, + runMode: state.runMode, + activityMode: state.activityMode, + rpgRun: state.rpgRun, + rpgFocusId: state.rpgFocusId, + rpgSpellResources: state.rpgSpellResources, endlessMode: state.endlessMode, endlessBossKills: state.endlessBossKills, endlessChoiceSelection: state.endlessChoiceSelection, @@ -222,6 +274,12 @@ export function currentBottomGameSnapshot(): BottomGameSnapshot { playerPosition: state.playerPosition, activeCast: state.activeCast, barrier: state.barrier, + healerMechanic: state.healerMechanic, + hockey: state.hockey, + blockbreaker: state.blockbreaker, + aetherAssault: state.aetherAssault, + hockeyPvp: state.hockeyPvp, + hockeyPvpOpponent: state.hockeyPvpOpponent, }; } diff --git a/src/styles.css b/src/styles.css index 0617216..6092cf3 100644 --- a/src/styles.css +++ b/src/styles.css @@ -374,6 +374,9 @@ button:focus-visible { .boss-bar-wrap.is-trio { top: 1.1%; width: 42%; gap: 2px; } .boss-bar-wrap.is-trio .boss-name strong { font-size: 8px; } .boss-bar-wrap.is-trio .boss-bar { height: 5px; margin-top: 1px; } +.boss-bar-wrap.is-pvp { width: 42%; } +.boss-bar-entry.is-opponent .boss-bar { border-color: rgba(190, 92, 153, 0.52); } +.boss-bar-entry.is-opponent .boss-bar i { background: linear-gradient(90deg, #6f315f, #d65c96); box-shadow: 0 0 9px rgba(214, 92, 150, 0.55); } .boss-name { display: grid; @@ -431,6 +434,25 @@ button:focus-visible { text-transform: uppercase; } +.objective-chip.is-hockey { + border-right-color: #67e8ff; + background: linear-gradient(90deg, rgba(7, 24, 30, 0.62), rgba(5, 18, 24, 0.94)); +} + +.objective-chip.is-hockey span { color: #89efff; } +.objective-chip.is-pvp { border-right-color: #ef6c9f; background: linear-gradient(90deg, rgba(17, 24, 39, 0.64), rgba(37, 11, 25, 0.94)); } +.objective-chip.is-pvp span { color: #ff9cc2; } +.objective-chip.is-blockbreaker { + border-right-color: #a9ef64; + background: linear-gradient(90deg, rgba(9, 28, 32, 0.65), rgba(22, 34, 13, 0.95)); +} +.objective-chip.is-blockbreaker span { color: #baf77e; } +.objective-chip.is-aether { + border-right-color: #66f1ff; + background: linear-gradient(90deg, rgba(11, 37, 48, 0.68), rgba(15, 28, 50, 0.95)); +} +.objective-chip.is-aether span { color: #9ef7ff; } + .objective-chip span { color: var(--gold); font-size: 7px; @@ -463,6 +485,28 @@ button:focus-visible { .encounter-callout.is-urgent { border-color: var(--red); animation: warning-pulse 0.7s ease-in-out infinite alternate; } .encounter-callout.is-urgent strong { color: #ff826b; } +.dampening-indicator { + position: absolute; + top: 13.5%; + right: 2.3%; + width: 148px; + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + gap: 3px 8px; + padding: 5px 7px 6px 9px; + border: 1px solid rgba(255, 126, 91, 0.38); + border-right: 2px solid #ff795d; + background: linear-gradient(90deg, rgba(38, 13, 12, 0.58), rgba(37, 10, 13, 0.92)); + text-align: right; + text-shadow: 0 1px 3px #000; +} + +.dampening-indicator span { color: #e3a092; font-size: 7px; font-weight: 700; letter-spacing: 0.13em; text-transform: uppercase; } +.dampening-indicator strong { color: #ffb19c; font-size: 12px; } +.dampening-indicator > i { grid-column: 1 / -1; height: 3px; overflow: hidden; background: #240908; } +.dampening-indicator > i em { display: block; height: 100%; background: linear-gradient(90deg, #c84635, #ff9776); box-shadow: 0 0 6px rgba(255, 104, 72, 0.75); transition: width 180ms ease; } + .casting-bar { position: absolute; left: 50%; @@ -570,6 +614,72 @@ button:focus-visible { .phase-intermission { background: radial-gradient(circle at center, rgba(94, 76, 26, 0.38), rgba(3, 9, 8, 0.84)); } .phase-defeat { background: radial-gradient(circle at center, rgba(85, 31, 21, 0.45), rgba(6, 5, 4, 0.86)); } +.goal-popup { + position: absolute; + z-index: 11; + top: 44%; + left: 50%; + min-width: 26%; + display: grid; + place-items: center; + padding: 10px 28px 12px; + border-top: 2px solid #9df4ff; + border-bottom: 2px solid #9df4ff; + color: #eaffff; + background: linear-gradient(90deg, transparent, rgba(13, 91, 104, 0.88) 20%, rgba(8, 43, 48, 0.94) 50%, rgba(13, 91, 104, 0.88) 80%, transparent); + filter: drop-shadow(0 0 16px rgba(83, 228, 255, 0.72)); + pointer-events: none; + transform: translate(-50%, -50%); + animation: goal-popup-burst 1.2s ease-out both; +} + +.goal-popup::before, +.goal-popup::after { + content: ""; + position: absolute; + top: 50%; + width: 34%; + height: 1px; + background: linear-gradient(90deg, transparent, #b8f8ff); +} + +.goal-popup::before { right: 100%; } +.goal-popup::after { left: 100%; transform: scaleX(-1); } +.goal-popup strong { font: 700 clamp(34px, 7.2vw, 76px) "Cinzel", serif; letter-spacing: 0.16em; text-indent: 0.16em; text-shadow: 0 0 10px #59dff5, 0 3px 5px #001315; } +.goal-popup.is-conceded { border-color: #ffb087; background: linear-gradient(90deg, transparent, rgba(132, 47, 31, 0.88) 20%, rgba(58, 19, 14, 0.94) 50%, rgba(132, 47, 31, 0.88) 80%, transparent); filter: drop-shadow(0 0 16px rgba(255, 111, 73, 0.74)); } +.goal-popup.is-conceded::before, +.goal-popup.is-conceded::after { background: linear-gradient(90deg, transparent, #ffd0ac); } +.goal-popup.is-conceded strong { text-shadow: 0 0 10px #ff815e, 0 3px 5px #190400; } + +.blockbreaker-score-popup { + position: absolute; + z-index: 11; + top: 58%; + left: 50%; + min-width: 150px; + display: grid; + justify-items: center; + padding: 8px 22px 10px; + border-top: 1px solid #a9ef64; + border-bottom: 1px solid #45d9ec; + color: #f2ffe7; + background: linear-gradient(90deg, transparent, rgba(28, 70, 35, 0.9) 30%, rgba(9, 44, 49, 0.92) 70%, transparent); + filter: drop-shadow(0 0 13px rgba(131, 235, 102, 0.58)); + pointer-events: none; + transform: translate(-50%, -50%); + animation: blockbreaker-score-burst 850ms ease-out both; +} +.blockbreaker-score-popup strong { font: 700 clamp(24px, 4.5vw, 48px) "Cinzel", serif; letter-spacing: .06em; text-shadow: 0 0 9px rgba(84, 231, 239, .8); } +.blockbreaker-score-popup small { color: #bef29b; font-size: clamp(7px, 1.1vw, 11px); font-weight: 700; letter-spacing: .14em; text-transform: uppercase; } +.aether-score-popup { + border-top-color: #ffcf77; + border-bottom-color: #66efff; + color: #f1fdff; + background: linear-gradient(90deg, transparent, rgba(19, 52, 76, .9) 28%, rgba(35, 32, 76, .92) 72%, transparent); + filter: drop-shadow(0 0 13px rgba(102, 239, 255, .62)); +} +.aether-score-popup small { color: #9ef7ff; } + .pause-overlay { position: absolute; z-index: 12; inset: 0; display: grid; place-items: center; background: rgba(2,8,7,0.76); backdrop-filter: blur(5px); pointer-events: auto; } .pause-panel { width: min(370px, 62%); display: grid; justify-items: center; padding: 25px 30px 20px; border: 1px solid rgba(232,200,114,0.48); background: linear-gradient(145deg, rgba(12,29,24,0.97), rgba(4,11,9,0.98)); box-shadow: 0 18px 55px rgba(0,0,0,0.6), inset 0 0 30px rgba(232,200,114,0.035); text-align: center; } .pause-panel > span { color: var(--gold); font-size: 7px; font-weight: 700; letter-spacing: 0.2em; text-transform: uppercase; } @@ -578,7 +688,7 @@ button:focus-visible { .pause-actions { width: 100%; display: grid; gap: 7px; } .pause-actions button { display: flex; align-items: center; justify-content: space-between; min-height: 38px; padding: 8px 12px; border: 1px solid var(--gold); color: #172019; background: linear-gradient(110deg, #ffe499, #d1af54); font-family: "Cinzel", serif; font-size: 10px; text-align: left; } .pause-actions button.secondary { border-color: rgba(197,218,209,0.25); color: #dce9e4; background: rgba(8,20,17,0.9); } -.pause-actions button.is-controller-focused { outline: 2px solid #fff1b6; outline-offset: 2px; } +.pause-actions button.is-controller-selected { outline: 2px solid #fff1b6; outline-offset: 2px; } .pause-actions small { font-family: Inter, sans-serif; font-size: 6px; letter-spacing: 0.08em; } .pause-panel footer { display: flex; align-items: center; gap: 6px; margin-top: 14px; color: #72877f; font-size: 7px; text-transform: uppercase; } .pause-panel footer b { color: #dce9e4; } @@ -799,6 +909,8 @@ button:focus-visible { .shield-effect { border: 1px solid #63bfff; color: #9edbff; background: #153d58; } .renew-effect { border: 1px solid #59c888; color: #b5f1cd; background: #163e29; } +.earth-shield-effect { border: 1px solid #d2b66c; color: #fff0ae; background: #51451b; } +.spirit-link-effect { border: 1px solid #a28cff; color: #e2d8ff; background: #352759; box-shadow: 0 0 7px rgba(162, 140, 255, 0.5); } .barrier-effect { border: 1px solid #ebca60; color: #fff1a8; background: #554715; box-shadow: 0 0 7px rgba(235, 202, 96, 0.55); } .tank-aura-effect { border: 1px solid #66bdf2; color: #cceeff; background: #174764; box-shadow: 0 0 7px rgba(102, 189, 242, 0.55); } .debuff-effect { border: 1px solid #ff795e; color: white; background: #9a3528; box-shadow: 0 0 8px rgba(229, 80, 54, 0.66); animation: warning-pulse 0.6s infinite alternate; } @@ -822,6 +934,28 @@ button:focus-visible { background: rgba(15, 31, 27, 0.66); } +.ability-meta.is-hockey { grid-template-columns: 0.85fr 1.1fr 1fr; } + +.hockey-run-meta { + min-width: 0; + display: grid; + grid-template-columns: auto auto; + align-items: baseline; + gap: 0 5px; + padding-left: 7px; + border-left: 1px solid rgba(103, 232, 255, 0.2); +} + +.hockey-run-meta span { color: #70a8b1; font-size: 7px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; } +.hockey-run-meta strong { color: #8ff1ff; font-size: 13px; } +.hockey-run-meta small { grid-column: 1 / -1; overflow: hidden; color: #6d8b90; font-size: 6px; letter-spacing: 0.06em; text-overflow: ellipsis; text-transform: uppercase; white-space: nowrap; } +.pvp-run-meta { border-left-color: rgba(239, 108, 159, 0.32); } +.pvp-run-meta strong { color: #ff9cc2; } +.blockbreaker-run-meta { border-left-color: rgba(169, 239, 100, 0.32); } +.blockbreaker-run-meta strong { color: #baf77e; } +.aether-run-meta { border-left-color: rgba(102, 241, 255, 0.35); } +.aether-run-meta strong { color: #9ef7ff; } + .target-chip, .mana-wrap { min-width: 0; @@ -830,6 +964,11 @@ button:focus-visible { gap: 6px; } +.resource-stack { min-width: 0; display: grid; gap: 2px; } +.class-resource { min-width: 0; display: flex; align-items: baseline; justify-content: space-between; gap: 6px; } +.class-resource span { overflow: hidden; color: #6fa984; font-size: 6px; font-weight: 700; letter-spacing: 0.09em; text-overflow: ellipsis; text-transform: uppercase; white-space: nowrap; } +.class-resource b { color: #9be9ae; font-size: 8px; white-space: nowrap; } + .target-chip span, .mana-wrap span { color: #63786f; font-size: 7px; font-weight: 700; letter-spacing: 0.13em; text-transform: uppercase; } .target-chip strong { overflow: hidden; color: var(--gold-strong); font-size: 11px; letter-spacing: 0.07em; text-overflow: ellipsis; text-transform: uppercase; white-space: nowrap; } @@ -1076,7 +1215,7 @@ button:focus-visible { .end-actions { display: flex; gap: 9px; margin-top: 18px; } .end-actions button { padding: 9px 18px; border: 1px solid var(--gold); color: #13170f; background: var(--gold); font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; cursor: pointer; } .end-actions button.secondary { color: #9eafa8; background: transparent; border-color: #43564f; } -.end-actions button.is-controller-focused { outline: 2px solid #fff1b6; outline-offset: 2px; } +.end-actions button.is-controller-selected { outline: 2px solid #fff1b6; outline-offset: 2px; } .endless-choice-actions button:first-child { display: grid; gap: 3px; min-width: 155px; } .endless-choice-actions button:first-child small { font-size: 7px; font-weight: 600; letter-spacing: 0.06em; opacity: 0.72; } @@ -1140,9 +1279,9 @@ button:focus-visible { background: linear-gradient(155deg, color-mix(in srgb, var(--buff-accent), #081310 91%), rgba(5, 13, 11, 0.94)); text-align: left; } -.buff-choice-grid button.is-controller-focused { outline: 2px solid var(--gold-strong); outline-offset: 2px; transform: translateY(-2px); } +.buff-choice-grid button.is-controller-selected { outline: 2px solid var(--gold-strong); outline-offset: 2px; transform: translateY(-2px); } .buff-draft.is-input-locked .buff-choice-grid button { cursor: default; filter: saturate(0.55) brightness(0.78); } -.buff-draft.is-input-locked .buff-choice-grid button.is-controller-focused { outline-color: #60726b; transform: none; } +.buff-draft.is-input-locked .buff-choice-grid button.is-controller-selected { outline-color: #60726b; transform: none; } .buff-choice-grid button > i { grid-row: 1 / 3; width: 32px; height: 32px; display: grid; place-items: center; border: 1px solid var(--buff-accent); color: var(--buff-accent); font-family: "Cinzel", serif; font-size: 16px; font-style: normal; } .buff-choice-grid button > span { min-width: 0; display: grid; } .buff-choice-grid button small { color: var(--buff-accent); font-size: 6px; letter-spacing: 0.1em; text-transform: uppercase; } @@ -1179,6 +1318,11 @@ button:focus-visible { .legend-party { background: var(--gold); } .legend-boss { background: var(--red); } .legend-exit { border: 1px solid var(--teal); } +.legend-paddle { width: 12px !important; border-radius: 2px !important; background: #ff9a61; box-shadow: 0 0 5px rgba(255, 118, 79, 0.7); } +.legend-opponent { background: #ef6c9f; box-shadow: 0 0 5px rgba(239, 108, 159, 0.65); } +.legend-brick { border-radius: 1px !important; background: linear-gradient(90deg, #36d9ef 0 25%, #f1b74f 25% 50%, #e85aa9 50% 75%, #93db54 75%); } +.legend-ship { border-radius: 1px !important; background: #66eaff; box-shadow: 0 0 5px rgba(102, 234, 255, .72); transform: rotate(45deg); } +.legend-shot { background: #ff2518; box-shadow: 0 0 5px rgba(255, 37, 24, .78); } .map-canvas { position: relative; @@ -1192,11 +1336,27 @@ button:focus-visible { } .map-canvas svg { width: auto; height: 94%; } +.hockey-map-room { stroke: rgba(111, 218, 235, 0.36); stroke-width: 2; } +.hockey-map-goal { fill: none; stroke-width: 5; stroke-linecap: round; } +.hockey-map-goal.is-npc { stroke: #e76a5b; filter: drop-shadow(0 0 4px rgba(231, 106, 91, 0.65)); } +.hockey-map-goal.is-healer { stroke: #67e8ff; filter: drop-shadow(0 0 4px rgba(103, 232, 255, 0.7)); } +.hockey-map-puck { fill: #ecfdff; stroke: #4cddff; stroke-width: 2; filter: drop-shadow(0 0 5px rgba(76, 221, 255, 0.9)); } +.hockey-map-paddle { fill: #ff8a5b; stroke: #ffe0a8; stroke-width: 1.4; filter: drop-shadow(0 0 5px rgba(255, 91, 61, 0.85)); } +.blockbreaker-map-danger { fill: none; stroke: #ff6d5d; stroke-width: 2; stroke-dasharray: 5 3; filter: drop-shadow(0 0 4px rgba(255, 83, 66, .75)); } +.blockbreaker-map-brick { stroke: rgba(244, 255, 245, .76); stroke-width: .8; filter: drop-shadow(0 0 2px rgba(190, 246, 220, .42)); } +.aether-map-lanes { stroke: rgba(67, 185, 205, .34); stroke-dasharray: 6 5; } +.aether-map-ship { fill: #34cee7; stroke: #e0fcff; stroke-width: 1; filter: drop-shadow(0 0 4px rgba(52, 206, 231, .8)); } +.aether-map-ship.is-armored { fill: #e8aa58; stroke: #fff0c8; filter: drop-shadow(0 0 4px rgba(232, 170, 88, .82)); } +.aether-map-warning { fill: rgba(255, 122, 78, .12); stroke: #ff7a4e; stroke-width: 1.5; stroke-dasharray: 3 2; animation: map-pulse 1s infinite; transform-box: fill-box; transform-origin: center; } +.aether-map-player-shot { fill: #f8ffff; stroke: #55e9ff; stroke-width: 1; } +.aether-map-enemy-shot { fill: #ff2518; stroke: #8f0b05; stroke-width: 1; filter: drop-shadow(0 0 3px rgba(255, 37, 24, .86)); } .map-room { stroke: #36554a; stroke-width: 2; } .map-ring { fill: none; stroke: #2b493f; stroke-width: 1; stroke-dasharray: 3 4; } .map-glyph { fill: none; stroke: #755f34; stroke-width: 1; opacity: 0.7; } .map-pillar { fill: #315247; stroke: #66877a; stroke-width: 1; } .map-boss { fill: #8c3929; stroke: #ff8a63; stroke-width: 2; filter: drop-shadow(0 0 5px #b74731); } +.map-boss.is-opponent { fill: #742c5c; stroke: #ff84bd; filter: drop-shadow(0 0 5px #b63f7b); } +.map-opponent { fill: #ef6c9f !important; stroke: #ffe2ee !important; } .map-boss-arrow { fill: #ff9f74; } .map-player-pulse { fill: rgba(237, 205, 116, 0.14); stroke: #e8c872; stroke-width: 1; animation: map-pulse 1.2s infinite; transform-box: fill-box; transform-origin: center; } .map-barrier { fill: rgba(237, 201, 83, 0.18); stroke: #f4d96f; stroke-width: 1.5; stroke-dasharray: 3 2; } @@ -1216,6 +1376,63 @@ button:focus-visible { padding: 4%; } +/* PVP opponent telemetry */ + +.pvp-panel { + height: 100%; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 3%; + padding: 3.2% 4%; + background: + radial-gradient(circle at 84% -8%, rgba(226, 73, 134, 0.12), transparent 38%), + linear-gradient(145deg, rgba(12, 28, 25, 0.3), rgba(13, 8, 18, 0.28)); +} + +.pvp-roster-header { + min-height: 58px; + display: grid; + grid-template-columns: minmax(150px, 1.8fr) repeat(3, minmax(72px, 0.72fr)); + align-items: stretch; + border: 1px solid rgba(239, 108, 159, 0.3); + border-left: 3px solid #ef6c9f; + background: linear-gradient(90deg, rgba(80, 22, 59, 0.42), rgba(10, 22, 20, 0.88)); + box-shadow: inset 0 0 22px rgba(239, 108, 159, 0.035); +} + +.pvp-roster-header > span, +.pvp-roster-header > div { min-width: 0; display: grid; align-content: center; gap: 3px; padding: 8px 12px; } +.pvp-roster-header > div { border-left: 1px solid rgba(239, 108, 159, 0.16); text-align: center; } +.pvp-roster-header small { color: #9b7086; font-size: clamp(6px, 1.3cqw, 8px); font-weight: 700; letter-spacing: 0.13em; text-transform: uppercase; } +.pvp-roster-header strong { overflow: hidden; color: #f6e9ef; font: 600 clamp(11px, 2.5cqw, 16px) "Cinzel", serif; text-overflow: ellipsis; white-space: nowrap; } +.pvp-roster-header > div strong { color: #ff9fc4; } + +.pvp-party-list { + min-height: 0; + display: grid; + grid-template-rows: 22px repeat(5, minmax(0, 1fr)); + gap: 1.5%; +} + +.pvp-party-frame { + min-height: 0; + display: grid; + grid-template-columns: 42px minmax(0, 1fr) 54px; + align-items: center; + gap: 9px; + padding: 4px 9px 4px 6px; + overflow: hidden; + border: 1px solid rgba(194, 124, 158, 0.18); + border-left: 2px solid rgba(239, 108, 159, 0.54); + border-radius: 4px; + background: linear-gradient(90deg, rgba(39, 17, 31, 0.9), rgba(9, 24, 20, 0.82)); +} + +.pvp-party-frame > b { color: #d697b4; font-size: clamp(8px, 1.7cqw, 10px); letter-spacing: 0.06em; text-align: right; } +.pvp-party-frame .health-bar > i { background: linear-gradient(90deg, #8e345f, #ef6c9f); } +.pvp-party-frame .health-bar > i.is-low { background: linear-gradient(90deg, #8d392d, #e45b45); } +.pvp-party-frame.is-down { filter: grayscale(0.85); opacity: 0.46; } + .item-list { min-width: 0; display: grid; @@ -1286,6 +1503,21 @@ button:focus-visible { to { transform: scale(1.5); opacity: 0; } } +@keyframes goal-popup-burst { + 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.55); } + 16% { opacity: 1; transform: translate(-50%, -50%) scale(1.08); } + 26% { transform: translate(-50%, -50%) scale(1); } + 76% { opacity: 1; transform: translate(-50%, -50%) scale(1); } + 100% { opacity: 0; transform: translate(-50%, -50%) scale(1.05); } +} + +@keyframes blockbreaker-score-burst { + 0% { opacity: 0; transform: translate(-50%, -42%) scale(.72); } + 20% { opacity: 1; transform: translate(-50%, -50%) scale(1.06); } + 72% { opacity: 1; transform: translate(-50%, -58%) scale(1); } + 100% { opacity: 0; transform: translate(-50%, -68%) scale(.98); } +} + @media (max-width: 760px) { .app-shell { padding: 10px 7px 45px; } .app-header { padding: 0 4px; } @@ -1311,6 +1543,9 @@ button:focus-visible { .encounter-callout { bottom: 8%; padding: 3px 5px; } .encounter-callout span { font-size: 5px; } .encounter-callout strong { font-size: 8px; } + .dampening-indicator { top: 15%; width: 106px; gap: 2px 5px; padding: 3px 4px 4px 6px; } + .dampening-indicator span { font-size: 5px; } + .dampening-indicator strong { font-size: 8px; } .casting-bar { bottom: 7.5%; min-width: 120px; padding: 3px 5px 4px; } .casting-bar strong, .casting-bar > b { font-size: 7px; } .casting-bar small { display: none; } @@ -1326,6 +1561,8 @@ button:focus-visible { animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; } + .goal-popup { animation: none !important; } + .blockbreaker-score-popup { animation: none !important; } } /* Capacitor single-display fallback. Thor dual-display routing will replace this @@ -1544,7 +1781,7 @@ button:focus-visible { opacity: 0.35; } -.front-surface button.is-controller-focused, +.front-surface button.is-controller-selected, .front-surface button:focus-visible { outline: 2px solid var(--gold-strong); outline-offset: 2px; @@ -1673,7 +1910,7 @@ button:focus-visible { .login-panel label { color: #81978e; font-size: 8px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; } .login-panel input { height: 36px; padding: 0 11px; border: 1px solid rgba(155,190,176,0.28); outline: 0; color: #e8f2ee; background: #071310; font: 600 12px "Rajdhani", sans-serif; } .login-panel input:focus, -.login-panel input.is-controller-focused { border-color: var(--gold); box-shadow: 0 0 0 2px rgba(232,200,114,0.12); } +.login-panel input.is-controller-selected { border-color: var(--gold); box-shadow: 0 0 0 2px rgba(232,200,114,0.12); } .login-panel .front-primary, .login-panel .front-secondary { min-height: 40px; padding-top: 6px; padding-bottom: 6px; } .login-surface > .front-notice { position: absolute; right: 44px; bottom: 83px; width: 300px; } @@ -1737,7 +1974,8 @@ button:focus-visible { .front-dialog form > span { color: var(--gold); font-size: 8px; font-weight: 700; letter-spacing: 0.15em; text-transform: uppercase; } .front-dialog form > label { justify-self: start; margin-top: 12px; color: #82978f; font-size: 8px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; } .front-dialog form > input { width: 100%; height: 38px; margin-top: 5px; padding: 0 11px; border: 1px solid rgba(155,190,176,0.32); outline: 0; color: #eef8f4; background: #06120f; font: 600 13px "Rajdhani", sans-serif; text-align: center; } -.front-dialog form > input:focus { border-color: var(--gold); box-shadow: 0 0 0 2px rgba(232,200,114,0.14); } +.front-dialog form > input:focus, +.front-dialog form > input.is-controller-selected { border-color: var(--gold); box-shadow: 0 0 0 2px rgba(232,200,114,0.14); } .front-dialog form > small { justify-self: end; margin-top: 3px; color: #647a71; font-size: 7px; } .front-dialog h2 { margin: 7px 0; font-family: "Cinzel", serif; font-size: 24px; font-weight: 500; } .front-dialog p { margin: 0; color: #8ca198; font-size: 11px; line-height: 1.4; } @@ -1797,9 +2035,13 @@ button:focus-visible { .home-header > span { margin-left: auto; color: #8fa39b; font-size: 10px; } .home-header > span b { color: #dce9e4; } .home-header > i { color: #6ecaa7; font-size: 8px; font-style: normal; font-weight: 700; letter-spacing: 0.08em; } -.mode-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-rows: repeat(2, 78px); gap: 10px; margin-top: 18px; } +.mode-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-rows: repeat(2, 78px); grid-auto-rows: 78px; gap: 10px; margin-top: 18px; } .mode-card { position: relative; display: grid; grid-template-columns: 54px 1fr 17px; align-items: center; gap: 12px; padding: 13px; overflow: hidden; text-align: left; } .mode-card::after { position: absolute; inset: 0; content: ""; background: linear-gradient(110deg, rgba(69,153,131,0.13), transparent 60%); pointer-events: none; } +.mode-card-blockbreaker::after { background: linear-gradient(110deg, rgba(54,217,239,.15), rgba(232,90,169,.1) 54%, rgba(147,219,84,.12)); } +.mode-card-blockbreaker > i { border-radius: 5px; color: #baf77e; box-shadow: inset 0 0 13px rgba(54,217,239,.09); } +.mode-card-aether-assault::after { background: linear-gradient(110deg, rgba(66, 222, 242, .18), rgba(100, 110, 225, .11) 62%, transparent); } +.mode-card-aether-assault > i { border-radius: 5px; color: #8cf5ff; box-shadow: inset 0 0 13px rgba(66, 222, 242, .1); } .mode-card.is-wide { grid-row: 1 / 3; } .mode-card > i { width: 48px; height: 48px; display: grid; place-items: center; border: 1px solid rgba(232,200,114,0.42); border-radius: 50%; color: var(--gold); background: rgba(2,9,7,0.45); font-family: "Cinzel", serif; font-size: 20px; font-style: normal; } .mode-card.is-wide > i { width: 64px; height: 64px; font-size: 27px; } @@ -1907,7 +2149,7 @@ button:focus-visible { .trophy-note { margin-top: 8px; } .boss-stats-heading { padding-top: 10px; } .boss-stats-layout { height: 331px; display: grid; grid-template-columns: minmax(235px, .78fr) minmax(0, 1.22fr); gap: 11px; } -.boss-stat-selector { min-width: 0; display: grid; align-content: start; gap: 5px; } +.boss-stat-selector { min-width: 0; display: grid; align-content: start; gap: 5px; overflow-y: auto; scrollbar-width: thin; } .boss-stat-selector button { width: 100%; min-height: 51px; display: grid; grid-template-columns: 30px minmax(0, 1fr) 32px; align-items: center; gap: 8px; padding: 6px 9px; text-align: left; } .boss-stat-selector button.is-selected { border-color: var(--gold); box-shadow: inset 3px 0 var(--gold); background: linear-gradient(90deg, rgba(93,73,23,.27), rgba(8,18,15,.88)); } .boss-stat-selector button > i { width: 27px; height: 27px; display: grid; place-items: center; border: 1px solid #496159; color: var(--gold); font-style: normal; } @@ -1920,9 +2162,11 @@ button:focus-visible { .leaderboard-panel > header span { min-width: 0; display: grid; } .leaderboard-panel > header small { color: var(--gold); font-size: 7px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; } .leaderboard-panel > header strong { overflow: hidden; font: 500 13px "Cinzel", serif; text-overflow: ellipsis; white-space: nowrap; } -.leaderboard-panel > header > b { color: #88a79b; font-size: 8px; text-transform: uppercase; } +.leaderboard-header-actions { display: flex; align-items: center; gap: 8px; } +.leaderboard-header-actions > b { color: #88a79b; font-size: 8px; text-transform: uppercase; white-space: nowrap; } +.leaderboard-refresh { min-height: 27px; padding: 4px 7px; color: var(--gold-strong); font-size: 7px; font-weight: 700; text-transform: uppercase; } .leaderboard-rows { min-height: 218px; padding: 4px 8px; } -.leaderboard-rows > div, .leaderboard-self { min-height: 40px; display: grid; grid-template-columns: 34px minmax(0, 1fr) 44px; align-items: center; gap: 8px; padding: 4px 7px; border-bottom: 1px solid rgba(164,195,184,.1); } +.leaderboard-rows > div, .leaderboard-self { min-height: 40px; display: grid; grid-template-columns: 34px minmax(0, 1fr) 64px; align-items: center; gap: 8px; padding: 4px 7px; border-bottom: 1px solid rgba(164,195,184,.1); } .leaderboard-rows > div.is-you { background: rgba(102,81,24,.2); } .leaderboard-rows > div > b, .leaderboard-self > b { color: var(--gold); font: 600 12px "Cinzel", serif; } .leaderboard-rows span, .leaderboard-self span { min-width: 0; display: grid; } @@ -1934,11 +2178,16 @@ button:focus-visible { .leaderboard-empty { min-height: 200px !important; border: 0 !important; } .profile-context { padding: 0 5.5% 18px; } .profile-context .context-header { margin: 0 -5.8%; } -.profile-stats { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); border-bottom: 1px solid var(--line); } +.profile-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-bottom: 1px solid var(--line); } .profile-stats span { min-width: 0; display: grid; padding: 8px 6px; border-right: 1px solid var(--line); } .profile-stats span:last-child { border-right: 0; } .profile-stats small { color: #6d8179; font-size: clamp(7px, 1.5cqw, 9px); text-transform: uppercase; } .profile-stats strong { color: var(--gold-strong); font-family: "Cinzel", serif; font-size: clamp(14px, 2.8cqw, 17px); font-weight: 500; } +.blockbreaker-profile-records { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 7px 0; border: 1px solid rgba(147,219,84,.22); background: linear-gradient(90deg, rgba(54,217,239,.07), rgba(232,90,169,.06), rgba(147,219,84,.07)); } +.blockbreaker-profile-records > span { min-width: 0; display: grid; padding: 6px 9px; border-right: 1px solid rgba(147,219,84,.15); } +.blockbreaker-profile-records > span:last-child { border-right: 0; } +.blockbreaker-profile-records small { color: #78968b; font-size: clamp(6px, 1.25cqw, 8px); letter-spacing: .07em; text-transform: uppercase; } +.blockbreaker-profile-records strong { overflow: hidden; color: #baf77e; font: 500 clamp(12px, 2.4cqw, 15px) "Cinzel", serif; text-overflow: ellipsis; white-space: nowrap; } .boss-log { margin-top: 7px; } .boss-log > span { color: #6c8179; font-size: 8px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; } .boss-log button { width: 100%; min-height: 36px; display: grid; grid-template-columns: 28px 1fr 30px; align-items: center; gap: 7px; margin-top: 3px; padding: 3px 8px; text-align: left; } @@ -1949,6 +2198,77 @@ button:focus-visible { .boss-log small { color: #687d75; font-size: clamp(7px, 1.45cqw, 9px); text-transform: uppercase; } .boss-log button > b { color: #85a198; font-size: 10px; } +.boss-index-panel, +.profile-records-panel { min-width: 0; height: 100%; overflow: hidden; } +.boss-index-panel { border: 1px solid #435950; background: linear-gradient(180deg, rgba(11,25,21,.96), rgba(5,13,11,.96)); } +.boss-index-panel > header, +.profile-records-panel > header { height: 43px; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px 10px; border: 1px solid #435950; background: linear-gradient(90deg, rgba(76,59,20,.25), rgba(8,20,17,.92)); } +.boss-index-panel > header { border-width: 0 0 1px; } +.boss-index-panel > header span, +.profile-records-panel > header span { min-width: 0; display: grid; } +.boss-index-panel > header small, +.profile-records-panel > header small { color: var(--gold); font-size: 7px; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; } +.boss-index-panel > header strong, +.profile-records-panel > header strong { overflow: hidden; font: 500 12px "Cinzel", serif; text-overflow: ellipsis; white-space: nowrap; } +.boss-index-panel > header > b, +.profile-records-panel > header > b { color: #8ca59c; font-size: 8px; letter-spacing: .06em; text-transform: uppercase; white-space: nowrap; } +.boss-pet-grid { height: calc(100% - 43px); display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); grid-template-rows: repeat(4, minmax(0, 1fr)); gap: 4px; padding: 5px; } +.boss-pet-card { --boss-accent: var(--gold); position: relative; min-width: 0; min-height: 0; display: grid; grid-template-rows: minmax(0, 1fr) auto auto; align-items: end; gap: 1px; overflow: hidden; padding: 5px 5px 4px; border-color: color-mix(in srgb, var(--boss-accent) 30%, #40544d); background: radial-gradient(circle at 48% 35%, color-mix(in srgb, var(--boss-accent) 17%, transparent), transparent 48%), rgba(6,16,13,.92); text-align: left; } +.boss-pet-card::after { content: ""; position: absolute; inset: 2px; border: 1px solid rgba(220,237,229,.045); pointer-events: none; } +.boss-pet-card.is-selected { border-color: color-mix(in srgb, var(--boss-accent) 72%, var(--gold)); box-shadow: inset 0 0 14px color-mix(in srgb, var(--boss-accent) 13%, transparent), 0 0 0 1px color-mix(in srgb, var(--boss-accent) 28%, transparent); } +.boss-pet-icon { align-self: center; justify-self: center; color: color-mix(in srgb, var(--boss-accent) 74%, #fff2c9); font: 500 23px "Cinzel", serif; filter: drop-shadow(0 2px 5px rgba(0,0,0,.65)); } +.boss-pet-card.is-unowned .boss-pet-icon { color: #819089; filter: grayscale(1); opacity: .58; } +.boss-kill-badge { position: absolute; top: 4px; right: 4px; z-index: 2; min-width: 27px; display: grid; padding: 2px 4px; border: 1px solid color-mix(in srgb, var(--boss-accent) 52%, #786e50); color: #fff0bb; background: rgba(3,9,7,.9); text-align: center; } +.boss-kill-badge small { color: #9a9d8d; font-size: 4px; font-weight: 700; letter-spacing: .1em; line-height: 1; text-transform: uppercase; } +.boss-kill-badge b { font: 600 10px "Cinzel", serif; line-height: 1.1; } +.boss-pet-card > strong { position: relative; z-index: 1; overflow: hidden; font-size: 7px; line-height: 1.05; text-overflow: ellipsis; white-space: nowrap; } +.boss-pet-card > small { position: relative; z-index: 1; overflow: hidden; color: color-mix(in srgb, var(--boss-accent) 55%, #74877f); font-size: 5px; line-height: 1.05; text-overflow: ellipsis; text-transform: uppercase; white-space: nowrap; } +.boss-pet-card.is-unowned > small { color: #68766f; } +.profile-records-panel { display: grid; grid-template-rows: 43px 68px minmax(0, 1fr); gap: 5px; } +.profile-metric-grid { min-width: 0; display: grid; gap: 5px; } +.profile-metric-grid.metric-count-1 { grid-template-columns: 1fr; } +.profile-metric-grid.metric-count-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.profile-metric-grid.metric-count-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.profile-metric-grid button { min-width: 0; display: grid; align-content: center; padding: 6px 9px; border-color: #40584f; background: linear-gradient(145deg, rgba(14,31,26,.94), rgba(6,16,13,.94)); text-align: left; } +.profile-metric-grid button.is-selected { border-color: var(--gold); box-shadow: inset 3px 0 var(--gold); background: linear-gradient(110deg, rgba(91,72,25,.29), rgba(8,20,17,.94)); } +.profile-metric-grid button > small { color: var(--gold); font-size: 6px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase; } +.profile-metric-grid button > strong { overflow: hidden; margin-top: 1px; color: #f0f5f2; font: 500 15px "Cinzel", serif; text-overflow: ellipsis; white-space: nowrap; } +.profile-metric-grid button > span { overflow: hidden; color: #758a82; font-size: 6px; text-overflow: ellipsis; text-transform: uppercase; white-space: nowrap; } +.profile-leaderboard { min-height: 0; display: grid; grid-template-rows: 39px minmax(0, 1fr) 35px; } +.profile-leaderboard > header { height: auto; min-height: 0; padding: 4px 8px; } +.profile-leaderboard .leaderboard-header-actions { gap: 5px; } +.profile-leaderboard .leaderboard-refresh { min-height: 22px; padding: 3px 6px; } +.profile-leaderboard .leaderboard-rows { min-height: 0; overflow: hidden; padding: 2px 6px; } +.profile-leaderboard .leaderboard-rows > div { min-height: 26px; grid-template-columns: 29px minmax(0, 1fr) 55px; gap: 6px; padding: 2px 5px; } +.profile-leaderboard .leaderboard-rows > div > b { font-size: 9px; } +.profile-leaderboard .leaderboard-rows strong { font-size: 7px; } +.profile-leaderboard .leaderboard-rows small { font-size: 5px; } +.profile-leaderboard .leaderboard-rows em { font-size: 11px; } +.profile-leaderboard .leaderboard-self { min-height: 0; margin: 0 6px 4px; grid-template-columns: 29px minmax(0, 1fr) 55px; gap: 6px; padding: 2px 5px; } +.profile-leaderboard .leaderboard-self > b { font-size: 9px; } +.profile-leaderboard .leaderboard-self strong { font-size: 7px; } +.profile-leaderboard .leaderboard-self small { font-size: 5px; } +.profile-leaderboard .leaderboard-self em { font-size: 11px; } +.profile-leaderboard .leaderboard-status, +.profile-leaderboard .leaderboard-empty { min-height: 0 !important; padding: 7px; font-size: 6px; } +.selected-boss-dossier { --boss-accent: var(--gold); min-height: 180px; display: grid; grid-template-columns: 145px minmax(0, 1fr); align-items: center; gap: 19px; margin-top: 17px; padding: 14px; border: 1px solid color-mix(in srgb, var(--boss-accent) 45%, #40564e); background: radial-gradient(circle at 13% 50%, color-mix(in srgb, var(--boss-accent) 18%, transparent), transparent 31%), linear-gradient(120deg, rgba(14,31,26,.96), rgba(6,15,12,.96)); } +.selected-boss-pet { height: 146px; display: grid; place-items: center; align-content: center; gap: 8px; border: 1px solid color-mix(in srgb, var(--boss-accent) 55%, #53645e); background: rgba(3,10,8,.62); } +.selected-boss-pet span { color: color-mix(in srgb, var(--boss-accent) 72%, #fff1bf); font: 500 55px "Cinzel", serif; filter: drop-shadow(0 5px 12px rgba(0,0,0,.7)); } +.selected-boss-pet small { color: color-mix(in srgb, var(--boss-accent) 60%, #82928c); font-size: 7px; letter-spacing: .13em; text-transform: uppercase; } +.selected-boss-pet.is-unowned span { color: #77857f; filter: grayscale(1); opacity: .56; } +.selected-boss-copy { min-width: 0; } +.selected-boss-copy > small { color: var(--boss-accent); font-size: 8px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; } +.selected-boss-copy h2 { margin: 5px 0 8px; font: 500 clamp(20px, 4.4cqw, 27px) "Cinzel", serif; } +.selected-boss-copy p { max-width: 390px; margin: 0; color: #91a69e; font-size: clamp(10px, 2.1cqw, 13px); line-height: 1.35; } +.selected-boss-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; margin-top: 10px; } +.selected-boss-stats span { min-width: 0; display: grid; min-height: 67px; align-content: center; padding: 8px 10px; border: 1px solid var(--line); background: rgba(6,17,14,.82); } +.selected-boss-stats small { color: #748a81; font-size: 7px; letter-spacing: .08em; text-transform: uppercase; } +.selected-boss-stats strong { overflow: hidden; color: var(--gold-strong); font: 500 clamp(13px, 2.7cqw, 17px) "Cinzel", serif; text-overflow: ellipsis; white-space: nowrap; } +.selected-boss-mechanics { display: grid; gap: 3px; margin-top: 10px; padding: 10px 12px; border-left: 2px solid var(--gold); background: rgba(61,48,16,.15); } +.selected-boss-mechanics span { color: var(--gold); font-size: 7px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; } +.selected-boss-mechanics strong { font-size: clamp(9px, 2cqw, 12px); font-weight: 600; } +.selected-boss-mechanics small { color: #72867f; font-size: clamp(7px, 1.5cqw, 9px); } + /* Settings */ .settings-surface { padding: 0 30px; } @@ -2000,6 +2320,7 @@ button:focus-visible { .mode-surface::after { position: absolute; right: -90px; bottom: -160px; width: 560px; height: 560px; z-index: -1; border: 1px solid rgba(232,200,114,0.13); border-radius: 50%; content: ""; box-shadow: inset 0 0 0 70px rgba(61,142,120,0.025), inset 0 0 0 140px rgba(232,200,114,0.018); } .mode-roguelike-pvp::after { border-color: rgba(192,99,92,0.2); } .mode-stadium-pvp::after { border-color: rgba(91,150,212,0.2); } +.mode-blockbreaker::after { border-color: rgba(147,219,84,.24); box-shadow: inset 0 0 0 70px rgba(54,217,239,.025), inset 0 0 0 140px rgba(232,90,169,.018); } .mode-hero { width: 56%; margin: 52px 0 0 22px; } .mode-hero > span { color: var(--gold); font-size: 9px; font-weight: 700; letter-spacing: 0.15em; text-transform: uppercase; } .mode-hero h2 { margin: 8px 0; font-family: "Cinzel", serif; font-size: 35px; font-weight: 500; line-height: 1; } @@ -2033,6 +2354,8 @@ button:focus-visible { .mode-launch { position: absolute; right: 38px; bottom: 18px; width: 275px; min-height: 52px; display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-color: var(--gold) !important; color: #182019 !important; background: linear-gradient(110deg, #ffe499, #d1af54) !important; text-align: left; } .mode-launch span { font-family: "Cinzel", serif; font-size: 13px; font-weight: 600; } .mode-launch small { font-size: 8px; font-weight: 700; text-transform: uppercase; opacity: 0.65; } +.mode-launch.is-queueing { border-color: #ff8eb9 !important; color: #fbe8ef !important; background: linear-gradient(110deg, #6f2348, #351525) !important; box-shadow: 0 0 18px rgba(239, 108, 159, 0.28); } +.mode-hockey-healing-pvp > .front-notice { right: 330px; bottom: 24px; width: min(360px, calc(100% - 400px)); } .mode-surface > .front-notice { position: absolute; right: 38px; bottom: 20px; width: 360px; } .mode-context { padding: 0 5.5% 18px; } .mode-context .context-header { margin: 0 -5.8%; } @@ -2083,6 +2406,8 @@ button:focus-visible { .force-reduced-motion *, .force-reduced-motion *::before, .force-reduced-motion *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; } +.force-reduced-motion .goal-popup { animation: none !important; } +.force-reduced-motion .blockbreaker-score-popup { animation: none !important; } @media (max-width: 760px) { .bottom-display { width: 100%; } @@ -2145,7 +2470,7 @@ button:focus-visible { .version-actions button small { font-size: 6px; } .home-header { height: 35px; } .home-header > span, .home-header > i { font-size: 5px; } - .mode-grid { grid-template-rows: repeat(2, 43px); gap: 5px; margin-top: 6px; } + .mode-grid { grid-template-rows: repeat(2, 43px); grid-auto-rows: 43px; gap: 5px; margin-top: 6px; } .mode-card { grid-template-columns: 25px 1fr 8px; gap: 4px; padding: 4px; } .mode-card > i, .mode-card.is-wide > i { width: 23px; height: 23px; font-size: 10px; } .mode-card strong, .mode-card.is-wide strong { font-size: 8px; } @@ -2202,7 +2527,9 @@ button:focus-visible { .leaderboard-panel > header { height: 35px; padding: 3px 6px; } .leaderboard-panel > header small { font-size: 4px; } .leaderboard-panel > header strong { font-size: 7px; } - .leaderboard-panel > header > b { font-size: 4px; } + .leaderboard-header-actions { gap: 3px; } + .leaderboard-header-actions > b { font-size: 4px; } + .leaderboard-refresh { min-height: 18px; padding: 2px 3px; font-size: 4px; } .leaderboard-rows { min-height: 139px; padding: 2px 4px; } .leaderboard-rows > div, .leaderboard-self { min-height: 25px; grid-template-columns: 20px minmax(0, 1fr) 24px; gap: 3px; padding: 2px 3px; } .leaderboard-rows > div > b, .leaderboard-self > b { font-size: 7px; } @@ -2212,6 +2539,45 @@ button:focus-visible { .leaderboard-self { min-height: 29px; margin: 0 4px 3px; } .leaderboard-status, .leaderboard-empty { min-height: 139px; padding: 7px; font-size: 5px; } .leaderboard-empty { min-height: 130px !important; } + .boss-index-panel > header, + .profile-records-panel > header { height: 26px; gap: 3px; padding: 2px 4px; } + .boss-index-panel > header small, + .profile-records-panel > header small { font-size: 3px; } + .boss-index-panel > header strong, + .profile-records-panel > header strong { font-size: 6px; } + .boss-index-panel > header > b, + .profile-records-panel > header > b { font-size: 3px; } + .boss-pet-grid { height: calc(100% - 26px); gap: 2px; padding: 2px; } + .boss-pet-card { gap: 0; padding: 2px; } + .boss-pet-card::after { inset: 1px; } + .boss-pet-icon { font-size: 12px; } + .boss-kill-badge { top: 2px; right: 2px; min-width: 15px; padding: 1px 2px; } + .boss-kill-badge small { font-size: 2px; } + .boss-kill-badge b { font-size: 5px; } + .boss-pet-card > strong { font-size: 4px; } + .boss-pet-card > small { font-size: 3px; } + .profile-records-panel { grid-template-rows: 26px 40px minmax(0, 1fr); gap: 2px; } + .profile-metric-grid { gap: 2px; } + .profile-metric-grid button { padding: 2px 4px; } + .profile-metric-grid button > small { font-size: 3px; } + .profile-metric-grid button > strong { font-size: 7px; } + .profile-metric-grid button > span { font-size: 3px; } + .profile-leaderboard { grid-template-rows: 25px minmax(0, 1fr) 25px; } + .profile-leaderboard > header { height: auto; padding: 2px 4px; } + .profile-leaderboard .leaderboard-refresh { min-height: 14px; padding: 1px 3px; } + .profile-leaderboard .leaderboard-rows { min-height: 0; padding: 1px 3px; } + .profile-leaderboard .leaderboard-rows > div { min-height: 18px; grid-template-columns: 16px minmax(0, 1fr) 27px; gap: 2px; padding: 1px 2px; } + .profile-leaderboard .leaderboard-rows > div > b, + .profile-leaderboard .leaderboard-self > b { font-size: 4px; } + .profile-leaderboard .leaderboard-rows strong, + .profile-leaderboard .leaderboard-self strong { font-size: 4px; } + .profile-leaderboard .leaderboard-rows small, + .profile-leaderboard .leaderboard-self small { font-size: 2px; } + .profile-leaderboard .leaderboard-rows em, + .profile-leaderboard .leaderboard-self em { font-size: 5px; } + .profile-leaderboard .leaderboard-self { min-height: 0; margin: 0 3px 2px; grid-template-columns: 16px minmax(0, 1fr) 27px; gap: 2px; padding: 1px 2px; } + .profile-leaderboard .leaderboard-status, + .profile-leaderboard .leaderboard-empty { min-height: 0 !important; padding: 3px; font-size: 3px; } .settings-layout { gap: 7px; padding-top: 7px; } .volume-setting { height: 98px; padding: 7px; } .volume-setting strong { font-size: 8px; } @@ -2249,12 +2615,13 @@ button:focus-visible { .mode-launch { right: 14px; bottom: 25px; width: 33%; min-height: 33px; padding: 5px 7px; } .mode-launch span { font-size: 7px; } .mode-launch small { font-size: 4px; } + .mode-hockey-healing-pvp > .front-notice { right: 37%; bottom: 27px; width: 36%; padding: 3px 5px; font-size: 5px; } .game-menu-button { padding: 3px 5px; font-size: 6px; } } /* Data-driven loot and gear workshop */ -.home-secondary-actions { grid-template-columns: repeat(3, 1fr); } +.home-secondary-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); } .collection-grid { grid-template-columns: repeat(3, 1fr); gap: 9px; } .collection-drop { height: 154px; padding: 8px 9px; } .collection-drop.rarity-epic { border-top-color: #a071c1; } @@ -2392,13 +2759,131 @@ button:focus-visible { .gear-passive-ability-filter button { min-height: 14px; padding: 1px 2px; font-size: 3px; } } +/* Appearance Lab */ + +.appearance-surface { padding: 0 24px 15px; } +.appearance-header { height: 58px; } +.appearance-preview-stage { + position: relative; + height: calc(100% - 91px); + margin-top: 8px; + overflow: hidden; + border: 1px solid rgba(151, 194, 178, .28); + background: #07100e; + box-shadow: inset 0 0 70px rgba(0, 0, 0, .4); +} +.appearance-preview-stage::after { + position: absolute; + inset: 7px; + z-index: 2; + border: 1px solid rgba(230, 207, 137, .1); + content: ""; + pointer-events: none; +} +.appearance-preview-stage > div:first-child, +.appearance-preview-stage canvas { width: 100% !important; height: 100% !important; } +.appearance-preview-loading { height: 100%; display: grid; place-content: center; gap: 6px; color: #7e948b; text-align: center; } +.appearance-preview-loading span { color: var(--gold); font-size: 25px; } +.appearance-preview-loading strong { font: 500 12px "Cinzel", serif; } +.appearance-preview-mode { + position: absolute; + top: 16px; + left: 17px; + z-index: 3; + display: grid; + gap: 2px; + padding: 7px 10px; + border-left: 2px solid #67c89e; + background: rgba(3, 12, 10, .84); + pointer-events: none; +} +.appearance-preview-mode span { color: #aee8d0; font-size: 8px; font-weight: 800; letter-spacing: .13em; } +.appearance-preview-mode small { color: #71877e; font-size: 7px; text-transform: uppercase; } +.appearance-preview-mode.is-legacy { border-color: #db9b65; } +.appearance-preview-mode.is-legacy span { color: #f0b47d; } +.appearance-preview-name { + position: absolute; + right: 18px; + bottom: 16px; + z-index: 3; + min-width: 195px; + display: grid; + justify-items: end; + padding: 9px 12px; + border-right: 2px solid var(--gold); + background: linear-gradient(90deg, transparent, rgba(3, 11, 9, .9) 28%); + pointer-events: none; +} +.appearance-preview-name small { color: #81968e; font-size: 7px; letter-spacing: .12em; text-transform: uppercase; } +.appearance-preview-name strong { font: 500 22px "Cinzel", serif; } +.appearance-preview-name span { margin-top: 2px; color: var(--gold); font-size: 7px; font-weight: 800; letter-spacing: .12em; } +.appearance-top-hint { height: 25px; display: flex; align-items: end; justify-content: space-between; color: #71867e; font-size: 7px; text-transform: uppercase; } +.appearance-top-hint b { color: var(--gold-strong); } + +.appearance-context { padding: 0 12px 9px; } +.appearance-context .context-header { height: 40px; margin: 0 -12px; padding: 0 12px; } +.appearance-context .context-header b { color: var(--gold-strong); } +.appearance-class-tabs { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 4px; margin-top: 6px; } +.appearance-class-tabs button { min-width: 0; min-height: 38px; display: grid; grid-template-columns: 18px minmax(0, 1fr); align-items: center; gap: 4px; padding: 4px 6px; color: #81978e; font-size: 8px; text-align: left; } +.appearance-class-tabs button i { display: grid; place-items: center; font-size: 10px; font-style: normal; } +.appearance-class-tabs button span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.appearance-class-tabs button.is-selected { border-color: var(--gold); color: #edf8f3; background: rgba(73, 59, 21, .3); } +.appearance-animation-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; margin-top: 5px; } +.appearance-animation-tabs button { min-height: 29px; padding: 4px; color: #778c84; font-size: 8px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; } +.appearance-animation-tabs button.is-selected { border-color: #60b997; color: #9ae0c2; background: rgba(39, 101, 78, .2); } +.appearance-slot-list { display: grid; gap: 3px; margin-top: 5px; } +.appearance-slot-row { + min-height: 36px; + display: grid; + grid-template-columns: 132px 31px minmax(0, 1fr) 31px; + align-items: center; + gap: 4px; + padding: 3px 4px 3px 8px; + border: 1px solid rgba(139, 178, 164, .15); + background: rgba(5, 16, 13, .72); +} +.appearance-slot-row.is-controller-active { border-color: rgba(232, 200, 114, .42); background: linear-gradient(90deg, rgba(77, 62, 22, .22), rgba(5, 16, 13, .78)); } +.appearance-slot-row > span { min-width: 0; display: grid; } +.appearance-slot-row > span strong { color: #dce9e4; font-size: 9px; } +.appearance-slot-row > span small { overflow: hidden; color: #647a71; font-size: 6px; text-overflow: ellipsis; text-transform: uppercase; white-space: nowrap; } +.appearance-slot-row > b { overflow: hidden; color: var(--gold-strong); font: 500 9px "Cinzel", serif; text-align: center; text-overflow: ellipsis; white-space: nowrap; } +.appearance-slot-row button { min-width: 0; height: 28px; padding: 0; color: #c9d8d2; font-size: 18px; line-height: 1; } +.appearance-slot-row.is-disabled { opacity: .55; } +.appearance-slot-row.is-disabled > b { color: #8f9c97; } +.appearance-actions { display: grid; grid-template-columns: 1.25fr .75fr 1fr .75fr; gap: 4px; margin-top: 6px; } +.appearance-actions button { min-width: 0; min-height: 42px; display: grid; align-content: center; padding: 5px 7px; text-align: left; } +.appearance-actions strong { overflow: hidden; font-size: 8px; text-overflow: ellipsis; white-space: nowrap; } +.appearance-actions small { color: #687d75; font-size: 6px; text-transform: uppercase; } +.appearance-actions button.is-primary { border-color: var(--gold); color: #172019; background: linear-gradient(110deg, #ffe499, #cfaa4c); } +.appearance-actions button.is-primary small { color: #615129; } +.appearance-save-state { margin-top: 5px; padding: 5px 8px; border-left: 2px solid #5fc197; color: #81978e; background: rgba(5, 16, 13, .72); font-size: 7px; } +.appearance-save-state.is-dirty { border-color: var(--gold); color: #d8bd72; } + +@media (max-width: 760px) { + .home-secondary-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .appearance-surface { padding: 0 12px 8px; } + .appearance-header { height: 44px; } + .appearance-preview-stage { height: calc(100% - 68px); margin-top: 5px; } + .appearance-preview-name { right: 10px; bottom: 10px; min-width: 145px; padding: 6px 8px; } + .appearance-preview-name strong { font-size: 15px; } + .appearance-preview-mode { top: 10px; left: 10px; padding: 5px 7px; } + .appearance-top-hint { height: 18px; font-size: 5px; } + .appearance-context { padding: 0 8px 6px; } + .appearance-context .context-header { margin: 0 -8px; padding: 0 8px; } + .appearance-class-tabs button { padding: 3px 4px; font-size: 7px; } + .appearance-slot-row { grid-template-columns: 122px 29px minmax(0, 1fr) 29px; min-height: 34px; padding: 2px 3px 2px 7px; } + .appearance-slot-row button { height: 27px; } + .appearance-slot-row > span strong { font-size: 8px; } + .appearance-slot-row > b { font-size: 8px; } +} + .reward-summary { display: grid; gap: 4px; margin: 8px 0; } .reward-summary > span { padding: 5px 7px; border: 1px solid rgba(232,200,114,.25); color: #dbe8e3; background: rgba(68,54,18,.17); font-size: 8px; } .reward-summary b { margin-right: 6px; color: var(--gold); } .reward-summary i { color: #d79b42; font-style: normal; } @media (max-width: 760px) { - .home-secondary-actions { grid-template-columns: repeat(3, 1fr); } + .home-secondary-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); } .collection-grid { grid-template-columns: repeat(3, 1fr); } .collection-drop { height: 96px; padding: 4px 5px; } .collection-drop .drop-icon { width: 25px; height: 25px; margin: 1px auto 2px; font-size: 10px; } @@ -2407,6 +2892,10 @@ button:focus-visible { .collection-note { margin-top: 3px; padding: 2px 4px; } .profile-stats span { padding: 5px 7px; } .profile-stats strong { font-size: 12px; } + .blockbreaker-profile-records { margin: 3px 0; } + .blockbreaker-profile-records > span { padding: 3px 5px; } + .blockbreaker-profile-records small { font-size: 4px; } + .blockbreaker-profile-records strong { font-size: 8px; } .boss-log { margin-top: 5px; } .boss-log button { min-height: 40px; gap: 5px; margin-top: 3px; padding: 3px 6px; } .boss-log button > i { width: 23px; height: 23px; }
  • `t6(d% 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 Pp}&88+zzIMLYw~wQ+^RO literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/shield_square.glb b/src/assets/game/models/claudecraft/weapons/shield_square.glb new file mode 100644 index 0000000000000000000000000000000000000000..1614b511d53222620e8060881d487c48c1a3ab33 GIT binary patch literal 9660 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 literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/skeleton_axe.glb b/src/assets/game/models/claudecraft/weapons/skeleton_axe.glb new file mode 100644 index 0000000000000000000000000000000000000000..60de9959a329dc9cec706ecfc6e21b898ff6e1c8 GIT binary patch literal 14420 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%0DTK~z%i;dKe?lS?&H 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 RG85qQUtZLt%TAE^e*hu20v7-P literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/skeleton_blade.glb b/src/assets/game/models/claudecraft/weapons/skeleton_blade.glb new file mode 100644 index 0000000000000000000000000000000000000000..9367df572c22b93fb50ddd41b28f628df018a4ce GIT binary patch literal 12988 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 O7)1EDmtpFI0PtV2vp1&z literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/skeleton_crossbow.glb b/src/assets/game/models/claudecraft/weapons/skeleton_crossbow.glb new file mode 100644 index 0000000000000000000000000000000000000000..8493a928fb3e61309f56624101dcc270205f3953 GIT binary patch literal 15812 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 VdIw`*h(dq*A1=Eyd28YD{{mt~+ByIL literal 0 HcmV?d00001 diff --git a/src/assets/game/models/claudecraft/weapons/skeleton_shield_large_a.glb b/src/assets/game/models/claudecraft/weapons/skeleton_shield_large_a.glb new file mode 100644 index 0000000000000000000000000000000000000000..bef369e708722380bfc6fe63fe234acae4aadafe GIT binary patch literal 17372 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