@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,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file renderer/entities.ts
|
|
3
|
+
* Entity renderer — creates and updates Three.js meshes for game entities.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Maintain a mesh-per-entity map (create on first sight, remove when gone)
|
|
7
|
+
* - Update mesh position, rotation (with smooth lerp) and color each frame
|
|
8
|
+
* - Track previous positions to compute movement direction for rotation
|
|
9
|
+
*
|
|
10
|
+
* What does NOT belong here:
|
|
11
|
+
* - Game logic / ECS (→ packages/core)
|
|
12
|
+
* - Dungeon mesh building (→ renderer/dungeon.ts)
|
|
13
|
+
* - Camera or input handling
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as THREE from 'three';
|
|
17
|
+
import type { EntitySnapshot } from '@loonylabs/gamedev-protocol';
|
|
18
|
+
import { store } from '../store.js';
|
|
19
|
+
|
|
20
|
+
interface EnemyVisualDef {
|
|
21
|
+
bodyColor: number;
|
|
22
|
+
headColor: number;
|
|
23
|
+
scale: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ENEMY_VISUALS: Record<string, EnemyVisualDef> = {
|
|
27
|
+
player: { bodyColor: 0x4488ff, headColor: 0x66aaff, scale: 1.0 },
|
|
28
|
+
grunt: { bodyColor: 0xcc4444, headColor: 0xee6666, scale: 1.0 },
|
|
29
|
+
wolf: { bodyColor: 0x888888, headColor: 0x999999, scale: 0.7 },
|
|
30
|
+
bandit: { bodyColor: 0x8B4513, headColor: 0xaa6633, scale: 1.0 },
|
|
31
|
+
skeleton: { bodyColor: 0xe8e0d0, headColor: 0xf0e8d8, scale: 0.9 },
|
|
32
|
+
golem: { bodyColor: 0x607060, headColor: 0x708070, scale: 1.6 },
|
|
33
|
+
// Shooter enemies
|
|
34
|
+
rusher: { bodyColor: 0xcc2222, headColor: 0xff4444, scale: 0.9 },
|
|
35
|
+
patrol: { bodyColor: 0x8B6914, headColor: 0xaa8833, scale: 1.0 },
|
|
36
|
+
sniper: { bodyColor: 0xcccccc, headColor: 0xeeeeee, scale: 1.1 },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const ITEM_WIDTH = 0.5;
|
|
40
|
+
const ITEM_HEIGHT = 0.3;
|
|
41
|
+
|
|
42
|
+
const ROTATION_SPEED = 12; // rad/s
|
|
43
|
+
const MOVE_THRESHOLD = 0.001;
|
|
44
|
+
|
|
45
|
+
/** Shortest-path angle lerp — avoids the 360° wrap-around spin */
|
|
46
|
+
function lerpAngle(current: number, target: number, t: number): number {
|
|
47
|
+
let delta = ((target - current + Math.PI) % (Math.PI * 2)) - Math.PI;
|
|
48
|
+
if (delta < -Math.PI) delta += Math.PI * 2;
|
|
49
|
+
return current + Math.max(-Math.PI * t, Math.min(Math.PI * t, delta));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildEntityMesh(enemyDefId = 'player'): THREE.Group {
|
|
53
|
+
const vis = ENEMY_VISUALS[enemyDefId] ?? ENEMY_VISUALS['grunt'];
|
|
54
|
+
const group = new THREE.Group();
|
|
55
|
+
|
|
56
|
+
const body = new THREE.Mesh(
|
|
57
|
+
new THREE.CylinderGeometry(0.3 * vis.scale, 0.35 * vis.scale, 1.0 * vis.scale, 8),
|
|
58
|
+
new THREE.MeshLambertMaterial({ color: vis.bodyColor }),
|
|
59
|
+
);
|
|
60
|
+
body.position.y = 0.5 * vis.scale;
|
|
61
|
+
|
|
62
|
+
const head = new THREE.Mesh(
|
|
63
|
+
new THREE.SphereGeometry(0.25 * vis.scale, 8, 6),
|
|
64
|
+
new THREE.MeshLambertMaterial({ color: vis.headColor }),
|
|
65
|
+
);
|
|
66
|
+
head.position.y = (0.5 + 0.25 + 0.25) * vis.scale;
|
|
67
|
+
|
|
68
|
+
// Direction indicator — small cone pointing forward (along +Z in local space)
|
|
69
|
+
const coneGeo = new THREE.ConeGeometry(0.12 * vis.scale, 0.4 * vis.scale, 6);
|
|
70
|
+
coneGeo.rotateX(Math.PI / 2); // tip points along +Z
|
|
71
|
+
const coneMat = new THREE.MeshLambertMaterial({ color: 0x00ff88 });
|
|
72
|
+
const directionCone = new THREE.Mesh(coneGeo, coneMat);
|
|
73
|
+
directionCone.position.set(0, 0.5 * vis.scale, 0.4 * vis.scale);
|
|
74
|
+
|
|
75
|
+
group.add(body, head, directionCone);
|
|
76
|
+
return group;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildItemMesh(): THREE.Mesh {
|
|
80
|
+
const geo = new THREE.BoxGeometry(ITEM_WIDTH, ITEM_HEIGHT, ITEM_WIDTH);
|
|
81
|
+
const mat = new THREE.MeshLambertMaterial({ color: 0xffcc00 });
|
|
82
|
+
return new THREE.Mesh(geo, mat);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class EntityRenderer {
|
|
86
|
+
private groups = new Map<number, THREE.Group>();
|
|
87
|
+
private itemMeshes = new Map<number, THREE.Mesh>();
|
|
88
|
+
private group = new THREE.Group();
|
|
89
|
+
private previousPositions = new Map<number, { x: number; y: number }>();
|
|
90
|
+
private targetRotations = new Map<number, number>();
|
|
91
|
+
|
|
92
|
+
constructor(private scene: THREE.Scene) {
|
|
93
|
+
scene.add(this.group);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
update(entities: EntitySnapshot[], deltaTime: number): void {
|
|
97
|
+
const seen = new Set<number>();
|
|
98
|
+
|
|
99
|
+
for (const e of entities) {
|
|
100
|
+
seen.add(e.entityId);
|
|
101
|
+
|
|
102
|
+
if (e.type === 'item') {
|
|
103
|
+
let mesh = this.itemMeshes.get(e.entityId);
|
|
104
|
+
if (!mesh) {
|
|
105
|
+
mesh = buildItemMesh();
|
|
106
|
+
this.itemMeshes.set(e.entityId, mesh);
|
|
107
|
+
this.group.add(mesh);
|
|
108
|
+
}
|
|
109
|
+
mesh.position.set(e.x, (e.worldY ?? 0) + ITEM_HEIGHT / 2 + 0.1, e.y);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// All non-item entities use the cylinder+sphere group
|
|
114
|
+
let entityGroup = this.groups.get(e.entityId);
|
|
115
|
+
if (!entityGroup) {
|
|
116
|
+
entityGroup = buildEntityMesh(e.enemyDefId ?? e.type ?? 'player');
|
|
117
|
+
this.groups.set(e.entityId, entityGroup);
|
|
118
|
+
this.group.add(entityGroup);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const vis = ENEMY_VISUALS[e.enemyDefId ?? e.type ?? 'player'] ?? ENEMY_VISUALS['grunt'];
|
|
122
|
+
entityGroup.position.set(e.x, (e.worldY ?? 0) + 0.1, e.y);
|
|
123
|
+
this._updateGroupRotation(e, entityGroup, deltaTime, vis.scale);
|
|
124
|
+
this.previousPositions.set(e.entityId, { x: e.x, y: e.y });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Remove entities no longer present
|
|
128
|
+
for (const [id, g] of this.groups) {
|
|
129
|
+
if (!seen.has(id)) {
|
|
130
|
+
this.group.remove(g);
|
|
131
|
+
g.traverse(obj => {
|
|
132
|
+
if (obj instanceof THREE.Mesh) {
|
|
133
|
+
obj.geometry.dispose();
|
|
134
|
+
(obj.material as THREE.Material).dispose();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
this.groups.delete(id);
|
|
138
|
+
this.previousPositions.delete(id);
|
|
139
|
+
this.targetRotations.delete(id);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const [id, mesh] of this.itemMeshes) {
|
|
143
|
+
if (!seen.has(id)) {
|
|
144
|
+
this.group.remove(mesh);
|
|
145
|
+
mesh.geometry.dispose();
|
|
146
|
+
(mesh.material as THREE.Material).dispose();
|
|
147
|
+
this.itemMeshes.delete(id);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private _updateGroupRotation(e: EntitySnapshot, group: THREE.Group, dt: number, scale: number): void {
|
|
153
|
+
const prev = this.previousPositions.get(e.entityId);
|
|
154
|
+
if (prev) {
|
|
155
|
+
const dx = e.x - prev.x;
|
|
156
|
+
const dy = e.y - prev.y;
|
|
157
|
+
if (Math.sqrt(dx * dx + dy * dy) > MOVE_THRESHOLD) {
|
|
158
|
+
if (e.type === 'player') {
|
|
159
|
+
this._updatePlayerFacing(e.entityId, dx, dy);
|
|
160
|
+
} else {
|
|
161
|
+
this.targetRotations.set(e.entityId, Math.atan2(dx, dy));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const target = this.targetRotations.get(e.entityId) ?? group.rotation.y;
|
|
166
|
+
group.rotation.y = lerpAngle(group.rotation.y, target, ROTATION_SPEED * dt);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private _updatePlayerFacing(entityId: number, dx: number, dy: number): void {
|
|
170
|
+
if (store.gameMode.startsWith('wasd')) {
|
|
171
|
+
if (store.playerFacingAngle !== null) {
|
|
172
|
+
this.targetRotations.set(entityId, store.playerFacingAngle);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Diablo/click-to-move: derive facing from movement direction
|
|
177
|
+
const angle = Math.atan2(dx, dy);
|
|
178
|
+
this.targetRotations.set(entityId, angle);
|
|
179
|
+
store.playerFacingAngle = angle;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
dispose(): void {
|
|
183
|
+
for (const g of this.groups.values()) {
|
|
184
|
+
g.traverse(obj => {
|
|
185
|
+
if (obj instanceof THREE.Mesh) {
|
|
186
|
+
obj.geometry.dispose();
|
|
187
|
+
(obj.material as THREE.Material).dispose();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
for (const mesh of this.itemMeshes.values()) {
|
|
192
|
+
mesh.geometry.dispose();
|
|
193
|
+
(mesh.material as THREE.Material).dispose();
|
|
194
|
+
}
|
|
195
|
+
this.scene.remove(this.group);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runner track renderer + client-side track generator.
|
|
3
|
+
*
|
|
4
|
+
* The track generator here is a duplicate of apps/experiences/runner/track/trackGenerator.ts
|
|
5
|
+
* because Vite cannot resolve imports outside of apps/client/. Both use the same seed-based
|
|
6
|
+
* PRNG so they produce identical results.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as THREE from 'three';
|
|
10
|
+
import { mulberry32 } from '@loonylabs/gamedev-core';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Track Generator (client-side duplicate — must match server exactly)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface SegmentDef {
|
|
17
|
+
type: string;
|
|
18
|
+
length: number;
|
|
19
|
+
lanes: [boolean, boolean, boolean];
|
|
20
|
+
gapLength?: number;
|
|
21
|
+
rampHeight?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PlacedSegment {
|
|
25
|
+
def: SegmentDef;
|
|
26
|
+
startZ: number;
|
|
27
|
+
endZ: number;
|
|
28
|
+
index: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TrackGeneratorState {
|
|
32
|
+
seed: number;
|
|
33
|
+
nextZ: number;
|
|
34
|
+
nextIndex: number;
|
|
35
|
+
segments: PlacedSegment[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const STRAIGHT: SegmentDef = { type: 'straight', length: 20, lanes: [true, true, true] };
|
|
39
|
+
const GAP: SegmentDef = { type: 'gap', length: 20, lanes: [true, true, true], gapLength: 6 };
|
|
40
|
+
const RAMP: SegmentDef = { type: 'ramp', length: 20, lanes: [true, true, true], rampHeight: 4 };
|
|
41
|
+
const NARROW_L: SegmentDef = { type: 'narrow-left', length: 15, lanes: [false, true, true] };
|
|
42
|
+
const NARROW_R: SegmentDef = { type: 'narrow-right', length: 15, lanes: [true, true, false] };
|
|
43
|
+
const NARROW_C: SegmentDef = { type: 'narrow-center', length: 12, lanes: [false, true, false] };
|
|
44
|
+
|
|
45
|
+
function pickSegment(seed: number, index: number): SegmentDef {
|
|
46
|
+
const rng = mulberry32(seed ^ (index * 31337));
|
|
47
|
+
rng(); rng();
|
|
48
|
+
const roll = rng();
|
|
49
|
+
const difficulty = Math.min(index / 50, 1);
|
|
50
|
+
|
|
51
|
+
if (index < 3) return { ...STRAIGHT };
|
|
52
|
+
|
|
53
|
+
const straightChance = 0.70 - 0.50 * difficulty;
|
|
54
|
+
const gapChance = 0.15 + 0.10 * difficulty;
|
|
55
|
+
const rampChance = 0.10 + 0.10 * difficulty;
|
|
56
|
+
|
|
57
|
+
if (roll < straightChance) {
|
|
58
|
+
const len = 15 + Math.floor(rng() * 15);
|
|
59
|
+
return { ...STRAIGHT, length: len };
|
|
60
|
+
} else if (roll < straightChance + gapChance) {
|
|
61
|
+
const gapLen = 4 + Math.floor(difficulty * 4);
|
|
62
|
+
return { ...GAP, gapLength: gapLen };
|
|
63
|
+
} else if (roll < straightChance + gapChance + rampChance) {
|
|
64
|
+
return { ...RAMP };
|
|
65
|
+
} else {
|
|
66
|
+
const narrowRoll = rng();
|
|
67
|
+
if (narrowRoll < 0.33) return { ...NARROW_L };
|
|
68
|
+
if (narrowRoll < 0.66) return { ...NARROW_R };
|
|
69
|
+
return { ...NARROW_C };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createTrackGenerator(seed: number): TrackGeneratorState {
|
|
74
|
+
return { seed, nextZ: 0, nextIndex: 0, segments: [] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function updateTrack(
|
|
78
|
+
state: TrackGeneratorState,
|
|
79
|
+
playerZ: number,
|
|
80
|
+
lookAhead: number = 200,
|
|
81
|
+
bufferBehind: number = 50,
|
|
82
|
+
): void {
|
|
83
|
+
while (state.nextZ < playerZ + lookAhead) {
|
|
84
|
+
const def = pickSegment(state.seed, state.nextIndex);
|
|
85
|
+
state.segments.push({
|
|
86
|
+
def,
|
|
87
|
+
startZ: state.nextZ,
|
|
88
|
+
endZ: state.nextZ + def.length,
|
|
89
|
+
index: state.nextIndex,
|
|
90
|
+
});
|
|
91
|
+
state.nextZ += def.length;
|
|
92
|
+
state.nextIndex++;
|
|
93
|
+
}
|
|
94
|
+
state.segments = state.segments.filter(s => s.endZ > playerZ - bufferBehind);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Three.js Rendering
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
export interface TrackSegmentData {
|
|
102
|
+
type: string;
|
|
103
|
+
startZ: number;
|
|
104
|
+
endZ: number;
|
|
105
|
+
length: number;
|
|
106
|
+
lanes: [boolean, boolean, boolean];
|
|
107
|
+
gapLength?: number;
|
|
108
|
+
rampHeight?: number;
|
|
109
|
+
index: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const LANE_WIDTH = 2;
|
|
113
|
+
const TRACK_WIDTH = LANE_WIDTH * 3; // 6 units total
|
|
114
|
+
const TRACK_THICKNESS = 0.3;
|
|
115
|
+
|
|
116
|
+
// Materials (reused across segments)
|
|
117
|
+
const trackMaterial = new THREE.MeshStandardMaterial({ color: 0x334455, roughness: 0.8 });
|
|
118
|
+
const rampMaterial = new THREE.MeshStandardMaterial({ color: 0x446655, roughness: 0.7 });
|
|
119
|
+
const laneDividerMaterial = new THREE.MeshStandardMaterial({ color: 0x556677, roughness: 0.5 });
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build a Three.js group containing the track visual for a single segment.
|
|
123
|
+
*/
|
|
124
|
+
export function buildSegmentMesh(seg: TrackSegmentData): THREE.Group {
|
|
125
|
+
const group = new THREE.Group();
|
|
126
|
+
group.userData.segmentIndex = seg.index;
|
|
127
|
+
|
|
128
|
+
if (seg.type === 'gap') {
|
|
129
|
+
// Two platform halves with a gap in the middle
|
|
130
|
+
const halfLen = (seg.length - (seg.gapLength ?? 6)) / 2;
|
|
131
|
+
if (halfLen > 0) {
|
|
132
|
+
const geo1 = new THREE.BoxGeometry(TRACK_WIDTH, TRACK_THICKNESS, halfLen);
|
|
133
|
+
const mesh1 = new THREE.Mesh(geo1, trackMaterial);
|
|
134
|
+
mesh1.position.set(0, -TRACK_THICKNESS / 2, seg.startZ + halfLen / 2);
|
|
135
|
+
group.add(mesh1);
|
|
136
|
+
|
|
137
|
+
const geo2 = new THREE.BoxGeometry(TRACK_WIDTH, TRACK_THICKNESS, halfLen);
|
|
138
|
+
const mesh2 = new THREE.Mesh(geo2, trackMaterial);
|
|
139
|
+
mesh2.position.set(0, -TRACK_THICKNESS / 2, seg.endZ - halfLen / 2);
|
|
140
|
+
group.add(mesh2);
|
|
141
|
+
}
|
|
142
|
+
} else if (seg.type === 'ramp') {
|
|
143
|
+
// Base platform
|
|
144
|
+
const geo = new THREE.BoxGeometry(TRACK_WIDTH, TRACK_THICKNESS, seg.length);
|
|
145
|
+
const mesh = new THREE.Mesh(geo, rampMaterial);
|
|
146
|
+
mesh.position.set(0, -TRACK_THICKNESS / 2, (seg.startZ + seg.endZ) / 2);
|
|
147
|
+
group.add(mesh);
|
|
148
|
+
|
|
149
|
+
// Ramp peak indicator
|
|
150
|
+
if (seg.rampHeight) {
|
|
151
|
+
const peakGeo = new THREE.BoxGeometry(TRACK_WIDTH, seg.rampHeight, 2);
|
|
152
|
+
const peakMesh = new THREE.Mesh(peakGeo, rampMaterial);
|
|
153
|
+
peakMesh.position.set(0, seg.rampHeight / 2 - TRACK_THICKNESS, (seg.startZ + seg.endZ) / 2);
|
|
154
|
+
group.add(peakMesh);
|
|
155
|
+
}
|
|
156
|
+
} else if (seg.type.startsWith('narrow')) {
|
|
157
|
+
// Build only the active lanes
|
|
158
|
+
for (let i = 0; i < 3; i++) {
|
|
159
|
+
if (!seg.lanes[i]) continue;
|
|
160
|
+
const laneX = (i - 1) * LANE_WIDTH;
|
|
161
|
+
const geo = new THREE.BoxGeometry(LANE_WIDTH, TRACK_THICKNESS, seg.length);
|
|
162
|
+
const mesh = new THREE.Mesh(geo, trackMaterial);
|
|
163
|
+
mesh.position.set(laneX, -TRACK_THICKNESS / 2, (seg.startZ + seg.endZ) / 2);
|
|
164
|
+
group.add(mesh);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// Straight — full width platform
|
|
168
|
+
const geo = new THREE.BoxGeometry(TRACK_WIDTH, TRACK_THICKNESS, seg.length);
|
|
169
|
+
const mesh = new THREE.Mesh(geo, trackMaterial);
|
|
170
|
+
mesh.position.set(0, -TRACK_THICKNESS / 2, (seg.startZ + seg.endZ) / 2);
|
|
171
|
+
group.add(mesh);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Lane divider lines
|
|
175
|
+
for (let i = 0; i < 2; i++) {
|
|
176
|
+
const x = (i === 0 ? -1 : 1) * LANE_WIDTH / 2;
|
|
177
|
+
if (!seg.lanes[i] || !seg.lanes[i + 1]) continue;
|
|
178
|
+
if (seg.type === 'gap') continue;
|
|
179
|
+
const divGeo = new THREE.BoxGeometry(0.05, TRACK_THICKNESS + 0.01, seg.length);
|
|
180
|
+
const divMesh = new THREE.Mesh(divGeo, laneDividerMaterial);
|
|
181
|
+
divMesh.position.set(x, 0, (seg.startZ + seg.endZ) / 2);
|
|
182
|
+
group.add(divMesh);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return group;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Dispose a segment mesh group and its geometries.
|
|
190
|
+
*/
|
|
191
|
+
export function disposeSegmentMesh(group: THREE.Group): void {
|
|
192
|
+
group.traverse((child) => {
|
|
193
|
+
if (child instanceof THREE.Mesh) {
|
|
194
|
+
child.geometry.dispose();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Build the full runner scene group (lighting).
|
|
201
|
+
*/
|
|
202
|
+
export function buildRunnerScene(): THREE.Group {
|
|
203
|
+
const group = new THREE.Group();
|
|
204
|
+
|
|
205
|
+
const ambient = new THREE.AmbientLight(0xaabbcc, 0.6);
|
|
206
|
+
group.add(ambient);
|
|
207
|
+
|
|
208
|
+
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
209
|
+
dirLight.position.set(0, 20, -10);
|
|
210
|
+
group.add(dirLight);
|
|
211
|
+
|
|
212
|
+
return group;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function disposeRunnerScene(group: THREE.Group): void {
|
|
216
|
+
group.traverse((child) => {
|
|
217
|
+
if (child instanceof THREE.Mesh) {
|
|
218
|
+
child.geometry.dispose();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file renderer/shaders/SkyShader.ts
|
|
3
|
+
* Procedural gradient sky dome — replaces solid scene background color.
|
|
4
|
+
*
|
|
5
|
+
* A large inside-facing sphere with a GLSL shader that blends between
|
|
6
|
+
* a top color and a horizon color based on the normalized world-space Y.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as THREE from 'three';
|
|
10
|
+
|
|
11
|
+
const vertexShader = /* glsl */ `
|
|
12
|
+
varying vec3 vWorldPos;
|
|
13
|
+
void main() {
|
|
14
|
+
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
15
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const fragmentShader = /* glsl */ `
|
|
20
|
+
uniform vec3 u_topColor;
|
|
21
|
+
uniform vec3 u_horizonColor;
|
|
22
|
+
uniform float u_offset;
|
|
23
|
+
uniform float u_time;
|
|
24
|
+
uniform vec3 u_sunDirection;
|
|
25
|
+
uniform float u_sunEnabled;
|
|
26
|
+
uniform float u_cloudsEnabled;
|
|
27
|
+
varying vec3 vWorldPos;
|
|
28
|
+
|
|
29
|
+
// Simple hash-based noise for clouds
|
|
30
|
+
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
|
31
|
+
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
|
32
|
+
vec3 permute(vec3 x) { return mod289(((x * 34.0) + 1.0) * x); }
|
|
33
|
+
|
|
34
|
+
float snoise2D(vec2 v) {
|
|
35
|
+
const vec4 C = vec4(0.211324865405187, 0.366025403784439,
|
|
36
|
+
-0.577350269189626, 0.024390243902439);
|
|
37
|
+
vec2 i = floor(v + dot(v, C.yy));
|
|
38
|
+
vec2 x0 = v - i + dot(i, C.xx);
|
|
39
|
+
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
|
40
|
+
vec4 x12 = x0.xyxy + C.xxzz;
|
|
41
|
+
x12.xy -= i1;
|
|
42
|
+
i = mod289(i);
|
|
43
|
+
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
|
|
44
|
+
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
|
|
45
|
+
m = m * m; m = m * m;
|
|
46
|
+
vec3 x = 2.0 * fract(p * C.www) - 1.0;
|
|
47
|
+
vec3 h = abs(x) - 0.5;
|
|
48
|
+
vec3 ox = floor(x + 0.5);
|
|
49
|
+
vec3 a0 = x - ox;
|
|
50
|
+
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
|
|
51
|
+
vec3 g;
|
|
52
|
+
g.x = a0.x * x0.x + h.x * x0.y;
|
|
53
|
+
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
|
|
54
|
+
return 130.0 * dot(m, g);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
float fbm(vec2 p) {
|
|
58
|
+
float f = 0.0;
|
|
59
|
+
f += 0.5000 * snoise2D(p); p *= 2.02;
|
|
60
|
+
f += 0.2500 * snoise2D(p); p *= 2.03;
|
|
61
|
+
f += 0.1250 * snoise2D(p); p *= 2.01;
|
|
62
|
+
f += 0.0625 * snoise2D(p);
|
|
63
|
+
return f / 0.9375;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
void main() {
|
|
67
|
+
vec3 dir = normalize(vWorldPos);
|
|
68
|
+
float h = dir.y;
|
|
69
|
+
float t = clamp(h + u_offset, 0.0, 1.0);
|
|
70
|
+
vec3 skyColor = mix(u_horizonColor, u_topColor, t);
|
|
71
|
+
|
|
72
|
+
// Sun disc + glow
|
|
73
|
+
if (u_sunEnabled > 0.5) {
|
|
74
|
+
float sunDot = max(dot(dir, u_sunDirection), 0.0);
|
|
75
|
+
// Sharp disc
|
|
76
|
+
float sunDisc = smoothstep(0.9995, 0.9998, sunDot);
|
|
77
|
+
vec3 sunColor = vec3(1.0, 0.95, 0.8);
|
|
78
|
+
skyColor = mix(skyColor, sunColor, sunDisc);
|
|
79
|
+
// Soft glow
|
|
80
|
+
float sunGlow = pow(sunDot, 64.0) * 0.4;
|
|
81
|
+
skyColor += vec3(1.0, 0.8, 0.5) * sunGlow;
|
|
82
|
+
// Horizon scatter near sun
|
|
83
|
+
float horizonGlow = pow(sunDot, 8.0) * max(0.0, 1.0 - abs(h) * 4.0) * 0.3;
|
|
84
|
+
skyColor += vec3(1.0, 0.6, 0.3) * horizonGlow;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Procedural clouds
|
|
88
|
+
if (u_cloudsEnabled > 0.5 && h > 0.0) {
|
|
89
|
+
// Project onto a dome
|
|
90
|
+
vec2 cloudUV = dir.xz / (h + 0.1) * 0.3;
|
|
91
|
+
float drift = u_time * 0.01;
|
|
92
|
+
float n = fbm(cloudUV + vec2(drift, drift * 0.5));
|
|
93
|
+
// Shape clouds: threshold + soft edges
|
|
94
|
+
float cloud = smoothstep(0.1, 0.5, n);
|
|
95
|
+
// Fade at horizon to avoid hard cutoff
|
|
96
|
+
float horizonFade = smoothstep(0.0, 0.15, h);
|
|
97
|
+
cloud *= horizonFade * 0.7;
|
|
98
|
+
// Cloud color: bright white, slightly shaded
|
|
99
|
+
vec3 cloudColor = vec3(1.0, 1.0, 1.0);
|
|
100
|
+
if (u_sunEnabled > 0.5) {
|
|
101
|
+
float sunLight = max(dot(dir, u_sunDirection), 0.0);
|
|
102
|
+
cloudColor = mix(vec3(0.8, 0.85, 0.9), vec3(1.0, 1.0, 0.98), sunLight);
|
|
103
|
+
}
|
|
104
|
+
skyColor = mix(skyColor, cloudColor, cloud);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
gl_FragColor = vec4(skyColor, 1.0);
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
export interface SkyColors {
|
|
112
|
+
top: string;
|
|
113
|
+
horizon: string;
|
|
114
|
+
/** 0–1: how far above the equator the horizon line sits. Default 0.1 */
|
|
115
|
+
offset?: number;
|
|
116
|
+
/** Enable sun disc + glow. Default false. */
|
|
117
|
+
sun?: boolean;
|
|
118
|
+
/** Sun direction (normalized). Default: upper-right. */
|
|
119
|
+
sunDirection?: [number, number, number];
|
|
120
|
+
/** Enable procedural clouds. Default false. */
|
|
121
|
+
clouds?: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const SKY_PRESETS: Record<string, SkyColors> = {
|
|
125
|
+
overworld: { top: '#87ceeb', horizon: '#ddeeff', offset: 0.15, sun: true, clouds: true, sunDirection: [0.4, 0.7, 0.3] },
|
|
126
|
+
dungeon: { top: '#0a0010', horizon: '#050005', offset: 0.05 },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export class SkyMesh {
|
|
130
|
+
private mesh: THREE.Mesh;
|
|
131
|
+
private uniforms: {
|
|
132
|
+
u_topColor: { value: THREE.Color };
|
|
133
|
+
u_horizonColor: { value: THREE.Color };
|
|
134
|
+
u_offset: { value: number };
|
|
135
|
+
u_time: { value: number };
|
|
136
|
+
u_sunDirection: { value: THREE.Vector3 };
|
|
137
|
+
u_sunEnabled: { value: number };
|
|
138
|
+
u_cloudsEnabled: { value: number };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
constructor(scene: THREE.Scene) {
|
|
142
|
+
this.uniforms = {
|
|
143
|
+
u_topColor: { value: new THREE.Color('#87ceeb') },
|
|
144
|
+
u_horizonColor: { value: new THREE.Color('#ddeeff') },
|
|
145
|
+
u_offset: { value: 0.15 },
|
|
146
|
+
u_time: { value: 0 },
|
|
147
|
+
u_sunDirection: { value: new THREE.Vector3(0.4, 0.7, 0.3).normalize() },
|
|
148
|
+
u_sunEnabled: { value: 0 },
|
|
149
|
+
u_cloudsEnabled: { value: 0 },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const geo = new THREE.SphereGeometry(500, 32, 16);
|
|
153
|
+
const mat = new THREE.ShaderMaterial({
|
|
154
|
+
vertexShader,
|
|
155
|
+
fragmentShader,
|
|
156
|
+
uniforms: this.uniforms,
|
|
157
|
+
side: THREE.BackSide,
|
|
158
|
+
depthWrite: false,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
this.mesh = new THREE.Mesh(geo, mat);
|
|
162
|
+
scene.add(this.mesh);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Call each frame with elapsed time in seconds. */
|
|
166
|
+
tick(time: number): void {
|
|
167
|
+
this.uniforms.u_time.value = time;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
setSkyColors(colors: SkyColors): void {
|
|
171
|
+
this.uniforms.u_topColor.value.set(colors.top);
|
|
172
|
+
this.uniforms.u_horizonColor.value.set(colors.horizon);
|
|
173
|
+
this.uniforms.u_offset.value = colors.offset ?? 0.1;
|
|
174
|
+
this.uniforms.u_sunEnabled.value = colors.sun ? 1 : 0;
|
|
175
|
+
this.uniforms.u_cloudsEnabled.value = colors.clouds ? 1 : 0;
|
|
176
|
+
if (colors.sunDirection) {
|
|
177
|
+
this.uniforms.u_sunDirection.value.set(...colors.sunDirection).normalize();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setSkyForArea(areaId: string): void {
|
|
182
|
+
const preset = SKY_PRESETS[areaId] ?? SKY_PRESETS['dungeon'];
|
|
183
|
+
this.setSkyColors(preset);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
dispose(): void {
|
|
187
|
+
this.mesh.geometry.dispose();
|
|
188
|
+
(this.mesh.material as THREE.Material).dispose();
|
|
189
|
+
}
|
|
190
|
+
}
|