@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,95 @@
1
+ <script lang="ts">
2
+ import { valtioToSvelte } from '@loonylabs/gamedev-client';
3
+ import { store } from '../../store.js';
4
+
5
+ const ammo = valtioToSvelte(store, s => s.shooterAmmo);
6
+ const reloading = valtioToSvelte(store, s => s.shooterReloading);
7
+ const wave = valtioToSvelte(store, s => s.shooterWave);
8
+ const waveClear = valtioToSvelte(store, s => s.shooterWaveClear);
9
+ </script>
10
+
11
+ <div class="shooter-hud">
12
+ <!-- Ammo Counter -->
13
+ <div class="ammo-counter">
14
+ {#if $reloading}
15
+ <span class="reloading">Reloading...</span>
16
+ {:else}
17
+ <span class="ammo-current">{$ammo.current}</span>
18
+ <span class="ammo-sep"> / </span>
19
+ <span class="ammo-max">{$ammo.max}</span>
20
+ {/if}
21
+ </div>
22
+
23
+ <!-- Wave Indicator -->
24
+ <div class="wave-indicator">
25
+ Wave {$wave}
26
+ {#if $waveClear}
27
+ <span class="wave-clear">Wave Clear!</span>
28
+ {/if}
29
+ </div>
30
+ </div>
31
+
32
+ <style>
33
+ .shooter-hud {
34
+ position: absolute;
35
+ bottom: 80px;
36
+ left: 50%;
37
+ transform: translateX(-50%);
38
+ display: flex;
39
+ gap: 40px;
40
+ align-items: center;
41
+ pointer-events: none;
42
+ font-family: 'Courier New', monospace;
43
+ }
44
+
45
+ .ammo-counter {
46
+ font-size: 1.2rem;
47
+ color: #ccc;
48
+ background: rgba(0, 0, 0, 0.5);
49
+ padding: 4px 12px;
50
+ border-radius: 3px;
51
+ border: 1px solid #444;
52
+ }
53
+
54
+ .ammo-current {
55
+ color: #fff;
56
+ font-weight: bold;
57
+ }
58
+
59
+ .ammo-sep, .ammo-max {
60
+ color: #888;
61
+ }
62
+
63
+ .reloading {
64
+ color: #ffaa44;
65
+ animation: blink 0.5s infinite alternate;
66
+ }
67
+
68
+ .wave-indicator {
69
+ font-size: 1rem;
70
+ color: #aaa;
71
+ background: rgba(0, 0, 0, 0.5);
72
+ padding: 4px 12px;
73
+ border-radius: 3px;
74
+ border: 1px solid #444;
75
+ text-transform: uppercase;
76
+ letter-spacing: 0.08em;
77
+ }
78
+
79
+ .wave-clear {
80
+ color: #44cc44;
81
+ font-weight: bold;
82
+ margin-left: 10px;
83
+ animation: flash 0.5s ease-out;
84
+ }
85
+
86
+ @keyframes blink {
87
+ from { opacity: 0.4; }
88
+ to { opacity: 1; }
89
+ }
90
+
91
+ @keyframes flash {
92
+ from { color: #88ff88; transform: scale(1.2); }
93
+ to { color: #44cc44; transform: scale(1); }
94
+ }
95
+ </style>
@@ -0,0 +1,146 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import { valtioToSvelte, currentAim } from '@loonylabs/gamedev-client';
4
+ import { store } from '../store.js';
5
+ import { sendMessage } from '../socket.js';
6
+
7
+ interface Skill {
8
+ id: string;
9
+ name: string;
10
+ cooldown: number; // ms
11
+ }
12
+
13
+ // Skills in slot order (keys 1-3)
14
+ const SKILLS: Skill[] = [
15
+ { id: 'basic-shot', name: 'Shot', cooldown: 500 },
16
+ { id: 'heavy-slam', name: 'Slam', cooldown: 3000 },
17
+ { id: 'burst-heal', name: 'Heal', cooldown: 15000 },
18
+ ];
19
+
20
+ const cooldowns = valtioToSvelte(store, s => s.skillCooldowns);
21
+
22
+ /** Returns 0..1 (0 = fully on cooldown, 1 = ready) */
23
+ function cdProgress(skill: Skill, nowMs: number): number {
24
+ const last = $cooldowns[skill.id] ?? 0;
25
+ if (last === 0) return 1;
26
+ return Math.min(1, (nowMs - last) / skill.cooldown);
27
+ }
28
+
29
+ let now = Date.now();
30
+ let rafId: number;
31
+ function tick() { now = Date.now(); rafId = requestAnimationFrame(tick); }
32
+
33
+ function tryUseSkill(skill: Skill): void {
34
+ if (cdProgress(skill, Date.now()) < 1) return;
35
+
36
+ // Client-side cooldown prediction
37
+ store.skillCooldowns = { ...store.skillCooldowns, [skill.id]: Date.now() };
38
+
39
+ const aim = currentAim;
40
+ sendMessage({
41
+ type: 'USE_SKILL',
42
+ skillId: skill.id,
43
+ ...(aim ? { origin: aim.origin, direction: aim.direction } : {}),
44
+ });
45
+ }
46
+
47
+ const onKeyDown = (e: KeyboardEvent) => {
48
+ const idx = parseInt(e.key, 10) - 1;
49
+ if (idx >= 0 && idx < SKILLS.length) tryUseSkill(SKILLS[idx]);
50
+ };
51
+
52
+ onMount(() => {
53
+ rafId = requestAnimationFrame(tick);
54
+ window.addEventListener('keydown', onKeyDown);
55
+ });
56
+ onDestroy(() => {
57
+ cancelAnimationFrame(rafId);
58
+ window.removeEventListener('keydown', onKeyDown);
59
+ });
60
+ </script>
61
+
62
+ <div class="skill-bar">
63
+ {#each SKILLS as skill, i}
64
+ {@const prog = cdProgress(skill, now)}
65
+ {@const onCd = prog < 1}
66
+ <div class="slot {onCd ? 'on-cooldown' : ''}">
67
+ <!-- Radial cooldown wipe -->
68
+ <div class="cd-overlay" style="--prog: {prog}"></div>
69
+ <span class="key">{i + 1}</span>
70
+ <span class="name">{skill.name}</span>
71
+ {#if onCd}
72
+ <span class="cd-text">{(skill.cooldown * (1 - prog) / 1000).toFixed(1)}s</span>
73
+ {/if}
74
+ </div>
75
+ {/each}
76
+ </div>
77
+
78
+ <style>
79
+ .skill-bar {
80
+ position: absolute;
81
+ bottom: 60px;
82
+ left: 50%;
83
+ transform: translateX(-50%);
84
+ display: flex;
85
+ gap: 6px;
86
+ pointer-events: none;
87
+ }
88
+
89
+ .slot {
90
+ position: relative;
91
+ width: 56px;
92
+ height: 56px;
93
+ background: rgba(0, 0, 0, 0.7);
94
+ border: 1px solid #555;
95
+ border-radius: 4px;
96
+ display: flex;
97
+ flex-direction: column;
98
+ align-items: center;
99
+ justify-content: center;
100
+ overflow: hidden;
101
+ }
102
+
103
+ .slot.on-cooldown {
104
+ border-color: #333;
105
+ }
106
+
107
+ /* Radial cooldown wipe using conic-gradient */
108
+ .cd-overlay {
109
+ position: absolute;
110
+ inset: 0;
111
+ background: conic-gradient(
112
+ rgba(0, 0, 0, 0.65) 0deg,
113
+ rgba(0, 0, 0, 0.65) calc((1 - var(--prog)) * 360deg),
114
+ transparent calc((1 - var(--prog)) * 360deg)
115
+ );
116
+ }
117
+
118
+ .key {
119
+ position: absolute;
120
+ top: 3px;
121
+ left: 5px;
122
+ font-size: 0.6rem;
123
+ color: #888;
124
+ font-family: 'Courier New', monospace;
125
+ line-height: 1;
126
+ z-index: 1;
127
+ }
128
+
129
+ .name {
130
+ font-size: 0.62rem;
131
+ color: #ccc;
132
+ font-family: 'Courier New', monospace;
133
+ text-align: center;
134
+ z-index: 1;
135
+ }
136
+
137
+ .cd-text {
138
+ font-size: 0.7rem;
139
+ color: #fff;
140
+ font-family: 'Courier New', monospace;
141
+ font-weight: bold;
142
+ position: absolute;
143
+ bottom: 3px;
144
+ z-index: 1;
145
+ }
146
+ </style>
@@ -0,0 +1,158 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import { eventBus } from '@loonylabs/gamedev-client';
4
+ import toastConfigRaw from '../../../game-data/src/events/toast-config.json';
5
+ import type { GameEvent } from '@loonylabs/gamedev-core';
6
+
7
+ // Allow partial runtime overrides via prop
8
+ export let overrides: Record<string, unknown> = {};
9
+
10
+ interface EventConfig {
11
+ enabled: boolean;
12
+ duration: number;
13
+ icon: string;
14
+ color: string;
15
+ template: string;
16
+ }
17
+
18
+ interface ToastConfig {
19
+ position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-center';
20
+ maxVisible: number;
21
+ stackDirection: 'up' | 'down';
22
+ events: Record<string, EventConfig>;
23
+ }
24
+
25
+ const config: ToastConfig = { ...toastConfigRaw as ToastConfig, ...overrides };
26
+
27
+ interface Toast {
28
+ id: number;
29
+ text: string;
30
+ color: string;
31
+ exiting: boolean;
32
+ }
33
+
34
+ let toasts: Toast[] = [];
35
+ let counter = 0;
36
+
37
+ function interpolateTemplate(template: string, event: GameEvent): string {
38
+ let result = template;
39
+ if ('item' in event && event.item) {
40
+ result = result.replace('{item}', (event.item as { name: string }).name);
41
+ }
42
+ if ('slotId' in event) {
43
+ result = result.replace('{slot}', event.slotId);
44
+ }
45
+ if ('damage' in event) {
46
+ result = result.replace('{damage}', String(event.damage));
47
+ }
48
+ if ('amount' in event) {
49
+ result = result.replace('{amount}', String(event.amount));
50
+ }
51
+ return result;
52
+ }
53
+
54
+ function addToast(event: GameEvent): void {
55
+ const cfg = config.events[event.type];
56
+ if (!cfg?.enabled) return;
57
+
58
+ let text = interpolateTemplate(cfg.template, event);
59
+ // Replace {icon} placeholder with the icon value
60
+ text = text.replace('{icon}', cfg.icon);
61
+
62
+ const toast: Toast = { id: counter++, text, color: cfg.color, exiting: false };
63
+
64
+ if (config.stackDirection === 'up') {
65
+ toasts = [toast, ...toasts].slice(0, config.maxVisible);
66
+ } else {
67
+ toasts = [...toasts, toast].slice(-config.maxVisible);
68
+ }
69
+
70
+ setTimeout(() => dismissToast(toast.id), cfg.duration);
71
+ }
72
+
73
+ function dismissToast(id: number): void {
74
+ toasts = toasts.map(t => t.id === id ? { ...t, exiting: true } : t);
75
+ setTimeout(() => {
76
+ toasts = toasts.filter(t => t.id !== id);
77
+ }, 300);
78
+ }
79
+
80
+ const unsubscribers: Array<() => void> = [];
81
+
82
+ onMount(() => {
83
+ for (const type of Object.keys(config.events)) {
84
+ const unsub = eventBus.on(type as GameEvent['type'], (e) => addToast(e));
85
+ unsubscribers.push(unsub);
86
+ }
87
+ });
88
+
89
+ onDestroy(() => {
90
+ for (const unsub of unsubscribers) unsub();
91
+ });
92
+
93
+ // Position styles
94
+ const positionStyles: Record<string, string> = {
95
+ 'bottom-right': 'bottom: 80px; right: 16px; align-items: flex-end;',
96
+ 'bottom-left': 'bottom: 80px; left: 16px; align-items: flex-start;',
97
+ 'top-right': 'top: 60px; right: 16px; align-items: flex-end;',
98
+ 'top-center': 'top: 60px; left: 50%; transform: translateX(-50%); align-items: center;',
99
+ };
100
+ $: containerStyle = positionStyles[config.position] ?? positionStyles['bottom-right'];
101
+ </script>
102
+
103
+ <div class="toast-container" style={containerStyle}>
104
+ {#each toasts as toast (toast.id)}
105
+ <div
106
+ class="toast {toast.exiting ? 'exiting' : 'entering'}"
107
+ style="color: {toast.color}; border-color: {toast.color};"
108
+ role="status"
109
+ >
110
+ {toast.text}
111
+ </div>
112
+ {/each}
113
+ </div>
114
+
115
+ <style>
116
+ .toast-container {
117
+ position: fixed;
118
+ display: flex;
119
+ flex-direction: column;
120
+ gap: 6px;
121
+ pointer-events: none;
122
+ z-index: 1000;
123
+ }
124
+
125
+ .toast {
126
+ background: rgba(0, 0, 0, 0.75);
127
+ border: 1px solid;
128
+ border-radius: 4px;
129
+ padding: 6px 14px;
130
+ font-family: 'Courier New', monospace;
131
+ font-size: 0.8rem;
132
+ letter-spacing: 0.04em;
133
+ white-space: nowrap;
134
+ transition: opacity 0.3s ease, transform 0.3s ease;
135
+ opacity: 1;
136
+ transform: translateX(0);
137
+ }
138
+
139
+ .toast.entering {
140
+ animation: slide-in 0.25s ease forwards;
141
+ }
142
+
143
+ .toast.exiting {
144
+ opacity: 0;
145
+ transform: translateX(20px);
146
+ }
147
+
148
+ @keyframes slide-in {
149
+ from {
150
+ opacity: 0;
151
+ transform: translateX(30px);
152
+ }
153
+ to {
154
+ opacity: 1;
155
+ transform: translateX(0);
156
+ }
157
+ }
158
+ </style>