@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,291 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import {
|
|
5
|
+
validateAsset,
|
|
6
|
+
validateAgainstRig,
|
|
7
|
+
defaultRigRegistry,
|
|
8
|
+
AssetDefinitionSchema,
|
|
9
|
+
} from '@loonylabs/gamedev-core';
|
|
10
|
+
import type { AssetDefinition } from '@loonylabs/gamedev-core';
|
|
11
|
+
|
|
12
|
+
const MOBS_DIR = join(__dirname, '../../assets/mobs');
|
|
13
|
+
|
|
14
|
+
function loadAsset(filename: string): AssetDefinition {
|
|
15
|
+
const raw = readFileSync(join(MOBS_DIR, filename), 'utf-8');
|
|
16
|
+
return JSON.parse(raw) as AssetDefinition;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const camel = loadAsset('camel.json');
|
|
20
|
+
const scorpion = loadAsset('scorpion.json');
|
|
21
|
+
const pig = loadAsset('pig.json');
|
|
22
|
+
const rabbit = loadAsset('rabbit.json');
|
|
23
|
+
const chicken = loadAsset('chicken.json');
|
|
24
|
+
const cow = loadAsset('cow.json');
|
|
25
|
+
const deer = loadAsset('deer.json');
|
|
26
|
+
const frog = loadAsset('frog.json');
|
|
27
|
+
const horse = loadAsset('horse.json');
|
|
28
|
+
const ocelot = loadAsset('ocelot.json');
|
|
29
|
+
const parrot = loadAsset('parrot.json');
|
|
30
|
+
const pegasus = loadAsset('pegasus.json');
|
|
31
|
+
const polarBear = loadAsset('polar_bear.json');
|
|
32
|
+
const sheep = loadAsset('sheep.json');
|
|
33
|
+
const spider = loadAsset('spider.json');
|
|
34
|
+
const unicorn = loadAsset('unicorn.json');
|
|
35
|
+
const wolf = loadAsset('wolf.json');
|
|
36
|
+
const zombie = loadAsset('zombie.json');
|
|
37
|
+
|
|
38
|
+
const assetFiles = [
|
|
39
|
+
{ name: 'camel.json', asset: camel },
|
|
40
|
+
{ name: 'scorpion.json', asset: scorpion },
|
|
41
|
+
{ name: 'pig.json', asset: pig },
|
|
42
|
+
{ name: 'rabbit.json', asset: rabbit },
|
|
43
|
+
{ name: 'chicken.json', asset: chicken },
|
|
44
|
+
{ name: 'cow.json', asset: cow },
|
|
45
|
+
{ name: 'deer.json', asset: deer },
|
|
46
|
+
{ name: 'frog.json', asset: frog },
|
|
47
|
+
{ name: 'horse.json', asset: horse },
|
|
48
|
+
{ name: 'ocelot.json', asset: ocelot },
|
|
49
|
+
{ name: 'parrot.json', asset: parrot },
|
|
50
|
+
{ name: 'pegasus.json', asset: pegasus },
|
|
51
|
+
{ name: 'polar_bear.json', asset: polarBear },
|
|
52
|
+
{ name: 'sheep.json', asset: sheep },
|
|
53
|
+
{ name: 'spider.json', asset: spider },
|
|
54
|
+
{ name: 'unicorn.json', asset: unicorn },
|
|
55
|
+
{ name: 'wolf.json', asset: wolf },
|
|
56
|
+
{ name: 'zombie.json', asset: zombie },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
describe('migrated assets — common validation', () => {
|
|
60
|
+
for (const { name, asset } of assetFiles) {
|
|
61
|
+
describe(name, () => {
|
|
62
|
+
test('validates against schema', () => {
|
|
63
|
+
const result = AssetDefinitionSchema.safeParse(asset);
|
|
64
|
+
expect(result.success).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('validates against declared rig', () => {
|
|
68
|
+
const result = validateAgainstRig(asset, defaultRigRegistry);
|
|
69
|
+
expect(result.valid).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('has category mob', () => {
|
|
73
|
+
expect(asset.category).toBe('mob');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('has migration author tag', () => {
|
|
77
|
+
expect(asset.metadata?.author).toBe('migration');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('camel specifics', () => {
|
|
84
|
+
test('has quadruped rig', () => {
|
|
85
|
+
expect(camel.rig).toBe('quadruped');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('has 2 humps', () => {
|
|
89
|
+
expect(camel.parts.hump_front).toBeDefined();
|
|
90
|
+
expect(camel.parts.hump_back).toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('has neck segments', () => {
|
|
94
|
+
expect(camel.parts.neck_base).toBeDefined();
|
|
95
|
+
expect(camel.parts.neck_top).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('has correct body color', () => {
|
|
99
|
+
expect(camel.parts.torso.color).toBe('#C2B280');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('has tall legs (0.9 height)', () => {
|
|
103
|
+
expect(camel.parts.leg_front_L.shape).toMatchObject({ type: 'box', size: [0.25, 0.90, 0.25] });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('has eyes as head children', () => {
|
|
107
|
+
expect(camel.parts.head.children?.eye_L).toBeDefined();
|
|
108
|
+
expect(camel.parts.head.children?.eye_R).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('scorpion specifics', () => {
|
|
113
|
+
test('has arthropod rig', () => {
|
|
114
|
+
expect(scorpion.rig).toBe('arthropod');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('has 8 legs', () => {
|
|
118
|
+
const legParts = Object.keys(scorpion.parts).filter((k) => k.startsWith('leg_'));
|
|
119
|
+
expect(legParts).toHaveLength(8);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('has 3 tail segments', () => {
|
|
123
|
+
const tailParts = Object.keys(scorpion.parts).filter((k) => k.startsWith('tail_'));
|
|
124
|
+
expect(tailParts).toHaveLength(3);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('stinger uses cone shape', () => {
|
|
128
|
+
expect(scorpion.parts.stinger.shape.type).toBe('cone');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('has custom walk animation override for tail sway', () => {
|
|
132
|
+
expect(scorpion.animations?.walk).toBeDefined();
|
|
133
|
+
const channels = scorpion.animations!.walk.channels;
|
|
134
|
+
const tailChannels = channels.filter((c) => c.slot.startsWith('tail_'));
|
|
135
|
+
expect(tailChannels.length).toBeGreaterThanOrEqual(3);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('left legs have negative Z rotation (splay outward)', () => {
|
|
139
|
+
expect(scorpion.parts.leg_0.rotation).toEqual([0, 0, -45]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('right legs have positive Z rotation (splay outward)', () => {
|
|
143
|
+
expect(scorpion.parts.leg_4.rotation).toEqual([0, 0, 45]);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('pig specifics', () => {
|
|
148
|
+
test('has quadruped rig', () => {
|
|
149
|
+
expect(pig.rig).toBe('quadruped');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('has correct body color', () => {
|
|
153
|
+
expect(pig.parts.torso.color).toBe('#F4A6A6');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('has correct leg color', () => {
|
|
157
|
+
expect(pig.parts.leg_front_L.color).toBe('#E89098');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('has short legs (0.4 height)', () => {
|
|
161
|
+
expect(pig.parts.leg_front_L.shape).toMatchObject({ type: 'box', size: [0.25, 0.40, 0.25] });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('has curly tail with rotation', () => {
|
|
165
|
+
expect(pig.parts.tail).toBeDefined();
|
|
166
|
+
expect(pig.parts.tail.rotation).toBeDefined();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('rabbit specifics', () => {
|
|
171
|
+
test('has quadruped rig', () => {
|
|
172
|
+
expect(rabbit.rig).toBe('quadruped');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('back legs are larger than front legs', () => {
|
|
176
|
+
const frontSize = rabbit.parts.leg_front_L.shape;
|
|
177
|
+
const backSize = rabbit.parts.leg_back_L.shape;
|
|
178
|
+
if (frontSize.type === 'box' && backSize.type === 'box') {
|
|
179
|
+
expect(backSize.size[1]).toBeGreaterThan(frontSize.size[1]);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('has ears as head children with pivots', () => {
|
|
184
|
+
expect(rabbit.parts.head.children?.ear_L).toBeDefined();
|
|
185
|
+
expect(rabbit.parts.head.children?.ear_R).toBeDefined();
|
|
186
|
+
expect(rabbit.parts.head.children?.ear_L.pivot).toBeDefined();
|
|
187
|
+
expect(rabbit.parts.head.children?.ear_R.pivot).toBeDefined();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('has custom walk animation override (hop motion)', () => {
|
|
191
|
+
expect(rabbit.animations?.walk).toBeDefined();
|
|
192
|
+
const channels = rabbit.animations!.walk.channels;
|
|
193
|
+
// Both back legs should move together (not diagonal)
|
|
194
|
+
const backLegChannels = channels.filter(
|
|
195
|
+
(c) => c.slot === 'leg_back_L' || c.slot === 'leg_back_R',
|
|
196
|
+
);
|
|
197
|
+
expect(backLegChannels).toHaveLength(2);
|
|
198
|
+
// Same keyframes for both back legs (simultaneous push)
|
|
199
|
+
expect(backLegChannels[0].keyframes).toEqual(backLegChannels[1].keyframes);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('has body arc in walk animation', () => {
|
|
203
|
+
const bodyChannel = rabbit.animations!.walk.channels.find(
|
|
204
|
+
(c) => c.slot === 'torso' && c.property === 'positionY',
|
|
205
|
+
);
|
|
206
|
+
expect(bodyChannel).toBeDefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('has ear sway animation', () => {
|
|
210
|
+
const earChannels = rabbit.animations!.walk.channels.filter((c) =>
|
|
211
|
+
c.slot.startsWith('ear_'),
|
|
212
|
+
);
|
|
213
|
+
expect(earChannels).toHaveLength(2);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('chicken specifics', () => {
|
|
218
|
+
test('has winged rig', () => {
|
|
219
|
+
expect(chicken.rig).toBe('winged');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('has beak and wattle as head children', () => {
|
|
223
|
+
expect(chicken.parts.head.children?.beak).toBeDefined();
|
|
224
|
+
expect(chicken.parts.head.children?.wattle).toBeDefined();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('spider specifics', () => {
|
|
229
|
+
test('has arthropod rig', () => {
|
|
230
|
+
expect(spider.rig).toBe('arthropod');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('has 8 legs', () => {
|
|
234
|
+
const legParts = Object.keys(spider.parts).filter((k) => k.startsWith('leg_'));
|
|
235
|
+
expect(legParts).toHaveLength(8);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('has red eyes', () => {
|
|
239
|
+
expect(spider.parts.head.children?.eye_L.color).toBe('#FF0000');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('zombie specifics', () => {
|
|
244
|
+
test('has biped rig', () => {
|
|
245
|
+
expect(zombie.rig).toBe('biped');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('has arms', () => {
|
|
249
|
+
expect(zombie.parts.arm_L).toBeDefined();
|
|
250
|
+
expect(zombie.parts.arm_R).toBeDefined();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('pegasus specifics', () => {
|
|
255
|
+
test('has winged_quadruped rig', () => {
|
|
256
|
+
expect(pegasus.rig).toBe('winged_quadruped');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('has wings and 4 legs', () => {
|
|
260
|
+
expect(pegasus.parts.wing_L).toBeDefined();
|
|
261
|
+
expect(pegasus.parts.wing_R).toBeDefined();
|
|
262
|
+
expect(pegasus.parts.leg_front_L).toBeDefined();
|
|
263
|
+
expect(pegasus.parts.leg_back_R).toBeDefined();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('unicorn specifics', () => {
|
|
268
|
+
test('has golden horn as head child', () => {
|
|
269
|
+
expect(unicorn.parts.head.children?.horn).toBeDefined();
|
|
270
|
+
expect(unicorn.parts.head.children?.horn.color).toBe('#FFD700');
|
|
271
|
+
expect(unicorn.parts.head.children?.horn.shape.type).toBe('cone');
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('wolf specifics', () => {
|
|
276
|
+
test('has snout and nose as head children', () => {
|
|
277
|
+
expect(wolf.parts.head.children?.snout).toBeDefined();
|
|
278
|
+
expect(wolf.parts.head.children?.nose).toBeDefined();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('deer specifics', () => {
|
|
283
|
+
test('has antlers', () => {
|
|
284
|
+
expect(deer.parts.antler_L).toBeDefined();
|
|
285
|
+
expect(deer.parts.antler_R).toBeDefined();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('has white spots', () => {
|
|
289
|
+
expect(deer.parts.spot_1.color).toBe('#FFFFFF');
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Music config — set 'src' to a path like '/assets/audio/dungeon_ambient.mp3'. null = silent. All tracks loop automatically.",
|
|
3
|
+
"crossfadeDuration": 2.0,
|
|
4
|
+
"combatDetectionRadius": 6.0,
|
|
5
|
+
"combatExitDelay": 4000,
|
|
6
|
+
"biomes": {
|
|
7
|
+
"dungeon": {
|
|
8
|
+
"ambient": { "src": null, "volume": 0.5 },
|
|
9
|
+
"layers": {
|
|
10
|
+
"combat": { "src": null, "volume": 0.0, "targetVolume": 0.7, "fadeIn": 1.5 }
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"safe_room": {
|
|
14
|
+
"ambient": { "src": null, "volume": 0.4 }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"stingers": {
|
|
18
|
+
"enemy_killed": { "src": null, "volume": 0.6 },
|
|
19
|
+
"item_crafted": { "src": null, "volume": 0.5 }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Sound config — set 'src' to a path like '/assets/audio/pickup.mp3' to enable. null = silent (framework runs without assets).",
|
|
3
|
+
"volume": 0.8,
|
|
4
|
+
"events": {
|
|
5
|
+
"item_pickup": { "src": null, "volume": 1.0 },
|
|
6
|
+
"enemy_killed": { "src": "/assets/audio/enemy_killed.wav", "volume": 0.9 },
|
|
7
|
+
"item_crafted": { "src": null, "volume": 1.0 },
|
|
8
|
+
"item_equipped": { "src": null, "volume": 0.8 },
|
|
9
|
+
"player_damaged": { "src": null, "volume": 1.0 }
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** This game's enemy definition — RPG-style with specific combat stats. */
|
|
2
|
+
export interface GameEnemyDef {
|
|
3
|
+
id: string;
|
|
4
|
+
hp: number;
|
|
5
|
+
maxHp: number;
|
|
6
|
+
speed: number;
|
|
7
|
+
attackDamage: number;
|
|
8
|
+
attackRange: number;
|
|
9
|
+
aggroRange: number;
|
|
10
|
+
color: string;
|
|
11
|
+
scale: number;
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Hitbox } from '@loonylabs/gamedev-core';
|
|
2
|
+
|
|
3
|
+
/** Default humanoid hitboxes for this game's 2-unit tall entities. */
|
|
4
|
+
export const HUMANOID_HITBOXES: Hitbox[] = [
|
|
5
|
+
{
|
|
6
|
+
label: 'head',
|
|
7
|
+
multiplier: 2.5,
|
|
8
|
+
min: { x: -0.2, y: 1.6, z: -0.2 },
|
|
9
|
+
max: { x: 0.2, y: 2.0, z: 0.2 },
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
label: 'torso',
|
|
13
|
+
multiplier: 1.0,
|
|
14
|
+
min: { x: -0.3, y: 0.8, z: -0.2 },
|
|
15
|
+
max: { x: 0.3, y: 1.6, z: 0.2 },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
label: 'legs',
|
|
19
|
+
multiplier: 0.5,
|
|
20
|
+
min: { x: -0.3, y: 0.0, z: -0.2 },
|
|
21
|
+
max: { x: 0.3, y: 0.8, z: 0.2 },
|
|
22
|
+
},
|
|
23
|
+
];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** This game's cell types for the dungeon grid. */
|
|
2
|
+
export type GameCellType = 'wall' | 'floor' | 'door' | 'spawn' | 'empty' | 'transition' | 'npc' | 'fire';
|
|
3
|
+
|
|
4
|
+
/** ASCII symbol → cell type mapping for this game's room definitions. */
|
|
5
|
+
export const GAME_SYMBOL_MAP: Record<string, string> = {
|
|
6
|
+
'W': 'wall',
|
|
7
|
+
'.': 'floor',
|
|
8
|
+
'D': 'door',
|
|
9
|
+
'S': 'spawn',
|
|
10
|
+
' ': 'empty',
|
|
11
|
+
'>': 'transition',
|
|
12
|
+
'<': 'transition',
|
|
13
|
+
'^': 'transition',
|
|
14
|
+
'v': 'transition',
|
|
15
|
+
'N': 'npc',
|
|
16
|
+
'F': 'fire',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Cell types considered walkable in this game. */
|
|
20
|
+
export const GAME_WALKABLE_TYPES = new Set(['floor', 'door', 'spawn', 'transition', 'npc', 'fire']);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CellVisual } from '@loonylabs/gamedev-core';
|
|
2
|
+
import type { GameCellType } from './cell-types.js';
|
|
3
|
+
|
|
4
|
+
export const CELL_VISUALS: Record<GameCellType, CellVisual | null> = {
|
|
5
|
+
wall: { height: 2, color: 0x888888 },
|
|
6
|
+
floor: { height: 0.1, color: 0x444444 },
|
|
7
|
+
door: { height: 1.5, color: 0xcc7733 },
|
|
8
|
+
spawn: { height: 0.2, color: 0x44cc44 },
|
|
9
|
+
empty: null,
|
|
10
|
+
transition: { height: 0.15, color: 0xffcc00 },
|
|
11
|
+
npc: { height: 1.8, color: 0xffff00 },
|
|
12
|
+
fire: { height: 0.3, color: 0xff6600 },
|
|
13
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "grunt",
|
|
4
|
+
"hp": 50, "maxHp": 50, "speed": 2.5,
|
|
5
|
+
"attackDamage": 10, "attackRange": 1.2, "aggroRange": 6,
|
|
6
|
+
"color": "#cc4444", "scale": 1.0
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "wolf",
|
|
10
|
+
"hp": 30, "maxHp": 30, "speed": 5.5,
|
|
11
|
+
"attackDamage": 8, "attackRange": 1.0, "aggroRange": 7,
|
|
12
|
+
"color": "#888888", "scale": 0.7
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "bandit",
|
|
16
|
+
"hp": 60, "maxHp": 60, "speed": 3.0,
|
|
17
|
+
"attackDamage": 15, "attackRange": 1.3, "aggroRange": 5,
|
|
18
|
+
"color": "#8B4513", "scale": 1.0
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "skeleton",
|
|
22
|
+
"hp": 45, "maxHp": 45, "speed": 2.0,
|
|
23
|
+
"attackDamage": 12, "attackRange": 1.2, "aggroRange": 8,
|
|
24
|
+
"color": "#e8e0d0", "scale": 0.9
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "golem",
|
|
28
|
+
"hp": 150, "maxHp": 150, "speed": 1.2,
|
|
29
|
+
"attackDamage": 30, "attackRange": 1.5, "aggroRange": 4,
|
|
30
|
+
"color": "#607060", "scale": 1.6
|
|
31
|
+
}
|
|
32
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Game-specific event payload interfaces for the dungeon-crawler reference implementation.
|
|
3
|
+
* These were previously in packages/core/src/events/types.ts.
|
|
4
|
+
*/
|
|
5
|
+
import type { GameEvent } from '@loonylabs/gamedev-core';
|
|
6
|
+
|
|
7
|
+
export interface ItemPickupEvent extends GameEvent { type: 'item_pickup'; item: { id: string; name: string; rarity: string } }
|
|
8
|
+
export interface EnemyKilledEvent extends GameEvent { type: 'enemy_killed'; enemyId: number; xp?: number }
|
|
9
|
+
export interface ItemCraftedEvent extends GameEvent { type: 'item_crafted'; item: { id: string; name: string; rarity: string } }
|
|
10
|
+
export interface ItemEquippedEvent extends GameEvent { type: 'item_equipped'; item: { name: string }; slotId: string }
|
|
11
|
+
export interface PlayerDamagedEvent extends GameEvent { type: 'player_damaged'; damage: number }
|
|
12
|
+
export interface PlayerHealedEvent extends GameEvent { type: 'player_healed'; amount: number }
|
|
13
|
+
|
|
14
|
+
export type GameGameEvent =
|
|
15
|
+
| ItemPickupEvent
|
|
16
|
+
| EnemyKilledEvent
|
|
17
|
+
| ItemCraftedEvent
|
|
18
|
+
| ItemEquippedEvent
|
|
19
|
+
| PlayerDamagedEvent
|
|
20
|
+
| PlayerHealedEvent;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"position": "bottom-right",
|
|
3
|
+
"maxVisible": 4,
|
|
4
|
+
"stackDirection": "up",
|
|
5
|
+
"events": {
|
|
6
|
+
"item_pickup": {
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"duration": 3000,
|
|
9
|
+
"icon": "⚔️",
|
|
10
|
+
"color": "#88ccff",
|
|
11
|
+
"template": "{icon} {item} picked up"
|
|
12
|
+
},
|
|
13
|
+
"enemy_killed": {
|
|
14
|
+
"enabled": true,
|
|
15
|
+
"duration": 2000,
|
|
16
|
+
"icon": "💀",
|
|
17
|
+
"color": "#ff8844",
|
|
18
|
+
"template": "{icon} Enemy slain"
|
|
19
|
+
},
|
|
20
|
+
"item_crafted": {
|
|
21
|
+
"enabled": true,
|
|
22
|
+
"duration": 4000,
|
|
23
|
+
"icon": "⚒️",
|
|
24
|
+
"color": "#ffcc44",
|
|
25
|
+
"template": "{icon} Crafted: {item}"
|
|
26
|
+
},
|
|
27
|
+
"item_equipped": {
|
|
28
|
+
"enabled": true,
|
|
29
|
+
"duration": 2500,
|
|
30
|
+
"icon": "🛡️",
|
|
31
|
+
"color": "#88ff88",
|
|
32
|
+
"template": "{icon} Equipped {item} ({slot})"
|
|
33
|
+
},
|
|
34
|
+
"player_damaged": {
|
|
35
|
+
"enabled": false,
|
|
36
|
+
"duration": 1500,
|
|
37
|
+
"icon": "💔",
|
|
38
|
+
"color": "#ff4444",
|
|
39
|
+
"template": "{icon} -{damage} HP"
|
|
40
|
+
},
|
|
41
|
+
"player_healed": {
|
|
42
|
+
"enabled": false,
|
|
43
|
+
"duration": 1500,
|
|
44
|
+
"icon": "💚",
|
|
45
|
+
"color": "#44ff88",
|
|
46
|
+
"template": "{icon} +{amount} HP"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "id": "rusty_sword", "name": "Rusty Sword", "rarity": "common", "weight": 30, "statBonus": { "attack": 3 } },
|
|
3
|
+
{ "id": "wooden_shield", "name": "Wooden Shield", "rarity": "common", "weight": 30, "statBonus": { "defense": 2 } },
|
|
4
|
+
{ "id": "health_potion", "name": "Health Potion", "rarity": "common", "weight": 25, "statBonus": { "hp": 20 } },
|
|
5
|
+
{ "id": "leather_boots", "name": "Leather Boots", "rarity": "common", "weight": 20, "statBonus": { "speed": 1 } },
|
|
6
|
+
{ "id": "iron_ring", "name": "Iron Ring", "rarity": "common", "weight": 15, "statBonus": { "attack": 1, "defense": 1 } },
|
|
7
|
+
{ "id": "silver_dagger", "name": "Silver Dagger", "rarity": "rare", "weight": 10, "statBonus": { "attack": 8 } },
|
|
8
|
+
{ "id": "chain_mail", "name": "Chain Mail", "rarity": "rare", "weight": 10, "statBonus": { "defense": 7 } },
|
|
9
|
+
{ "id": "elixir_of_speed", "name": "Elixir of Speed", "rarity": "rare", "weight": 8, "statBonus": { "speed": 4, "attack": 2 } },
|
|
10
|
+
{ "id": "ruby_amulet", "name": "Ruby Amulet", "rarity": "rare", "weight": 7, "statBonus": { "hp": 30, "attack": 3 } },
|
|
11
|
+
{ "id": "shadow_blade", "name": "Shadow Blade", "rarity": "unique", "weight": 3, "statBonus": { "attack": 20, "speed": 5 } },
|
|
12
|
+
{ "id": "dragon_scale", "name": "Dragon Scale", "rarity": "unique", "weight": 2, "statBonus": { "defense": 15, "hp": 50 } }
|
|
13
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** This game's default loot pool — fantasy RPG items. */
|
|
2
|
+
export const DEFAULT_ITEM_POOL = [
|
|
3
|
+
{ id: 'rusty_sword', name: 'Rusty Sword', rarity: 'common', weight: 30, statBonus: { attack: 3 } },
|
|
4
|
+
{ id: 'wooden_shield', name: 'Wooden Shield', rarity: 'common', weight: 30, statBonus: { defense: 2 } },
|
|
5
|
+
{ id: 'health_potion', name: 'Health Potion', rarity: 'common', weight: 25, statBonus: { hp: 20 } },
|
|
6
|
+
{ id: 'leather_boots', name: 'Leather Boots', rarity: 'common', weight: 20, statBonus: { speed: 1 } },
|
|
7
|
+
{ id: 'iron_ring', name: 'Iron Ring', rarity: 'common', weight: 15, statBonus: { attack: 1, defense: 1 } },
|
|
8
|
+
{ id: 'silver_dagger', name: 'Silver Dagger', rarity: 'rare', weight: 10, statBonus: { attack: 8 } },
|
|
9
|
+
{ id: 'chain_mail', name: 'Chain Mail', rarity: 'rare', weight: 10, statBonus: { defense: 7 } },
|
|
10
|
+
{ id: 'elixir_of_speed', name: 'Elixir of Speed', rarity: 'rare', weight: 8, statBonus: { speed: 4, attack: 2 } },
|
|
11
|
+
{ id: 'ruby_amulet', name: 'Ruby Amulet', rarity: 'rare', weight: 7, statBonus: { hp: 30, attack: 3 } },
|
|
12
|
+
{ id: 'shadow_blade', name: 'Shadow Blade', rarity: 'unique', weight: 3, statBonus: { attack: 20, speed: 5 } },
|
|
13
|
+
{ id: 'dragon_scale', name: 'Dragon Scale', rarity: 'unique', weight: 2, statBonus: { defense: 15, hp: 50 } },
|
|
14
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PhysicsConfig } from '@loonylabs/gamedev-core';
|
|
2
|
+
|
|
3
|
+
/** Physics config for dungeon areas — tighter ground snap for flat floors. */
|
|
4
|
+
export const DUNGEON_PHYSICS_CONFIG: PhysicsConfig = {
|
|
5
|
+
gravity: -20,
|
|
6
|
+
terminalVelocity: -50,
|
|
7
|
+
groundSnapThreshold: 0.3,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Constant heights for dungeon cells — must match dungeon mesh geometry. */
|
|
11
|
+
export const DUNGEON_FLOOR_Y = 0;
|
|
12
|
+
export const DUNGEON_WALL_Y = 10.0;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { JumpConfig } from '@loonylabs/gamedev-core';
|
|
2
|
+
|
|
3
|
+
/** Default jump config for normal gameplay. */
|
|
4
|
+
export const GAME_JUMP_CONFIG: JumpConfig = {
|
|
5
|
+
jumpVelocity: 10,
|
|
6
|
+
coyoteTime: 0.1,
|
|
7
|
+
jumpBuffer: 0.1,
|
|
8
|
+
allowDoubleJump: false,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Obby/platformer mode: higher jumps, double jump enabled. */
|
|
12
|
+
export const OBBY_JUMP_CONFIG: JumpConfig = {
|
|
13
|
+
jumpVelocity: 14,
|
|
14
|
+
coyoteTime: 0.15,
|
|
15
|
+
jumpBuffer: 0.15,
|
|
16
|
+
allowDoubleJump: true,
|
|
17
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "iron_sword",
|
|
4
|
+
"name": "Iron Sword",
|
|
5
|
+
"rarity": "rare",
|
|
6
|
+
"statBonus": { "attack": 12 },
|
|
7
|
+
"pattern": [
|
|
8
|
+
"rusty_sword", "rusty_sword", null,
|
|
9
|
+
"wooden_shield", null, null,
|
|
10
|
+
null, null, null
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "warriors_ring",
|
|
15
|
+
"name": "Warrior's Ring",
|
|
16
|
+
"rarity": "rare",
|
|
17
|
+
"statBonus": { "attack": 5, "defense": 5 },
|
|
18
|
+
"pattern": [
|
|
19
|
+
"iron_ring", "iron_ring", null,
|
|
20
|
+
"iron_ring", null, null,
|
|
21
|
+
null, null, null
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "fortified_shield",
|
|
26
|
+
"name": "Fortified Shield",
|
|
27
|
+
"rarity": "rare",
|
|
28
|
+
"statBonus": { "defense": 12 },
|
|
29
|
+
"pattern": [
|
|
30
|
+
"wooden_shield", "wooden_shield", null,
|
|
31
|
+
"wooden_shield", null, null,
|
|
32
|
+
null, null, null
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "grand_elixir",
|
|
37
|
+
"name": "Grand Elixir",
|
|
38
|
+
"rarity": "rare",
|
|
39
|
+
"statBonus": { "hp": 60, "speed": 2 },
|
|
40
|
+
"pattern": [
|
|
41
|
+
"health_potion", "health_potion", null,
|
|
42
|
+
"elixir_of_speed", null, null,
|
|
43
|
+
null, null, null
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "shadow_ring",
|
|
48
|
+
"name": "Shadow Ring",
|
|
49
|
+
"rarity": "unique",
|
|
50
|
+
"statBonus": { "attack": 15, "defense": 8, "speed": 3 },
|
|
51
|
+
"pattern": [
|
|
52
|
+
"silver_dagger", "iron_ring", null,
|
|
53
|
+
"iron_ring", null, null,
|
|
54
|
+
null, null, null
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "dragon_sword",
|
|
59
|
+
"name": "Dragon Sword",
|
|
60
|
+
"rarity": "unique",
|
|
61
|
+
"statBonus": { "attack": 25, "hp": 30 },
|
|
62
|
+
"pattern": [
|
|
63
|
+
"shadow_blade", "rusty_sword", null,
|
|
64
|
+
"ruby_amulet", null, null,
|
|
65
|
+
null, null, null
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
]
|