@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,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "room_basecamp",
|
|
3
|
+
"ascii": [
|
|
4
|
+
"WWWWWWWWWWWWWWWWWWWWW",
|
|
5
|
+
"W...................W",
|
|
6
|
+
"W...................W",
|
|
7
|
+
"W.....NNNNN.........W",
|
|
8
|
+
"W..........S........W",
|
|
9
|
+
"W....SSSSSSSSS......W",
|
|
10
|
+
"W.........F.........W",
|
|
11
|
+
"W...................W",
|
|
12
|
+
"W...................W",
|
|
13
|
+
"WWWWWWWWWWW>WWWWWWWWW"
|
|
14
|
+
],
|
|
15
|
+
"tags": ["basecamp", "safe"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"basic-shot": {
|
|
3
|
+
"name": "Basic Shot",
|
|
4
|
+
"actionType": "raycast",
|
|
5
|
+
"cooldown": 500,
|
|
6
|
+
"stats": { "damage": 10, "range": 20 }
|
|
7
|
+
},
|
|
8
|
+
"heavy-slam": {
|
|
9
|
+
"name": "Heavy Slam",
|
|
10
|
+
"actionType": "melee",
|
|
11
|
+
"cooldown": 3000,
|
|
12
|
+
"stats": { "damage": 40, "range": 3 }
|
|
13
|
+
},
|
|
14
|
+
"burst-heal": {
|
|
15
|
+
"name": "Burst Heal",
|
|
16
|
+
"actionType": "self_heal",
|
|
17
|
+
"cooldown": 15000,
|
|
18
|
+
"stats": { "value": 30 }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { VoxelTerrainOptions } from '@loonylabs/gamedev-core';
|
|
2
|
+
import { VOXEL_MATERIALS } from './materials.js';
|
|
3
|
+
|
|
4
|
+
/** Terrain generation presets per biome. */
|
|
5
|
+
export const BIOME_TERRAIN_OPTIONS: Record<string, VoxelTerrainOptions> = {
|
|
6
|
+
plains: {
|
|
7
|
+
baseHeight: 32,
|
|
8
|
+
amplitude: 8,
|
|
9
|
+
frequency: 0.015,
|
|
10
|
+
octaves: 4,
|
|
11
|
+
caveThreshold: 0.2,
|
|
12
|
+
materialLayers: [
|
|
13
|
+
{ material: VOXEL_MATERIALS.GRASS, maxDepthBelowSurface: 2 },
|
|
14
|
+
{ material: VOXEL_MATERIALS.DIRT, maxDepthBelowSurface: 6 },
|
|
15
|
+
{ material: VOXEL_MATERIALS.STONE, maxDepthBelowSurface: Infinity },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
basecamp: {
|
|
19
|
+
baseHeight: 34,
|
|
20
|
+
amplitude: 1,
|
|
21
|
+
frequency: 0.005,
|
|
22
|
+
octaves: 2,
|
|
23
|
+
caveThreshold: 0,
|
|
24
|
+
materialLayers: [
|
|
25
|
+
{ material: VOXEL_MATERIALS.GRASS, maxDepthBelowSurface: 2 },
|
|
26
|
+
{ material: VOXEL_MATERIALS.DIRT, maxDepthBelowSurface: 6 },
|
|
27
|
+
{ material: VOXEL_MATERIALS.STONE, maxDepthBelowSurface: Infinity },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
wall: {
|
|
31
|
+
baseHeight: 60,
|
|
32
|
+
amplitude: 15,
|
|
33
|
+
frequency: 0.03,
|
|
34
|
+
octaves: 5,
|
|
35
|
+
caveThreshold: 0,
|
|
36
|
+
materialLayers: [
|
|
37
|
+
{ material: VOXEL_MATERIALS.SNOW, maxDepthBelowSurface: 0 },
|
|
38
|
+
{ material: VOXEL_MATERIALS.STONE, maxDepthBelowSurface: Infinity },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
swamp: {
|
|
42
|
+
baseHeight: 30,
|
|
43
|
+
amplitude: 1.5,
|
|
44
|
+
frequency: 0.02,
|
|
45
|
+
octaves: 3,
|
|
46
|
+
caveThreshold: 0,
|
|
47
|
+
waterLevel: 31,
|
|
48
|
+
materialLayers: [
|
|
49
|
+
{ material: VOXEL_MATERIALS.DIRT, maxDepthBelowSurface: 2 },
|
|
50
|
+
{ material: VOXEL_MATERIALS.DIRT, maxDepthBelowSurface: 5 },
|
|
51
|
+
{ material: VOXEL_MATERIALS.STONE, maxDepthBelowSurface: Infinity },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
mountains: {
|
|
55
|
+
baseHeight: 40,
|
|
56
|
+
amplitude: 20,
|
|
57
|
+
frequency: 0.025,
|
|
58
|
+
octaves: 6,
|
|
59
|
+
caveThreshold: 0.4,
|
|
60
|
+
materialLayers: [
|
|
61
|
+
{ material: VOXEL_MATERIALS.SNOW, maxDepthBelowSurface: 0 },
|
|
62
|
+
{ material: VOXEL_MATERIALS.STONE, maxDepthBelowSurface: Infinity },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
desert: {
|
|
66
|
+
baseHeight: 30,
|
|
67
|
+
amplitude: 5,
|
|
68
|
+
frequency: 0.01,
|
|
69
|
+
octaves: 3,
|
|
70
|
+
caveThreshold: 0.1,
|
|
71
|
+
materialLayers: [
|
|
72
|
+
{ material: VOXEL_MATERIALS.SAND, maxDepthBelowSurface: 3 },
|
|
73
|
+
{ material: VOXEL_MATERIALS.STONE, maxDepthBelowSurface: Infinity },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Voxel material indices — must match what the shader expects. */
|
|
2
|
+
export const VOXEL_MATERIALS = {
|
|
3
|
+
AIR: 0,
|
|
4
|
+
GRASS: 1,
|
|
5
|
+
DIRT: 2,
|
|
6
|
+
STONE: 3,
|
|
7
|
+
SAND: 4,
|
|
8
|
+
SNOW: 5,
|
|
9
|
+
WATER: 6,
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export type VoxelMaterialName = keyof typeof VOXEL_MATERIALS;
|
|
13
|
+
|
|
14
|
+
/** Visual properties per material (for the terrain shader). */
|
|
15
|
+
export const VOXEL_MATERIAL_COLORS: Record<number, number> = {
|
|
16
|
+
[VOXEL_MATERIALS.GRASS]: 0x4a8c3f,
|
|
17
|
+
[VOXEL_MATERIALS.DIRT]: 0x8b6914,
|
|
18
|
+
[VOXEL_MATERIALS.STONE]: 0x808080,
|
|
19
|
+
[VOXEL_MATERIALS.SAND]: 0xc2b280,
|
|
20
|
+
[VOXEL_MATERIALS.SNOW]: 0xf0f0f0,
|
|
21
|
+
[VOXEL_MATERIALS.WATER]: 0x3366aa,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Extended visual properties for the procedural terrain shader. */
|
|
25
|
+
export interface VoxelMaterialVisual {
|
|
26
|
+
color: [number, number, number]; // RGB 0-1
|
|
27
|
+
noiseScale: number;
|
|
28
|
+
noiseStrength: number;
|
|
29
|
+
noiseType: 'fbm' | 'cellular' | 'ridged';
|
|
30
|
+
fbmOctaves: number; // 2-6
|
|
31
|
+
fbmLacunarity: number; // frequency multiplier per octave (typically 2.0)
|
|
32
|
+
fbmGain: number; // amplitude multiplier per octave (typically 0.5)
|
|
33
|
+
heightTintLow: [number, number, number]; // RGB offset at low altitude
|
|
34
|
+
heightTintHigh: [number, number, number]; // RGB offset at high altitude
|
|
35
|
+
bumpStrength: number; // 0.0 (flat) to 1.0 (very bumpy) — controls normal perturbation
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const VOXEL_MATERIAL_VISUALS: Record<number, VoxelMaterialVisual> = {
|
|
39
|
+
[VOXEL_MATERIALS.GRASS]: { color: [0.29, 0.55, 0.25], noiseScale: 3.0, noiseStrength: 0.45, noiseType: 'fbm', fbmOctaves: 5, fbmLacunarity: 2.0, fbmGain: 0.5, heightTintLow: [0.0, 0.03, 0.0], heightTintHigh: [-0.06, 0.03, 0.02], bumpStrength: 0.4 },
|
|
40
|
+
[VOXEL_MATERIALS.DIRT]: { color: [0.55, 0.41, 0.08], noiseScale: 4.0, noiseStrength: 0.50, noiseType: 'fbm', fbmOctaves: 4, fbmLacunarity: 2.2, fbmGain: 0.45, heightTintLow: [-0.02, -0.01, 0.0], heightTintHigh: [0.03, 0.02, 0.01], bumpStrength: 0.5 },
|
|
41
|
+
[VOXEL_MATERIALS.STONE]: { color: [0.50, 0.50, 0.50], noiseScale: 1.5, noiseStrength: 0.50, noiseType: 'cellular', fbmOctaves: 3, fbmLacunarity: 2.0, fbmGain: 0.5, heightTintLow: [-0.04, -0.02, 0.0], heightTintHigh: [0.06, 0.06, 0.06], bumpStrength: 0.8 },
|
|
42
|
+
[VOXEL_MATERIALS.SAND]: { color: [0.76, 0.70, 0.50], noiseScale: 2.0, noiseStrength: 0.12, noiseType: 'fbm', fbmOctaves: 2, fbmLacunarity: 2.0, fbmGain: 0.5, heightTintLow: [0.0, 0.0, 0.0], heightTintHigh: [0.02, 0.01, -0.02], bumpStrength: 0.2 },
|
|
43
|
+
[VOXEL_MATERIALS.SNOW]: { color: [0.94, 0.94, 0.94], noiseScale: 1.0, noiseStrength: 0.06, noiseType: 'fbm', fbmOctaves: 2, fbmLacunarity: 2.0, fbmGain: 0.5, heightTintLow: [-0.02, -0.02, 0.0], heightTintHigh: [0.0, 0.0, 0.02], bumpStrength: 0.1 },
|
|
44
|
+
[VOXEL_MATERIALS.WATER]: { color: [0.20, 0.40, 0.67], noiseScale: 0.8, noiseStrength: 0.20, noiseType: 'ridged', fbmOctaves: 3, fbmLacunarity: 2.0, fbmGain: 0.5, heightTintLow: [0.0, 0.0, 0.0], heightTintHigh: [0.0, 0.0, 0.0], bumpStrength: 0.0 },
|
|
45
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { VoxelTerrainOptions } from '@loonylabs/gamedev-core';
|
|
2
|
+
import type { PhysicsConfig } from '@loonylabs/gamedev-core';
|
|
3
|
+
import { BIOME_TERRAIN_OPTIONS } from './biome-terrain.js';
|
|
4
|
+
|
|
5
|
+
/** Terrain generation options for the voxel sandbox area. */
|
|
6
|
+
export const SANDBOX_TERRAIN_OPTIONS: VoxelTerrainOptions = {
|
|
7
|
+
...BIOME_TERRAIN_OPTIONS.plains,
|
|
8
|
+
caveThreshold: 0, // no caves for sandbox — keep it simple
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Physics config for the voxel sandbox. */
|
|
12
|
+
export const SANDBOX_PHYSICS_CONFIG: PhysicsConfig = {
|
|
13
|
+
gravity: -20,
|
|
14
|
+
terminalVelocity: -50,
|
|
15
|
+
groundSnapThreshold: 0.5,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Spawn position (world coordinates, center of chunk 0,0). */
|
|
19
|
+
export const SANDBOX_SPAWN = { x: 16, z: 16 };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SafePoint, BorderConfig } from '@loonylabs/gamedev-core';
|
|
2
|
+
|
|
3
|
+
/** Safe points per area — positions that must remain obstacle-free. */
|
|
4
|
+
export const AREA_SAFE_POINTS: Record<string, SafePoint[]> = {
|
|
5
|
+
ow1: [{ wx: 2, wy: 5 }], // OW1 basecamp spawn
|
|
6
|
+
ow2: [{ wx: 2, wy: 47 }], // OW2 west-corridor spawn (zone 0,1 row 15 = world y 47)
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** Border configurations per area. */
|
|
10
|
+
export const AREA_BORDER_CONFIGS: Record<string, BorderConfig> = {
|
|
11
|
+
ow1: {
|
|
12
|
+
gridSize: 3,
|
|
13
|
+
borderCellType: 'wall',
|
|
14
|
+
borderObstacleType: 'rock',
|
|
15
|
+
corridors: [{
|
|
16
|
+
edge: 'east',
|
|
17
|
+
zoneIndex: 1, // ty === 1
|
|
18
|
+
cellPositions: [15, 16],
|
|
19
|
+
cellType: 'transition',
|
|
20
|
+
}],
|
|
21
|
+
},
|
|
22
|
+
ow2: {
|
|
23
|
+
gridSize: 3,
|
|
24
|
+
borderCellType: 'wall',
|
|
25
|
+
borderObstacleType: 'rock',
|
|
26
|
+
corridors: [{
|
|
27
|
+
edge: 'west',
|
|
28
|
+
zoneIndex: 1,
|
|
29
|
+
cellPositions: [15, 16],
|
|
30
|
+
cellType: 'transition',
|
|
31
|
+
}],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Game-specific biome definition for the dungeon-crawler reference implementation.
|
|
3
|
+
* Extends core's minimal BiomeDef with rendering and gameplay fields.
|
|
4
|
+
*/
|
|
5
|
+
export interface GameBiomeDef {
|
|
6
|
+
id: string;
|
|
7
|
+
groundColor: string; // CSS hex color for floor tiles
|
|
8
|
+
heightScale: number; // max height generated by noise (0 = flat)
|
|
9
|
+
noiseFrequency: number; // noise input scale
|
|
10
|
+
fogColor: string; // scene fog color
|
|
11
|
+
fogDensity: number; // THREE.FogExp2 density
|
|
12
|
+
mobs: string[]; // mob type IDs spawnable in this biome
|
|
13
|
+
obstacleDensity: number; // probability (0.0 - 1.0) of a floor tile having an obstacle
|
|
14
|
+
obstacleTypes: string[]; // obstacle IDs spawnable in this biome
|
|
15
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"grasslands": {
|
|
3
|
+
"id": "grasslands",
|
|
4
|
+
"groundColor": "#3a5a1a",
|
|
5
|
+
"heightScale": 2.0,
|
|
6
|
+
"noiseFrequency": 0.05,
|
|
7
|
+
"fogColor": "#a0b080",
|
|
8
|
+
"fogDensity": 0.018,
|
|
9
|
+
"mobs": ["wolf", "bandit", "slime"],
|
|
10
|
+
"obstacleDensity": 0.12,
|
|
11
|
+
"obstacleTypes": ["tree", "rock", "bush"]
|
|
12
|
+
},
|
|
13
|
+
"mountains": {
|
|
14
|
+
"id": "mountains",
|
|
15
|
+
"groundColor": "#888888",
|
|
16
|
+
"heightScale": 8.0,
|
|
17
|
+
"noiseFrequency": 0.1,
|
|
18
|
+
"fogColor": "#d0d8e0",
|
|
19
|
+
"fogDensity": 0.025,
|
|
20
|
+
"mobs": ["harpy", "golem"],
|
|
21
|
+
"obstacleDensity": 0.08,
|
|
22
|
+
"obstacleTypes": ["rock"]
|
|
23
|
+
},
|
|
24
|
+
"dungeon": {
|
|
25
|
+
"id": "dungeon",
|
|
26
|
+
"groundColor": "#2a1a0a",
|
|
27
|
+
"heightScale": 0.0,
|
|
28
|
+
"noiseFrequency": 0.0,
|
|
29
|
+
"fogColor": "#110800",
|
|
30
|
+
"fogDensity": 0.04,
|
|
31
|
+
"mobs": ["grunt", "skeleton"],
|
|
32
|
+
"obstacleDensity": 0.0,
|
|
33
|
+
"obstacleTypes": []
|
|
34
|
+
},
|
|
35
|
+
"desert": {
|
|
36
|
+
"id": "desert",
|
|
37
|
+
"groundColor": "#c8a050",
|
|
38
|
+
"heightScale": 3.0,
|
|
39
|
+
"noiseFrequency": 0.04,
|
|
40
|
+
"fogColor": "#e8c880",
|
|
41
|
+
"fogDensity": 0.015,
|
|
42
|
+
"mobs": ["scorpion", "bandit"],
|
|
43
|
+
"obstacleDensity": 0.05,
|
|
44
|
+
"obstacleTypes": ["rock", "bush"]
|
|
45
|
+
},
|
|
46
|
+
"swamp": {
|
|
47
|
+
"id": "swamp",
|
|
48
|
+
"groundColor": "#1a2b1a",
|
|
49
|
+
"heightScale": 1.5,
|
|
50
|
+
"noiseFrequency": 0.08,
|
|
51
|
+
"fogColor": "#2a3a1a",
|
|
52
|
+
"fogDensity": 0.045,
|
|
53
|
+
"mobs": ["skeleton", "golem"],
|
|
54
|
+
"obstacleDensity": 0.18,
|
|
55
|
+
"obstacleTypes": ["dead_tree", "bog_stone"]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
parseOverworldLayout,
|
|
4
|
+
getTileAt,
|
|
5
|
+
getBiomeTerrainOptions,
|
|
6
|
+
OVERWORLD_ASCII,
|
|
7
|
+
TILE_LEGEND,
|
|
8
|
+
} from './overworld-layout.js';
|
|
9
|
+
|
|
10
|
+
describe('parseOverworldLayout', () => {
|
|
11
|
+
const layout = parseOverworldLayout();
|
|
12
|
+
|
|
13
|
+
test('parses correct dimensions', () => {
|
|
14
|
+
expect(layout.width).toBe(8);
|
|
15
|
+
expect(layout.height).toBe(6);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('all tiles have valid definitions', () => {
|
|
19
|
+
for (const row of layout.tiles) {
|
|
20
|
+
for (const tile of row) {
|
|
21
|
+
expect(tile.def).toBeDefined();
|
|
22
|
+
expect(tile.def.biomeId).toBeTruthy();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('corners are walls', () => {
|
|
28
|
+
expect(getTileAt(layout, 0, 0)?.def.type).toBe('W');
|
|
29
|
+
expect(getTileAt(layout, 7, 0)?.def.type).toBe('W');
|
|
30
|
+
expect(getTileAt(layout, 0, 5)?.def.type).toBe('W');
|
|
31
|
+
expect(getTileAt(layout, 7, 5)?.def.type).toBe('W');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('basecamp tiles at expected position', () => {
|
|
35
|
+
expect(getTileAt(layout, 1, 1)?.def.type).toBe('B');
|
|
36
|
+
expect(getTileAt(layout, 2, 1)?.def.type).toBe('B');
|
|
37
|
+
expect(getTileAt(layout, 1, 1)?.def.biomeId).toBe('basecamp');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('dungeon entrance exists', () => {
|
|
41
|
+
const dungeon = getTileAt(layout, 5, 4);
|
|
42
|
+
expect(dungeon?.def.type).toBe('D');
|
|
43
|
+
expect(dungeon?.def.biomeId).toBe('swamp');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('passage tile exists', () => {
|
|
47
|
+
const passage = getTileAt(layout, 4, 1);
|
|
48
|
+
expect(passage?.def.type).toBe('T');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('wilderness tiles spawn mobs', () => {
|
|
52
|
+
const grassTile = getTileAt(layout, 3, 2);
|
|
53
|
+
expect(grassTile?.def.spawnsMobs).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('wall tiles do not spawn mobs', () => {
|
|
57
|
+
const wallTile = getTileAt(layout, 0, 0);
|
|
58
|
+
expect(wallTile?.def.spawnsMobs).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('out of bounds returns undefined', () => {
|
|
62
|
+
expect(getTileAt(layout, -1, 0)).toBeUndefined();
|
|
63
|
+
expect(getTileAt(layout, 8, 0)).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('throws on unknown char', () => {
|
|
67
|
+
expect(() => parseOverworldLayout('X')).toThrow('Unknown tile char');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('getBiomeTerrainOptions', () => {
|
|
72
|
+
test('returns options for all biomes used in layout', () => {
|
|
73
|
+
const biomeIds = new Set<string>();
|
|
74
|
+
for (const def of Object.values(TILE_LEGEND)) {
|
|
75
|
+
biomeIds.add(def.biomeId);
|
|
76
|
+
}
|
|
77
|
+
for (const id of biomeIds) {
|
|
78
|
+
const opts = getBiomeTerrainOptions(id);
|
|
79
|
+
expect(opts).toBeDefined();
|
|
80
|
+
expect(opts.baseHeight).toBeGreaterThan(0);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('wall terrain is significantly higher than plains', () => {
|
|
85
|
+
const wall = getBiomeTerrainOptions('wall');
|
|
86
|
+
const plains = getBiomeTerrainOptions('plains');
|
|
87
|
+
expect(wall.baseHeight! - plains.baseHeight!).toBeGreaterThanOrEqual(20);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('throws for unknown biome', () => {
|
|
91
|
+
expect(() => getBiomeTerrainOptions('nonexistent')).toThrow('Unknown biome');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { VoxelTerrainOptions, FlatZone } from '@loonylabs/gamedev-core';
|
|
2
|
+
import { BIOME_TERRAIN_OPTIONS } from '../voxel/biome-terrain.js';
|
|
3
|
+
|
|
4
|
+
/** Tile types in the overworld ASCII layout. */
|
|
5
|
+
export type OverworldTileType = 'B' | '.' | 'W' | 'D' | 'T';
|
|
6
|
+
|
|
7
|
+
export interface OverworldTileDef {
|
|
8
|
+
type: OverworldTileType;
|
|
9
|
+
biomeId: string;
|
|
10
|
+
spawnsMobs: boolean;
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Legend mapping ASCII chars to tile definitions. */
|
|
15
|
+
export const TILE_LEGEND: Record<string, OverworldTileDef> = {
|
|
16
|
+
'B': { type: 'B', biomeId: 'basecamp', spawnsMobs: false, label: 'Basecamp' },
|
|
17
|
+
'g': { type: '.', biomeId: 'plains', spawnsMobs: true, label: 'Grasslands' },
|
|
18
|
+
's': { type: '.', biomeId: 'swamp', spawnsMobs: true, label: 'Swamp' },
|
|
19
|
+
'W': { type: 'W', biomeId: 'wall', spawnsMobs: false, label: 'Wall' },
|
|
20
|
+
'D': { type: 'D', biomeId: 'swamp', spawnsMobs: false, label: 'Dungeon Entrance' },
|
|
21
|
+
'T': { type: 'T', biomeId: 'plains', spawnsMobs: false, label: 'Passage' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 8×6 overworld layout.
|
|
26
|
+
* Grasslands (g) on the left, swamp (s) on the right.
|
|
27
|
+
* Wall mountains (W) surround the map, with a passage (T) between biomes.
|
|
28
|
+
*/
|
|
29
|
+
export const OVERWORLD_ASCII = [
|
|
30
|
+
'WWWWWWWW',
|
|
31
|
+
'WBBgTssW',
|
|
32
|
+
'WggggssW',
|
|
33
|
+
'WggggssW',
|
|
34
|
+
'WgggsDsW',
|
|
35
|
+
'WWWWWWWW',
|
|
36
|
+
].join('\n');
|
|
37
|
+
|
|
38
|
+
export interface ParsedTile {
|
|
39
|
+
col: number;
|
|
40
|
+
row: number;
|
|
41
|
+
char: string;
|
|
42
|
+
def: OverworldTileDef;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface OverworldLayout {
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
tiles: ParsedTile[][]; // [row][col]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Parse the ASCII layout into a structured grid. */
|
|
52
|
+
export function parseOverworldLayout(ascii: string = OVERWORLD_ASCII): OverworldLayout {
|
|
53
|
+
const rows = ascii.split('\n').filter(r => r.length > 0);
|
|
54
|
+
const height = rows.length;
|
|
55
|
+
const width = rows[0].length;
|
|
56
|
+
const tiles: ParsedTile[][] = [];
|
|
57
|
+
|
|
58
|
+
for (let row = 0; row < height; row++) {
|
|
59
|
+
const tileRow: ParsedTile[] = [];
|
|
60
|
+
for (let col = 0; col < width; col++) {
|
|
61
|
+
const char = rows[row][col];
|
|
62
|
+
const def = TILE_LEGEND[char];
|
|
63
|
+
if (!def) {
|
|
64
|
+
throw new Error(`Unknown tile char '${char}' at (${col}, ${row})`);
|
|
65
|
+
}
|
|
66
|
+
tileRow.push({ col, row, char, def });
|
|
67
|
+
}
|
|
68
|
+
tiles.push(tileRow);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { width, height, tiles };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Get the tile definition at a tile coordinate, or undefined if out of bounds. */
|
|
75
|
+
export function getTileAt(layout: OverworldLayout, col: number, row: number): ParsedTile | undefined {
|
|
76
|
+
if (row < 0 || row >= layout.height || col < 0 || col >= layout.width) return undefined;
|
|
77
|
+
return layout.tiles[row][col];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Get the VoxelTerrainOptions for a biome ID. */
|
|
81
|
+
export function getBiomeTerrainOptions(biomeId: string): VoxelTerrainOptions {
|
|
82
|
+
const options = BIOME_TERRAIN_OPTIONS[biomeId];
|
|
83
|
+
if (!options) throw new Error(`Unknown biome '${biomeId}'`);
|
|
84
|
+
return options;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** World units per tile (= one voxel chunk width). */
|
|
88
|
+
export const TILE_SIZE = 32;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Compute flat zones for structure placement from the tile layout.
|
|
92
|
+
* Basecamp tiles get a large flat zone, dungeon entrance gets a smaller one.
|
|
93
|
+
*/
|
|
94
|
+
export function getStructureFlatZones(layout: OverworldLayout): FlatZone[] {
|
|
95
|
+
const zones: FlatZone[] = [];
|
|
96
|
+
const basecampHeight = BIOME_TERRAIN_OPTIONS.basecamp.baseHeight ?? 34;
|
|
97
|
+
|
|
98
|
+
// Basecamp: one big flat zone covering all B tiles
|
|
99
|
+
const basecampTiles = layout.tiles.flat().filter(t => t.def.type === 'B');
|
|
100
|
+
if (basecampTiles.length > 0) {
|
|
101
|
+
const cx = basecampTiles.reduce((s, t) => s + (t.col + 0.5) * TILE_SIZE, 0) / basecampTiles.length;
|
|
102
|
+
const cz = basecampTiles.reduce((s, t) => s + (t.row + 0.5) * TILE_SIZE, 0) / basecampTiles.length;
|
|
103
|
+
// Flat radius covers all basecamp tiles + some margin
|
|
104
|
+
const flatRadius = Math.max(basecampTiles.length, 2) * TILE_SIZE * 0.5;
|
|
105
|
+
zones.push({ x: cx, z: cz, flatRadius, falloffRadius: 10, height: basecampHeight });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Dungeon entrance: small flat zone around portal
|
|
109
|
+
const dungeonTile = layout.tiles.flat().find(t => t.def.type === 'D');
|
|
110
|
+
if (dungeonTile) {
|
|
111
|
+
const px = (dungeonTile.col + 0.5) * TILE_SIZE;
|
|
112
|
+
const pz = (dungeonTile.row + 0.5) * TILE_SIZE;
|
|
113
|
+
const portalHeight = BIOME_TERRAIN_OPTIONS[dungeonTile.def.biomeId]?.baseHeight ?? 30;
|
|
114
|
+
zones.push({ x: px, z: pz, flatRadius: 5, falloffRadius: 8, height: portalHeight });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return zones;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Maps tile biome IDs to the mob list biome keys from biomes.json.
|
|
122
|
+
* Only biomes that spawn mobs need an entry.
|
|
123
|
+
*/
|
|
124
|
+
export const BIOME_MOB_MAPPING: Record<string, string> = {
|
|
125
|
+
plains: 'grasslands',
|
|
126
|
+
swamp: 'swamp',
|
|
127
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gamedev-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch --clear-screen=false src/index.ts",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"test": "vitest run"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@loonylabs/game-data": "workspace:*",
|
|
14
|
+
"@loonylabs/gamedev-core": "workspace:*",
|
|
15
|
+
"@loonylabs/gamedev-server": "workspace:*",
|
|
16
|
+
"@loonylabs/gamedev-protocol": "workspace:*",
|
|
17
|
+
"better-sqlite3": "^12.8.0",
|
|
18
|
+
"drizzle-orm": "^0.45.2",
|
|
19
|
+
"socket.io": "^4.7.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
23
|
+
"@types/node": "^20.0.0",
|
|
24
|
+
"drizzle-kit": "^0.31.10",
|
|
25
|
+
"socket.io-client": "^4.7.0",
|
|
26
|
+
"tsx": "^4.7.0",
|
|
27
|
+
"typescript": "^5.0.0",
|
|
28
|
+
"vitest": "^1.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|