@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.
- package/bin/create-game.js +213 -0
- package/package.json +18 -0
- package/template/.claude/skills/aigdtk-create-game-stories/SKILL.md +177 -0
- package/template/.claude/skills/aigdtk-create-game-stories/story-template.md +85 -0
- package/template/.claude/skills/aigdtk-implement-game-stories/SKILL.md +129 -0
- package/template/.claude/skills/aigdtk-new-game/SKILL.md +126 -0
- package/template/.claude/skills/aigdtk-shared/ascii-grammar.md +133 -0
- package/template/.claude/skills/aigdtk-shared/enemies.md +112 -0
- package/template/.claude/skills/aigdtk-shared/framework.md +93 -0
- package/template/.claude/skills/aigdtk-shared/visuals.md +125 -0
- package/template/apps/client/index.html +14 -0
- package/template/apps/client/package.json +31 -0
- package/template/apps/client/public/assets/audio/enemy_killed.wav +0 -0
- package/template/apps/client/src/components/App.svelte +290 -0
- package/template/apps/client/src/components/CraftingPanel.svelte +253 -0
- package/template/apps/client/src/components/DevPanel.svelte +180 -0
- package/template/apps/client/src/components/DungeonClearOverlay.svelte +53 -0
- package/template/apps/client/src/components/EquipmentPanel.svelte +191 -0
- package/template/apps/client/src/components/HealthBar.svelte +50 -0
- package/template/apps/client/src/components/Hub/BackToHubButton.svelte +37 -0
- package/template/apps/client/src/components/Hub/ExperienceCard.svelte +115 -0
- package/template/apps/client/src/components/Hub/Hub.svelte +88 -0
- package/template/apps/client/src/components/Inventory.svelte +174 -0
- package/template/apps/client/src/components/Runner/RunnerDeathScreen.svelte +182 -0
- package/template/apps/client/src/components/Runner/RunnerHUD.svelte +157 -0
- package/template/apps/client/src/components/Shooter/DamageNumbers.svelte +96 -0
- package/template/apps/client/src/components/Shooter/GameOverScreen.svelte +109 -0
- package/template/apps/client/src/components/Shooter/ShooterHUD.svelte +95 -0
- package/template/apps/client/src/components/SkillBar.svelte +146 -0
- package/template/apps/client/src/components/ToastSystem.svelte +158 -0
- package/template/apps/client/src/game.ts +918 -0
- package/template/apps/client/src/input.ts +92 -0
- package/template/apps/client/src/lib/audio/CombatDetector.test.ts +59 -0
- package/template/apps/client/src/lib/audio/CombatDetector.ts +53 -0
- package/template/apps/client/src/lib/audio/MusicManager.ts +137 -0
- package/template/apps/client/src/lib/audio/SoundManager.ts +59 -0
- package/template/apps/client/src/lib/audio/index.ts +9 -0
- package/template/apps/client/src/main.ts +32 -0
- package/template/apps/client/src/renderer/basecamp.ts +126 -0
- package/template/apps/client/src/renderer/dungeon.ts +250 -0
- package/template/apps/client/src/renderer/dungeonPortal.ts +73 -0
- package/template/apps/client/src/renderer/dungeonZone.ts +301 -0
- package/template/apps/client/src/renderer/entities.ts +197 -0
- package/template/apps/client/src/renderer/runnerTrack.ts +221 -0
- package/template/apps/client/src/renderer/shaders/SkyShader.ts +190 -0
- package/template/apps/client/src/renderer/shaders/TerrainShaderMaterial.ts +133 -0
- package/template/apps/client/src/renderer/shaders/floor.frag.glsl.ts +17 -0
- package/template/apps/client/src/renderer/shaders/shaderConfig.ts +18 -0
- package/template/apps/client/src/renderer/shaders/spawn.frag.glsl.ts +19 -0
- package/template/apps/client/src/renderer/shaders/terrain.frag.glsl.ts +314 -0
- package/template/apps/client/src/renderer/shaders/terrain.vert.glsl.ts +16 -0
- package/template/apps/client/src/renderer/shaders/wall.frag.glsl.ts +20 -0
- package/template/apps/client/src/renderer/shooterArena.ts +102 -0
- package/template/apps/client/src/renderer/voxelChunkStreamer.ts +79 -0
- package/template/apps/client/src/renderer/voxelMesh.ts +86 -0
- package/template/apps/client/src/renderer/voxelTerrain.ts +74 -0
- package/template/apps/client/src/socket.ts +268 -0
- package/template/apps/client/src/store.ts +74 -0
- package/template/apps/client/src/style.css +60 -0
- package/template/apps/client/tsconfig.json +11 -0
- package/template/apps/client/vite.config.ts +10 -0
- package/template/apps/client/vitest.config.ts +8 -0
- package/template/apps/experiences/diablo/index.ts +94 -0
- package/template/apps/experiences/diablo/systems/dungeonClearSystem.ts +60 -0
- package/template/apps/experiences/diablo/systems/enemyAISystem.ts +11 -0
- package/template/apps/experiences/diablo/systems/entitySyncSystem.ts +80 -0
- package/template/apps/experiences/diablo/systems/itemPickupSystem.ts +11 -0
- package/template/apps/experiences/diablo/systems/movementSystem.ts +13 -0
- package/template/apps/experiences/diablo/systems/physicsSystem.ts +92 -0
- package/template/apps/experiences/diablo/systems/transitionSystem.ts +105 -0
- package/template/apps/experiences/runner/data/runner-config.ts +54 -0
- package/template/apps/experiences/runner/index.ts +143 -0
- package/template/apps/experiences/runner/systems/collectibleSystem.ts +157 -0
- package/template/apps/experiences/runner/systems/deathSystem.ts +42 -0
- package/template/apps/experiences/runner/systems/entitySyncSystem.ts +59 -0
- package/template/apps/experiences/runner/systems/obstacleSystem.ts +91 -0
- package/template/apps/experiences/runner/systems/runnerPhysicsSystem.ts +82 -0
- package/template/apps/experiences/runner/systems/trackStreamSystem.ts +19 -0
- package/template/apps/experiences/runner/track/laneSystem.ts +53 -0
- package/template/apps/experiences/runner/track/segmentTypes.ts +141 -0
- package/template/apps/experiences/runner/track/trackGenerator.ts +292 -0
- package/template/apps/experiences/shooter/ai/aiStateMachine.ts +394 -0
- package/template/apps/experiences/shooter/ai/lineOfSight.ts +32 -0
- package/template/apps/experiences/shooter/arena/arenaGenerator.ts +101 -0
- package/template/apps/experiences/shooter/arena/arenaTypes.ts +49 -0
- package/template/apps/experiences/shooter/arena/hitscan.ts +101 -0
- package/template/apps/experiences/shooter/data/enemy-types.ts +108 -0
- package/template/apps/experiences/shooter/data/wave-definitions.ts +40 -0
- package/template/apps/experiences/shooter/data/weapon-config.ts +80 -0
- package/template/apps/experiences/shooter/index.ts +127 -0
- package/template/apps/experiences/shooter/systems/enemyAISystem.ts +113 -0
- package/template/apps/experiences/shooter/systems/entitySyncSystem.ts +68 -0
- package/template/apps/experiences/shooter/systems/shooterPhysicsSystem.ts +89 -0
- package/template/apps/experiences/shooter/systems/waveSpawnerSystem.ts +87 -0
- package/template/apps/experiences/shooter/systems/weaponSystem.ts +157 -0
- package/template/apps/game-data/src/areas/area-manifest.json +18 -0
- package/template/apps/game-data/src/assets/migration.test.ts +291 -0
- package/template/apps/game-data/src/audio/music-config.json +21 -0
- package/template/apps/game-data/src/audio/sound-config.json +11 -0
- package/template/apps/game-data/src/combat/action-types.ts +2 -0
- package/template/apps/game-data/src/combat/enemy-def.ts +12 -0
- package/template/apps/game-data/src/combat/hitboxes.ts +23 -0
- package/template/apps/game-data/src/dungeon/cell-types.ts +20 -0
- package/template/apps/game-data/src/dungeon/cell-visuals.ts +13 -0
- package/template/apps/game-data/src/dungeon/door-directions.ts +2 -0
- package/template/apps/game-data/src/enemies/enemy-defs.json +32 -0
- package/template/apps/game-data/src/equipment/slots.json +5 -0
- package/template/apps/game-data/src/events/event-defs.ts +20 -0
- package/template/apps/game-data/src/events/event-types.ts +10 -0
- package/template/apps/game-data/src/events/toast-config.json +49 -0
- package/template/apps/game-data/src/items/item-pool.json +13 -0
- package/template/apps/game-data/src/loot/item-pool.ts +14 -0
- package/template/apps/game-data/src/loot/rarities.ts +2 -0
- package/template/apps/game-data/src/physics/dungeon-physics-config.ts +12 -0
- package/template/apps/game-data/src/physics/jump-config.ts +17 -0
- package/template/apps/game-data/src/recipes/recipe-book.json +68 -0
- package/template/apps/game-data/src/rooms/room_basecamp.json +16 -0
- package/template/apps/game-data/src/rooms/room_corridor_ew.json +9 -0
- package/template/apps/game-data/src/rooms/room_corridor_ns.json +11 -0
- package/template/apps/game-data/src/rooms/room_crossroads.json +11 -0
- package/template/apps/game-data/src/rooms/room_dead_end.json +10 -0
- package/template/apps/game-data/src/rooms/room_staircase.json +12 -0
- package/template/apps/game-data/src/rooms/room_start.json +11 -0
- package/template/apps/game-data/src/skills/skill-book.json +20 -0
- package/template/apps/game-data/src/voxel/biome-terrain.ts +76 -0
- package/template/apps/game-data/src/voxel/materials.ts +45 -0
- package/template/apps/game-data/src/voxel/sandbox-terrain-config.ts +19 -0
- package/template/apps/game-data/src/world/area-config.ts +33 -0
- package/template/apps/game-data/src/world/biome-def.ts +15 -0
- package/template/apps/game-data/src/world/biomes.json +57 -0
- package/template/apps/game-data/src/world/movement.ts +2 -0
- package/template/apps/game-data/src/world/overworld-layout.test.ts +93 -0
- package/template/apps/game-data/src/world/overworld-layout.ts +127 -0
- package/template/apps/server/data/game.db +0 -0
- package/template/apps/server/package.json +30 -0
- package/template/apps/server/src/areaManager.ts +346 -0
- package/template/apps/server/src/db/client.ts +45 -0
- package/template/apps/server/src/db/schema.ts +40 -0
- package/template/apps/server/src/gameLoop.ts +267 -0
- package/template/apps/server/src/gameState.ts +3 -0
- package/template/apps/server/src/handlers/actionEvent.ts +55 -0
- package/template/apps/server/src/handlers/craftHandler.ts +59 -0
- package/template/apps/server/src/handlers/equipHandler.ts +73 -0
- package/template/apps/server/src/handlers/raycastHandler.ts +97 -0
- package/template/apps/server/src/handlers/skillHandler.ts +87 -0
- package/template/apps/server/src/handlers/terraformHandler.ts +74 -0
- package/template/apps/server/src/index.ts +597 -0
- package/template/apps/server/src/persistence.ts +135 -0
- package/template/apps/server/src/rooms.ts +20 -0
- package/template/apps/server/src/systems/dungeonPhysics.test.ts +32 -0
- package/template/apps/server/src/systems/dungeonPhysics.ts +16 -0
- package/template/apps/server/src/systems/enemyAI.ts +129 -0
- package/template/apps/server/src/systems/itemPickup.ts +31 -0
- package/template/apps/server/src/tests/areaManager.test.ts +77 -0
- package/template/apps/server/src/tests/diablo-experience.test.ts +60 -0
- package/template/apps/server/src/tests/runner-experience.test.ts +273 -0
- package/template/apps/server/src/tests/runner-powerups-scoring.test.ts +221 -0
- package/template/apps/server/src/tests/server.integration.test.ts +92 -0
- package/template/apps/server/src/tests/shooter-enemy-ai.test.ts +328 -0
- package/template/apps/server/src/tests/shooter-experience.test.ts +281 -0
- package/template/apps/server/src/tests/voxelChunkCache.test.ts +29 -0
- package/template/apps/server/src/tests/voxelSandbox.test.ts +133 -0
- package/template/apps/server/src/voxelChunkCache.ts +31 -0
- package/template/apps/server/src/voxelPlayerState.ts +23 -0
- package/template/apps/server/tsconfig.json +17 -0
- 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>
|