@loonylabs/create-game 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/bin/create-game.js +213 -0
  2. package/package.json +18 -0
  3. package/template/.claude/skills/aigdtk-create-game-stories/SKILL.md +177 -0
  4. package/template/.claude/skills/aigdtk-create-game-stories/story-template.md +85 -0
  5. package/template/.claude/skills/aigdtk-implement-game-stories/SKILL.md +129 -0
  6. package/template/.claude/skills/aigdtk-new-game/SKILL.md +126 -0
  7. package/template/.claude/skills/aigdtk-shared/ascii-grammar.md +133 -0
  8. package/template/.claude/skills/aigdtk-shared/enemies.md +112 -0
  9. package/template/.claude/skills/aigdtk-shared/framework.md +93 -0
  10. package/template/.claude/skills/aigdtk-shared/visuals.md +125 -0
  11. package/template/apps/client/index.html +14 -0
  12. package/template/apps/client/package.json +31 -0
  13. package/template/apps/client/public/assets/audio/enemy_killed.wav +0 -0
  14. package/template/apps/client/src/components/App.svelte +290 -0
  15. package/template/apps/client/src/components/CraftingPanel.svelte +253 -0
  16. package/template/apps/client/src/components/DevPanel.svelte +180 -0
  17. package/template/apps/client/src/components/DungeonClearOverlay.svelte +53 -0
  18. package/template/apps/client/src/components/EquipmentPanel.svelte +191 -0
  19. package/template/apps/client/src/components/HealthBar.svelte +50 -0
  20. package/template/apps/client/src/components/Hub/BackToHubButton.svelte +37 -0
  21. package/template/apps/client/src/components/Hub/ExperienceCard.svelte +115 -0
  22. package/template/apps/client/src/components/Hub/Hub.svelte +88 -0
  23. package/template/apps/client/src/components/Inventory.svelte +174 -0
  24. package/template/apps/client/src/components/Runner/RunnerDeathScreen.svelte +182 -0
  25. package/template/apps/client/src/components/Runner/RunnerHUD.svelte +157 -0
  26. package/template/apps/client/src/components/Shooter/DamageNumbers.svelte +96 -0
  27. package/template/apps/client/src/components/Shooter/GameOverScreen.svelte +109 -0
  28. package/template/apps/client/src/components/Shooter/ShooterHUD.svelte +95 -0
  29. package/template/apps/client/src/components/SkillBar.svelte +146 -0
  30. package/template/apps/client/src/components/ToastSystem.svelte +158 -0
  31. package/template/apps/client/src/game.ts +918 -0
  32. package/template/apps/client/src/input.ts +92 -0
  33. package/template/apps/client/src/lib/audio/CombatDetector.test.ts +59 -0
  34. package/template/apps/client/src/lib/audio/CombatDetector.ts +53 -0
  35. package/template/apps/client/src/lib/audio/MusicManager.ts +137 -0
  36. package/template/apps/client/src/lib/audio/SoundManager.ts +59 -0
  37. package/template/apps/client/src/lib/audio/index.ts +9 -0
  38. package/template/apps/client/src/main.ts +32 -0
  39. package/template/apps/client/src/renderer/basecamp.ts +126 -0
  40. package/template/apps/client/src/renderer/dungeon.ts +250 -0
  41. package/template/apps/client/src/renderer/dungeonPortal.ts +73 -0
  42. package/template/apps/client/src/renderer/dungeonZone.ts +301 -0
  43. package/template/apps/client/src/renderer/entities.ts +197 -0
  44. package/template/apps/client/src/renderer/runnerTrack.ts +221 -0
  45. package/template/apps/client/src/renderer/shaders/SkyShader.ts +190 -0
  46. package/template/apps/client/src/renderer/shaders/TerrainShaderMaterial.ts +133 -0
  47. package/template/apps/client/src/renderer/shaders/floor.frag.glsl.ts +17 -0
  48. package/template/apps/client/src/renderer/shaders/shaderConfig.ts +18 -0
  49. package/template/apps/client/src/renderer/shaders/spawn.frag.glsl.ts +19 -0
  50. package/template/apps/client/src/renderer/shaders/terrain.frag.glsl.ts +314 -0
  51. package/template/apps/client/src/renderer/shaders/terrain.vert.glsl.ts +16 -0
  52. package/template/apps/client/src/renderer/shaders/wall.frag.glsl.ts +20 -0
  53. package/template/apps/client/src/renderer/shooterArena.ts +102 -0
  54. package/template/apps/client/src/renderer/voxelChunkStreamer.ts +79 -0
  55. package/template/apps/client/src/renderer/voxelMesh.ts +86 -0
  56. package/template/apps/client/src/renderer/voxelTerrain.ts +74 -0
  57. package/template/apps/client/src/socket.ts +268 -0
  58. package/template/apps/client/src/store.ts +74 -0
  59. package/template/apps/client/src/style.css +60 -0
  60. package/template/apps/client/tsconfig.json +11 -0
  61. package/template/apps/client/vite.config.ts +10 -0
  62. package/template/apps/client/vitest.config.ts +8 -0
  63. package/template/apps/experiences/diablo/index.ts +94 -0
  64. package/template/apps/experiences/diablo/systems/dungeonClearSystem.ts +60 -0
  65. package/template/apps/experiences/diablo/systems/enemyAISystem.ts +11 -0
  66. package/template/apps/experiences/diablo/systems/entitySyncSystem.ts +80 -0
  67. package/template/apps/experiences/diablo/systems/itemPickupSystem.ts +11 -0
  68. package/template/apps/experiences/diablo/systems/movementSystem.ts +13 -0
  69. package/template/apps/experiences/diablo/systems/physicsSystem.ts +92 -0
  70. package/template/apps/experiences/diablo/systems/transitionSystem.ts +105 -0
  71. package/template/apps/experiences/runner/data/runner-config.ts +54 -0
  72. package/template/apps/experiences/runner/index.ts +143 -0
  73. package/template/apps/experiences/runner/systems/collectibleSystem.ts +157 -0
  74. package/template/apps/experiences/runner/systems/deathSystem.ts +42 -0
  75. package/template/apps/experiences/runner/systems/entitySyncSystem.ts +59 -0
  76. package/template/apps/experiences/runner/systems/obstacleSystem.ts +91 -0
  77. package/template/apps/experiences/runner/systems/runnerPhysicsSystem.ts +82 -0
  78. package/template/apps/experiences/runner/systems/trackStreamSystem.ts +19 -0
  79. package/template/apps/experiences/runner/track/laneSystem.ts +53 -0
  80. package/template/apps/experiences/runner/track/segmentTypes.ts +141 -0
  81. package/template/apps/experiences/runner/track/trackGenerator.ts +292 -0
  82. package/template/apps/experiences/shooter/ai/aiStateMachine.ts +394 -0
  83. package/template/apps/experiences/shooter/ai/lineOfSight.ts +32 -0
  84. package/template/apps/experiences/shooter/arena/arenaGenerator.ts +101 -0
  85. package/template/apps/experiences/shooter/arena/arenaTypes.ts +49 -0
  86. package/template/apps/experiences/shooter/arena/hitscan.ts +101 -0
  87. package/template/apps/experiences/shooter/data/enemy-types.ts +108 -0
  88. package/template/apps/experiences/shooter/data/wave-definitions.ts +40 -0
  89. package/template/apps/experiences/shooter/data/weapon-config.ts +80 -0
  90. package/template/apps/experiences/shooter/index.ts +127 -0
  91. package/template/apps/experiences/shooter/systems/enemyAISystem.ts +113 -0
  92. package/template/apps/experiences/shooter/systems/entitySyncSystem.ts +68 -0
  93. package/template/apps/experiences/shooter/systems/shooterPhysicsSystem.ts +89 -0
  94. package/template/apps/experiences/shooter/systems/waveSpawnerSystem.ts +87 -0
  95. package/template/apps/experiences/shooter/systems/weaponSystem.ts +157 -0
  96. package/template/apps/game-data/src/areas/area-manifest.json +18 -0
  97. package/template/apps/game-data/src/assets/migration.test.ts +291 -0
  98. package/template/apps/game-data/src/audio/music-config.json +21 -0
  99. package/template/apps/game-data/src/audio/sound-config.json +11 -0
  100. package/template/apps/game-data/src/combat/action-types.ts +2 -0
  101. package/template/apps/game-data/src/combat/enemy-def.ts +12 -0
  102. package/template/apps/game-data/src/combat/hitboxes.ts +23 -0
  103. package/template/apps/game-data/src/dungeon/cell-types.ts +20 -0
  104. package/template/apps/game-data/src/dungeon/cell-visuals.ts +13 -0
  105. package/template/apps/game-data/src/dungeon/door-directions.ts +2 -0
  106. package/template/apps/game-data/src/enemies/enemy-defs.json +32 -0
  107. package/template/apps/game-data/src/equipment/slots.json +5 -0
  108. package/template/apps/game-data/src/events/event-defs.ts +20 -0
  109. package/template/apps/game-data/src/events/event-types.ts +10 -0
  110. package/template/apps/game-data/src/events/toast-config.json +49 -0
  111. package/template/apps/game-data/src/items/item-pool.json +13 -0
  112. package/template/apps/game-data/src/loot/item-pool.ts +14 -0
  113. package/template/apps/game-data/src/loot/rarities.ts +2 -0
  114. package/template/apps/game-data/src/physics/dungeon-physics-config.ts +12 -0
  115. package/template/apps/game-data/src/physics/jump-config.ts +17 -0
  116. package/template/apps/game-data/src/recipes/recipe-book.json +68 -0
  117. package/template/apps/game-data/src/rooms/room_basecamp.json +16 -0
  118. package/template/apps/game-data/src/rooms/room_corridor_ew.json +9 -0
  119. package/template/apps/game-data/src/rooms/room_corridor_ns.json +11 -0
  120. package/template/apps/game-data/src/rooms/room_crossroads.json +11 -0
  121. package/template/apps/game-data/src/rooms/room_dead_end.json +10 -0
  122. package/template/apps/game-data/src/rooms/room_staircase.json +12 -0
  123. package/template/apps/game-data/src/rooms/room_start.json +11 -0
  124. package/template/apps/game-data/src/skills/skill-book.json +20 -0
  125. package/template/apps/game-data/src/voxel/biome-terrain.ts +76 -0
  126. package/template/apps/game-data/src/voxel/materials.ts +45 -0
  127. package/template/apps/game-data/src/voxel/sandbox-terrain-config.ts +19 -0
  128. package/template/apps/game-data/src/world/area-config.ts +33 -0
  129. package/template/apps/game-data/src/world/biome-def.ts +15 -0
  130. package/template/apps/game-data/src/world/biomes.json +57 -0
  131. package/template/apps/game-data/src/world/movement.ts +2 -0
  132. package/template/apps/game-data/src/world/overworld-layout.test.ts +93 -0
  133. package/template/apps/game-data/src/world/overworld-layout.ts +127 -0
  134. package/template/apps/server/data/game.db +0 -0
  135. package/template/apps/server/package.json +30 -0
  136. package/template/apps/server/src/areaManager.ts +346 -0
  137. package/template/apps/server/src/db/client.ts +45 -0
  138. package/template/apps/server/src/db/schema.ts +40 -0
  139. package/template/apps/server/src/gameLoop.ts +267 -0
  140. package/template/apps/server/src/gameState.ts +3 -0
  141. package/template/apps/server/src/handlers/actionEvent.ts +55 -0
  142. package/template/apps/server/src/handlers/craftHandler.ts +59 -0
  143. package/template/apps/server/src/handlers/equipHandler.ts +73 -0
  144. package/template/apps/server/src/handlers/raycastHandler.ts +97 -0
  145. package/template/apps/server/src/handlers/skillHandler.ts +87 -0
  146. package/template/apps/server/src/handlers/terraformHandler.ts +74 -0
  147. package/template/apps/server/src/index.ts +597 -0
  148. package/template/apps/server/src/persistence.ts +135 -0
  149. package/template/apps/server/src/rooms.ts +20 -0
  150. package/template/apps/server/src/systems/dungeonPhysics.test.ts +32 -0
  151. package/template/apps/server/src/systems/dungeonPhysics.ts +16 -0
  152. package/template/apps/server/src/systems/enemyAI.ts +129 -0
  153. package/template/apps/server/src/systems/itemPickup.ts +31 -0
  154. package/template/apps/server/src/tests/areaManager.test.ts +77 -0
  155. package/template/apps/server/src/tests/diablo-experience.test.ts +60 -0
  156. package/template/apps/server/src/tests/runner-experience.test.ts +273 -0
  157. package/template/apps/server/src/tests/runner-powerups-scoring.test.ts +221 -0
  158. package/template/apps/server/src/tests/server.integration.test.ts +92 -0
  159. package/template/apps/server/src/tests/shooter-enemy-ai.test.ts +328 -0
  160. package/template/apps/server/src/tests/shooter-experience.test.ts +281 -0
  161. package/template/apps/server/src/tests/voxelChunkCache.test.ts +29 -0
  162. package/template/apps/server/src/tests/voxelSandbox.test.ts +133 -0
  163. package/template/apps/server/src/voxelChunkCache.ts +31 -0
  164. package/template/apps/server/src/voxelPlayerState.ts +23 -0
  165. package/template/apps/server/tsconfig.json +17 -0
  166. package/template/apps/server/vitest.config.ts +8 -0
@@ -0,0 +1,290 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import HealthBar from './HealthBar.svelte';
4
+ import Inventory from './Inventory.svelte';
5
+ import CraftingPanel from './CraftingPanel.svelte';
6
+ import EquipmentPanel from './EquipmentPanel.svelte';
7
+ import ToastSystem from './ToastSystem.svelte';
8
+ import DevPanel from './DevPanel.svelte';
9
+ import DungeonClearOverlay from './DungeonClearOverlay.svelte';
10
+ import SkillBar from './SkillBar.svelte';
11
+ import Hub from './Hub/Hub.svelte';
12
+ import BackToHubButton from './Hub/BackToHubButton.svelte';
13
+ import ShooterHUD from './Shooter/ShooterHUD.svelte';
14
+ import DamageNumbers from './Shooter/DamageNumbers.svelte';
15
+ import GameOverScreen from './Shooter/GameOverScreen.svelte';
16
+ import RunnerHUD from './Runner/RunnerHUD.svelte';
17
+ import RunnerDeathScreen from './Runner/RunnerDeathScreen.svelte';
18
+ import { valtioToSvelte } from '@loonylabs/gamedev-client';
19
+ import { store } from '../store.js';
20
+ import { sendMessage } from '../socket.js';
21
+ import type { GameMode } from '../store.js';
22
+
23
+ let inventoryOpen = false;
24
+ let craftingOpen = false;
25
+ let equipmentOpen = false;
26
+ let devPanelOpen = false;
27
+ const connected = valtioToSvelte(store, s => s.connected);
28
+ const currentMode = valtioToSvelte(store, s => s.gameMode);
29
+ const inHub = valtioToSvelte(store, s => s.currentExperience === null);
30
+ const isShooter = valtioToSvelte(store, s => s.currentExperience === 'shooter');
31
+ const shooterDead = valtioToSvelte(store, s => s.shooterDead);
32
+ const isRunner = valtioToSvelte(store, s => s.currentExperience === 'runner');
33
+ const runnerDead = valtioToSvelte(store, s => s.runnerDead);
34
+
35
+ function requestNewDungeon() {
36
+ sendMessage({ type: 'REQUEST_NEW_DUNGEON' });
37
+ }
38
+
39
+ const MODES: { id: GameMode; label: string; hint: string }[] = [
40
+ { id: 'click+orbit', label: 'Diablo', hint: 'Click to move · Space/RMB = attack' },
41
+ { id: 'wasd+thirdperson', label: 'Third Person', hint: 'WASD · RMB drag = rotate cam · E = attack' },
42
+ { id: 'wasd+firstperson', label: 'FPS', hint: 'WASD · Click = pointer lock · E = attack · Esc = unlock' },
43
+ { id: 'wasd+follow', label: 'Follow', hint: 'WASD · Scroll = zoom FPS↔TPS · RMB drag = rotate' },
44
+ ];
45
+
46
+ function setMode(mode: GameMode) {
47
+ store.gameMode = mode;
48
+ }
49
+
50
+ function returnToHub() {
51
+ sendMessage({ type: 'EXPERIENCE_LEAVE' });
52
+ store.currentExperience = null;
53
+ store.experienceLoading = false;
54
+ }
55
+
56
+ function onKeyDown(e: KeyboardEvent) {
57
+ if (e.code === 'KeyI') inventoryOpen = !inventoryOpen;
58
+ if (e.code === 'KeyC') craftingOpen = !craftingOpen;
59
+ if (e.code === 'KeyU') equipmentOpen = !equipmentOpen;
60
+ if (e.code === 'F1' && e.shiftKey) { devPanelOpen = !devPanelOpen; e.preventDefault(); }
61
+ if (e.code === 'Escape') {
62
+ if (inventoryOpen || craftingOpen || equipmentOpen) {
63
+ inventoryOpen = false; craftingOpen = false; equipmentOpen = false;
64
+ } else if (!$inHub) {
65
+ returnToHub();
66
+ }
67
+ }
68
+ }
69
+
70
+ onMount(() => window.addEventListener('keydown', onKeyDown));
71
+ onDestroy(() => window.removeEventListener('keydown', onKeyDown));
72
+ </script>
73
+
74
+ <!-- Hub overlay (shown when no experience is active) -->
75
+ {#if $inHub}
76
+ <Hub />
77
+ {:else}
78
+ <!-- Back to Hub button -->
79
+ <BackToHubButton />
80
+
81
+ <!-- Connection status (top-left) -->
82
+ <div class="status {$connected ? 'connected' : 'disconnected'}">
83
+ {$connected ? 'Connected' : 'Disconnected'}
84
+ </div>
85
+
86
+ <!-- Mode switcher (top-right) -->
87
+ <div class="mode-switcher">
88
+ <button class="mode-btn regen-btn" on:click={requestNewDungeon} title="Regenerate Dungeon">Regen</button>
89
+ {#each MODES as m}
90
+ <button
91
+ class="mode-btn {$currentMode === m.id ? 'active' : ''}"
92
+ on:click={() => setMode(m.id)}
93
+ title={m.hint}
94
+ >{m.label}</button>
95
+ {/each}
96
+ </div>
97
+
98
+ <!-- Active mode hint (below buttons) -->
99
+ <div class="mode-hint">{MODES.find(m => m.id === $currentMode)?.hint ?? ''}</div>
100
+
101
+ <!-- Skill Bar (bottom-center, above health) -->
102
+ <SkillBar />
103
+
104
+ <!-- Health bar (bottom-center) -->
105
+ <div class="healthbar-anchor">
106
+ <HealthBar />
107
+ </div>
108
+
109
+ <!-- Inventory (center, toggled with I) -->
110
+ <Inventory bind:visible={inventoryOpen} />
111
+
112
+ <!-- Crafting Panel (toggled with C) -->
113
+ <CraftingPanel bind:visible={craftingOpen} />
114
+
115
+ <!-- Equipment Panel (toggled with E) -->
116
+ <EquipmentPanel bind:visible={equipmentOpen} />
117
+
118
+ <!-- Crosshair (visible in FPS/WASD modes) -->
119
+ {#if $currentMode.startsWith('wasd')}
120
+ <div class="crosshair"></div>
121
+ {/if}
122
+
123
+ <!-- Shooter HUD + Damage Numbers -->
124
+ {#if $isShooter}
125
+ <ShooterHUD />
126
+ <DamageNumbers />
127
+ {#if $shooterDead}
128
+ <GameOverScreen />
129
+ {/if}
130
+ {/if}
131
+
132
+ <!-- Runner HUD + Death Screen -->
133
+ {#if $isRunner}
134
+ <RunnerHUD />
135
+ {#if $runnerDead}
136
+ <RunnerDeathScreen />
137
+ {/if}
138
+ {/if}
139
+
140
+ <!-- Toast notification system (always visible) -->
141
+ <ToastSystem />
142
+
143
+ <!-- Dungeon Clear countdown overlay -->
144
+ <DungeonClearOverlay />
145
+
146
+ <!-- Dev Panel (Shift+F1 to toggle) -->
147
+ {#if devPanelOpen}
148
+ <DevPanel />
149
+ {/if}
150
+
151
+ <!-- Hint -->
152
+ {#if !inventoryOpen && !craftingOpen && !equipmentOpen}
153
+ <div class="hint">I = Inventory · C = Crafting · U = Equipment · ESC = Hub</div>
154
+ {/if}
155
+ {/if}
156
+
157
+ <style>
158
+ .status {
159
+ position: absolute;
160
+ top: 12px;
161
+ left: 12px;
162
+ padding: 4px 10px;
163
+ border-radius: 3px;
164
+ letter-spacing: 0.05em;
165
+ text-transform: uppercase;
166
+ font-size: 0.75rem;
167
+ font-family: 'Courier New', monospace;
168
+ pointer-events: none;
169
+ }
170
+
171
+ .status.connected {
172
+ background: rgba(68, 204, 68, 0.2);
173
+ color: #44cc44;
174
+ border: 1px solid #44cc44;
175
+ }
176
+
177
+ .status.disconnected {
178
+ background: rgba(204, 68, 68, 0.2);
179
+ color: #cc4444;
180
+ border: 1px solid #cc4444;
181
+ }
182
+
183
+ .healthbar-anchor {
184
+ position: absolute;
185
+ bottom: 24px;
186
+ left: 50%;
187
+ transform: translateX(-50%);
188
+ pointer-events: none;
189
+ }
190
+
191
+ .hint {
192
+ position: absolute;
193
+ bottom: 8px;
194
+ right: 12px;
195
+ color: #555;
196
+ font-size: 0.65rem;
197
+ letter-spacing: 0.05em;
198
+ pointer-events: none;
199
+ }
200
+
201
+ .mode-switcher {
202
+ position: absolute;
203
+ top: 12px;
204
+ right: 12px;
205
+ display: flex;
206
+ gap: 6px;
207
+ pointer-events: auto;
208
+ }
209
+
210
+ .mode-btn {
211
+ padding: 4px 12px;
212
+ background: rgba(0, 0, 0, 0.6);
213
+ color: #888;
214
+ border: 1px solid #333;
215
+ border-radius: 3px;
216
+ font-family: 'Courier New', monospace;
217
+ font-size: 0.7rem;
218
+ letter-spacing: 0.05em;
219
+ cursor: pointer;
220
+ text-transform: uppercase;
221
+ }
222
+
223
+ .mode-btn:hover {
224
+ border-color: #666;
225
+ color: #aaa;
226
+ }
227
+
228
+ .mode-btn.active {
229
+ background: rgba(68, 136, 204, 0.25);
230
+ border-color: #4488cc;
231
+ color: #4488cc;
232
+ }
233
+
234
+ .regen-btn {
235
+ background: rgba(204, 136, 68, 0.2);
236
+ border-color: #cc8844;
237
+ color: #cc8844;
238
+ }
239
+
240
+ .regen-btn:hover {
241
+ background: rgba(204, 136, 68, 0.4);
242
+ border-color: #ffaa44;
243
+ color: #ffaa44;
244
+ }
245
+
246
+ .mode-hint {
247
+ position: absolute;
248
+ top: 42px;
249
+ right: 12px;
250
+ color: #444;
251
+ font-size: 0.62rem;
252
+ font-family: 'Courier New', monospace;
253
+ letter-spacing: 0.03em;
254
+ pointer-events: none;
255
+ text-align: right;
256
+ }
257
+
258
+ .crosshair {
259
+ position: absolute;
260
+ top: 50%;
261
+ left: 50%;
262
+ width: 20px;
263
+ height: 20px;
264
+ transform: translate(-50%, -50%);
265
+ pointer-events: none;
266
+ }
267
+
268
+ .crosshair::before,
269
+ .crosshair::after {
270
+ content: '';
271
+ position: absolute;
272
+ background: rgba(255, 255, 255, 0.8);
273
+ }
274
+
275
+ .crosshair::before {
276
+ top: 50%;
277
+ left: 0;
278
+ width: 100%;
279
+ height: 2px;
280
+ margin-top: -1px;
281
+ }
282
+
283
+ .crosshair::after {
284
+ left: 50%;
285
+ top: 0;
286
+ width: 2px;
287
+ height: 100%;
288
+ margin-left: -1px;
289
+ }
290
+ </style>
@@ -0,0 +1,253 @@
1
+ <script lang="ts">
2
+ import { valtioToSvelte, dragState, craftedSlots } from '@loonylabs/gamedev-client';
3
+ import { store } from '../store.js';
4
+ import { sendMessage } from '../socket.js';
5
+
6
+ export let visible = false;
7
+
8
+ // When panel is hidden, return all items to inventory (unlock)
9
+ $: if (!visible) {
10
+ craftedSlots.set(new Set());
11
+ craftGrid = Array(9).fill(null);
12
+ craftGridNames = Array(9).fill(null);
13
+ craftGridRarity = Array(9).fill(null);
14
+ craftGridSources = Array(9).fill(null);
15
+ }
16
+
17
+ const inventory = valtioToSvelte(store, s => s.inventory);
18
+
19
+ // 9 slots for 3×3 crafting grid, each holds an item id or null
20
+ let craftGrid: Array<string | null> = Array(9).fill(null);
21
+ let craftGridNames: Array<string | null> = Array(9).fill(null);
22
+ let craftGridRarity: Array<string | null> = Array(9).fill(null);
23
+ // tracks which inventory slot index each crafting slot came from (for craftedSlots lock)
24
+ let craftGridSources: Array<number | null> = Array(9).fill(null);
25
+
26
+ let dragOverIdx: number | null = null;
27
+
28
+ function rarityColor(rarity: string | null): string {
29
+ if (!rarity) return 'var(--color-rarity-common)';
30
+ switch (rarity) {
31
+ case 'rare': return 'var(--color-rarity-rare)';
32
+ case 'unique': return 'var(--color-rarity-unique)';
33
+ default: return 'var(--color-rarity-common)';
34
+ }
35
+ }
36
+
37
+ function onDragOver(idx: number) {
38
+ dragOverIdx = idx;
39
+ }
40
+
41
+ function onDragLeave() {
42
+ dragOverIdx = null;
43
+ }
44
+
45
+ function onDrop(targetIdx: number) {
46
+ dragOverIdx = null;
47
+ const ds = $dragState;
48
+ if (!ds) return;
49
+
50
+ if (ds.source === 'inventory') {
51
+ const item = $inventory.find(i => i.slot === ds.index);
52
+ if (item) {
53
+ // Free the old inventory slot if this crafting slot was already occupied
54
+ const oldSrc = craftGridSources[targetIdx];
55
+ if (oldSrc !== null) {
56
+ craftedSlots.update(s => { s.delete(oldSrc); return new Set(s); });
57
+ }
58
+ craftGrid[targetIdx] = item.id;
59
+ craftGridNames[targetIdx] = item.name;
60
+ craftGridRarity[targetIdx] = item.rarity;
61
+ craftGridSources[targetIdx] = ds.index;
62
+ craftGrid = [...craftGrid];
63
+ craftGridNames = [...craftGridNames];
64
+ craftGridRarity = [...craftGridRarity];
65
+ craftGridSources = [...craftGridSources];
66
+ craftedSlots.update(s => { s.add(ds.index); return new Set(s); });
67
+ }
68
+ } else if (ds.source === 'crafting') {
69
+ // Move within crafting grid
70
+ const srcId = craftGrid[ds.index];
71
+ const srcName = craftGridNames[ds.index];
72
+ const srcRarity = craftGridRarity[ds.index];
73
+ const srcSource = craftGridSources[ds.index];
74
+ craftGrid[ds.index] = craftGrid[targetIdx];
75
+ craftGridNames[ds.index] = craftGridNames[targetIdx];
76
+ craftGridRarity[ds.index] = craftGridRarity[targetIdx];
77
+ craftGridSources[ds.index] = craftGridSources[targetIdx];
78
+ craftGrid[targetIdx] = srcId;
79
+ craftGridNames[targetIdx] = srcName;
80
+ craftGridRarity[targetIdx] = srcRarity;
81
+ craftGridSources[targetIdx] = srcSource;
82
+ craftGrid = [...craftGrid];
83
+ craftGridNames = [...craftGridNames];
84
+ craftGridRarity = [...craftGridRarity];
85
+ craftGridSources = [...craftGridSources];
86
+ }
87
+ dragState.set(null);
88
+ }
89
+
90
+ function onDragEnd() {
91
+ dragOverIdx = null;
92
+ dragState.set(null);
93
+ }
94
+
95
+ function clearSlot(idx: number) {
96
+ const src = craftGridSources[idx];
97
+ if (src !== null) craftedSlots.update(s => { s.delete(src); return new Set(s); });
98
+ craftGrid[idx] = null;
99
+ craftGridNames[idx] = null;
100
+ craftGridRarity[idx] = null;
101
+ craftGridSources[idx] = null;
102
+ craftGrid = [...craftGrid];
103
+ craftGridNames = [...craftGridNames];
104
+ craftGridRarity = [...craftGridRarity];
105
+ craftGridSources = [...craftGridSources];
106
+ }
107
+
108
+ function craft() {
109
+ sendMessage({ type: 'CRAFT_REQUEST', grid: craftGrid });
110
+ // Release all locked inventory slots and clear the grid
111
+ craftedSlots.set(new Set());
112
+ craftGrid = Array(9).fill(null);
113
+ craftGridNames = Array(9).fill(null);
114
+ craftGridRarity = Array(9).fill(null);
115
+ craftGridSources = Array(9).fill(null);
116
+ }
117
+
118
+ $: hasSomething = craftGrid.some(c => c !== null);
119
+ </script>
120
+
121
+ {#if visible}
122
+ <div class="crafting-panel" role="dialog" aria-label="Crafting">
123
+ <div class="crafting-title">Crafting</div>
124
+ <div class="crafting-grid">
125
+ {#each craftGrid as itemId, idx}
126
+ <div
127
+ class="slot"
128
+ class:filled={!!itemId}
129
+ class:drag-over={dragOverIdx === idx}
130
+ draggable={!!itemId}
131
+ style={itemId ? `background: ${rarityColor(craftGridRarity[idx])}22; border-color: ${rarityColor(craftGridRarity[idx])};` : ''}
132
+ title={craftGridNames[idx] ?? ''}
133
+ on:dragstart={() => itemId && dragState.set({ source: 'crafting', index: idx })}
134
+ on:dragover|preventDefault={() => onDragOver(idx)}
135
+ on:dragleave={onDragLeave}
136
+ on:drop={() => onDrop(idx)}
137
+ on:dragend={onDragEnd}
138
+ on:dblclick={() => clearSlot(idx)}
139
+ role="listitem"
140
+ >
141
+ {#if itemId}
142
+ <span class="item-name" style="color: {rarityColor(craftGridRarity[idx])}">
143
+ {(craftGridNames[idx] ?? '').split(' ')[0]}
144
+ </span>
145
+ {/if}
146
+ </div>
147
+ {/each}
148
+ </div>
149
+
150
+ <button
151
+ class="craft-btn"
152
+ disabled={!hasSomething}
153
+ on:click={craft}
154
+ >
155
+ CRAFT
156
+ </button>
157
+
158
+ <div class="hint-text">Drag items from Inventory · Dbl-click to remove</div>
159
+ </div>
160
+ {/if}
161
+
162
+ <style>
163
+ .crafting-panel {
164
+ position: absolute;
165
+ top: 50%;
166
+ left: calc(50% + 280px);
167
+ transform: translateY(-50%);
168
+ background: rgba(10, 10, 10, 0.95);
169
+ border: 1px solid #444;
170
+ padding: 16px;
171
+ pointer-events: auto;
172
+ user-select: none;
173
+ }
174
+
175
+ .crafting-title {
176
+ color: #aaa;
177
+ font-size: 0.75rem;
178
+ letter-spacing: 0.1em;
179
+ text-transform: uppercase;
180
+ margin-bottom: 10px;
181
+ text-align: center;
182
+ }
183
+
184
+ .crafting-grid {
185
+ display: grid;
186
+ grid-template-columns: repeat(3, 1fr);
187
+ gap: 3px;
188
+ margin-bottom: 10px;
189
+ }
190
+
191
+ .slot {
192
+ width: 48px;
193
+ height: 48px;
194
+ background: var(--color-slot-empty);
195
+ border: 1px solid var(--color-slot-border);
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ font-size: 0.55rem;
200
+ text-align: center;
201
+ overflow: hidden;
202
+ position: relative;
203
+ cursor: default;
204
+ }
205
+
206
+ .slot[draggable="true"] {
207
+ cursor: grab;
208
+ }
209
+
210
+ .slot.drag-over {
211
+ border-color: #88aaff;
212
+ }
213
+
214
+ .item-name {
215
+ font-size: 0.5rem;
216
+ line-height: 1.1;
217
+ text-align: center;
218
+ padding: 2px;
219
+ word-break: break-word;
220
+ }
221
+
222
+ .craft-btn {
223
+ width: 100%;
224
+ padding: 6px;
225
+ background: rgba(68, 136, 68, 0.3);
226
+ border: 1px solid #448844;
227
+ color: #44cc44;
228
+ font-family: 'Courier New', monospace;
229
+ font-size: 0.75rem;
230
+ letter-spacing: 0.1em;
231
+ cursor: pointer;
232
+ text-transform: uppercase;
233
+ }
234
+
235
+ .craft-btn:disabled {
236
+ background: rgba(40, 40, 40, 0.3);
237
+ border-color: #333;
238
+ color: #555;
239
+ cursor: default;
240
+ }
241
+
242
+ .craft-btn:not(:disabled):hover {
243
+ background: rgba(68, 136, 68, 0.5);
244
+ }
245
+
246
+ .hint-text {
247
+ margin-top: 6px;
248
+ color: #444;
249
+ font-size: 0.55rem;
250
+ text-align: center;
251
+ letter-spacing: 0.03em;
252
+ }
253
+ </style>