@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,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file renderer/dungeon.ts
|
|
3
|
+
* Dungeon mesh builder — converts a Dungeon data structure into Three.js geometry.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Build a THREE.Group of floor/wall/door meshes from a Dungeon object
|
|
7
|
+
* - Assign shader materials via ShaderManager
|
|
8
|
+
* - Dispose all geometry and materials when the dungeon is swapped
|
|
9
|
+
*
|
|
10
|
+
* What does NOT belong here:
|
|
11
|
+
* - Entity rendering (→ renderer/entities.ts)
|
|
12
|
+
* - Dungeon generation logic (→ packages/core)
|
|
13
|
+
* - Game loop / animation (→ game.ts)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as THREE from 'three';
|
|
17
|
+
import { CELL_VISUALS } from '../../../game-data/src/dungeon/cell-visuals.js';
|
|
18
|
+
import type { Dungeon, CellType } from '@loonylabs/gamedev-core';
|
|
19
|
+
import { ProceduralShaderMaterial, ShaderManager } from '@loonylabs/gamedev-client';
|
|
20
|
+
import { DUNGEON_SHADERS } from './shaders/shaderConfig.js';
|
|
21
|
+
|
|
22
|
+
function isWebGL2Available(): boolean {
|
|
23
|
+
try {
|
|
24
|
+
const canvas = document.createElement('canvas');
|
|
25
|
+
return !!canvas.getContext('webgl2');
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createMaterial(
|
|
32
|
+
type: CellType,
|
|
33
|
+
color: number,
|
|
34
|
+
useShaders: boolean,
|
|
35
|
+
shaderManager?: ShaderManager,
|
|
36
|
+
): THREE.Material {
|
|
37
|
+
const shaderConfig = DUNGEON_SHADERS[type];
|
|
38
|
+
if (useShaders && shaderConfig) {
|
|
39
|
+
const mat = new ProceduralShaderMaterial(shaderConfig, new THREE.Color(color), { glowColor: shaderConfig.glowColor });
|
|
40
|
+
shaderManager?.register(mat);
|
|
41
|
+
return mat;
|
|
42
|
+
}
|
|
43
|
+
return new THREE.MeshLambertMaterial({ color });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildDungeonMesh(dungeon: Dungeon, shaderManager?: ShaderManager): THREE.Group {
|
|
47
|
+
const useShaders = isWebGL2Available();
|
|
48
|
+
const group = new THREE.Group();
|
|
49
|
+
|
|
50
|
+
// Count cells per type across all rooms
|
|
51
|
+
const typeCounts: Partial<Record<CellType, number>> = {};
|
|
52
|
+
for (const room of dungeon.rooms) {
|
|
53
|
+
for (const row of room.grid.cells) {
|
|
54
|
+
for (const cell of row) {
|
|
55
|
+
const visual = CELL_VISUALS[cell.type];
|
|
56
|
+
if (visual) typeCounts[cell.type] = (typeCounts[cell.type] ?? 0) + 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Build InstancedMesh per type
|
|
62
|
+
const meshes: Partial<Record<CellType, THREE.InstancedMesh>> = {};
|
|
63
|
+
for (const [typeStr, count] of Object.entries(typeCounts)) {
|
|
64
|
+
const type = typeStr as CellType;
|
|
65
|
+
const visual = CELL_VISUALS[type];
|
|
66
|
+
if (!visual || !count) continue;
|
|
67
|
+
const geo = new THREE.BoxGeometry(1, visual.height, 1);
|
|
68
|
+
const mat = createMaterial(type, visual.color, useShaders, shaderManager);
|
|
69
|
+
meshes[type] = new THREE.InstancedMesh(geo, mat, count);
|
|
70
|
+
group.add(meshes[type]!);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const indices: Partial<Record<CellType, number>> = {};
|
|
74
|
+
const dummy = new THREE.Object3D();
|
|
75
|
+
|
|
76
|
+
for (const room of dungeon.rooms) {
|
|
77
|
+
const ox = room.worldOffset.x;
|
|
78
|
+
const oy = room.worldOffset.y;
|
|
79
|
+
for (const row of room.grid.cells) {
|
|
80
|
+
for (const cell of row) {
|
|
81
|
+
const visual = CELL_VISUALS[cell.type];
|
|
82
|
+
const mesh = meshes[cell.type];
|
|
83
|
+
if (!visual || !mesh) continue;
|
|
84
|
+
const idx = indices[cell.type] ?? 0;
|
|
85
|
+
// Floors at elevated positions; walls stay at ground level
|
|
86
|
+
const baseY = cell.type === 'floor' || cell.type === 'spawn' ? cell.height : 0;
|
|
87
|
+
dummy.position.set(ox + cell.x, baseY + visual.height / 2, oy + cell.y);
|
|
88
|
+
dummy.updateMatrix();
|
|
89
|
+
mesh.setMatrixAt(idx, dummy.matrix);
|
|
90
|
+
indices[cell.type] = idx + 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const mesh of Object.values(meshes)) {
|
|
96
|
+
if (mesh) mesh.instanceMatrix.needsUpdate = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Debug: plain-colored markers above elevated floor cells (shader-independent)
|
|
100
|
+
addHeightDebugMarkers(dungeon, group);
|
|
101
|
+
|
|
102
|
+
// Add side walls to fill vertical gaps between different-height floor cells
|
|
103
|
+
buildSideWalls(dungeon, group, useShaders, shaderManager);
|
|
104
|
+
// Add stair step meshes at height transitions
|
|
105
|
+
buildStairMeshes(dungeon, group);
|
|
106
|
+
|
|
107
|
+
return group;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const HEIGHT_MARKER_COLORS = [0xff8800, 0x00ccff, 0xff00ff, 0xffff00]; // orange, cyan, magenta, yellow
|
|
111
|
+
|
|
112
|
+
function addHeightDebugMarkers(dungeon: Dungeon, group: THREE.Group): void {
|
|
113
|
+
for (const room of dungeon.rooms) {
|
|
114
|
+
const ox = room.worldOffset.x;
|
|
115
|
+
const oz = room.worldOffset.y;
|
|
116
|
+
for (const row of room.grid.cells) {
|
|
117
|
+
for (const cell of row) {
|
|
118
|
+
if (cell.height === 0) continue;
|
|
119
|
+
const color = HEIGHT_MARKER_COLORS[(cell.height - 1) % HEIGHT_MARKER_COLORS.length];
|
|
120
|
+
const geo = new THREE.BoxGeometry(0.6, 0.3, 0.6);
|
|
121
|
+
const mat = new THREE.MeshBasicMaterial({ color });
|
|
122
|
+
const mesh = new THREE.Mesh(geo, mat);
|
|
123
|
+
mesh.position.set(ox + cell.x, cell.height + 0.25, oz + cell.y);
|
|
124
|
+
group.add(mesh);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const SIDE_WALL_COLOR = 0x666666;
|
|
131
|
+
const DIRS_4 = [
|
|
132
|
+
{ dx: 1, dz: 0 },
|
|
133
|
+
{ dx: -1, dz: 0 },
|
|
134
|
+
{ dx: 0, dz: 1 },
|
|
135
|
+
{ dx: 0, dz: -1 },
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
function buildSideWalls(
|
|
139
|
+
dungeon: Dungeon,
|
|
140
|
+
group: THREE.Group,
|
|
141
|
+
useShaders: boolean,
|
|
142
|
+
shaderManager?: ShaderManager,
|
|
143
|
+
): void {
|
|
144
|
+
const mat = useShaders
|
|
145
|
+
? (() => { const wallCfg = DUNGEON_SHADERS['wall']; const m = new ProceduralShaderMaterial(wallCfg, new THREE.Color(SIDE_WALL_COLOR), { glowColor: wallCfg.glowColor }); shaderManager?.register(m); return m; })()
|
|
146
|
+
: new THREE.MeshLambertMaterial({ color: SIDE_WALL_COLOR });
|
|
147
|
+
|
|
148
|
+
// Collect all elevated floor cells and check each cardinal direction
|
|
149
|
+
for (const room of dungeon.rooms) {
|
|
150
|
+
const ox = room.worldOffset.x;
|
|
151
|
+
const oz = room.worldOffset.y;
|
|
152
|
+
for (const row of room.grid.cells) {
|
|
153
|
+
for (const cell of row) {
|
|
154
|
+
if (cell.type !== 'floor' && cell.type !== 'spawn') continue;
|
|
155
|
+
if (cell.height === 0) continue;
|
|
156
|
+
|
|
157
|
+
for (const { dx, dz } of DIRS_4) {
|
|
158
|
+
// Find neighbor cell height (search all rooms)
|
|
159
|
+
const nx = cell.x + dx;
|
|
160
|
+
const nz = cell.y + dz;
|
|
161
|
+
let neighborHeight = 0;
|
|
162
|
+
|
|
163
|
+
// Check within same room
|
|
164
|
+
const nCell = room.grid.cells[nz]?.[nx];
|
|
165
|
+
if (nCell) {
|
|
166
|
+
if (nCell.type === 'wall' || nCell.type === 'empty' || nCell.type === 'door') continue; // wall handles its own gap
|
|
167
|
+
neighborHeight = nCell.height;
|
|
168
|
+
}
|
|
169
|
+
// else: out of room bounds → treat as ground level
|
|
170
|
+
|
|
171
|
+
const gapHeight = cell.height - neighborHeight;
|
|
172
|
+
if (gapHeight <= 0) continue;
|
|
173
|
+
|
|
174
|
+
// Build a side wall panel filling the vertical gap
|
|
175
|
+
const panelH = gapHeight;
|
|
176
|
+
const panelW = 1.0;
|
|
177
|
+
const panelD = 0.12;
|
|
178
|
+
|
|
179
|
+
const geo = new THREE.BoxGeometry(
|
|
180
|
+
dx !== 0 ? panelD : panelW,
|
|
181
|
+
panelH,
|
|
182
|
+
dz !== 0 ? panelD : panelW,
|
|
183
|
+
);
|
|
184
|
+
const mesh = new THREE.Mesh(geo, mat);
|
|
185
|
+
mesh.position.set(
|
|
186
|
+
ox + cell.x + dx * 0.5,
|
|
187
|
+
neighborHeight + panelH / 2,
|
|
188
|
+
oz + cell.y + dz * 0.5,
|
|
189
|
+
);
|
|
190
|
+
group.add(mesh);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildStairMeshes(dungeon: Dungeon, group: THREE.Group): void {
|
|
198
|
+
const mat = new THREE.MeshLambertMaterial({ color: 0xff00aa }); // hot pink for debug visibility
|
|
199
|
+
|
|
200
|
+
for (const room of dungeon.rooms) {
|
|
201
|
+
const ox = room.worldOffset.x;
|
|
202
|
+
const oz = room.worldOffset.y;
|
|
203
|
+
for (const row of room.grid.cells) {
|
|
204
|
+
for (const cell of row) {
|
|
205
|
+
if (cell.type !== 'floor' && cell.type !== 'spawn') continue;
|
|
206
|
+
|
|
207
|
+
for (const { dx, dz } of DIRS_4) {
|
|
208
|
+
const nx = cell.x + dx;
|
|
209
|
+
const nz = cell.y + dz;
|
|
210
|
+
const nCell = room.grid.cells[nz]?.[nx];
|
|
211
|
+
if (!nCell) continue;
|
|
212
|
+
if (nCell.type !== 'floor' && nCell.type !== 'spawn') continue;
|
|
213
|
+
|
|
214
|
+
const hDiff = nCell.height - cell.height;
|
|
215
|
+
if (hDiff !== 1) continue; // only render step from lower → higher
|
|
216
|
+
|
|
217
|
+
// Place a small step box at the boundary between the two cells
|
|
218
|
+
const stepGeo = new THREE.BoxGeometry(
|
|
219
|
+
dx !== 0 ? 0.5 : 1.0,
|
|
220
|
+
0.15,
|
|
221
|
+
dz !== 0 ? 0.5 : 1.0,
|
|
222
|
+
);
|
|
223
|
+
const stepMesh = new THREE.Mesh(stepGeo, mat);
|
|
224
|
+
stepMesh.position.set(
|
|
225
|
+
ox + cell.x + dx * 0.4,
|
|
226
|
+
cell.height + 0.075,
|
|
227
|
+
oz + cell.y + dz * 0.4,
|
|
228
|
+
);
|
|
229
|
+
group.add(stepMesh);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export { buildStairMeshes };
|
|
237
|
+
|
|
238
|
+
export function disposeMesh(group: THREE.Group, shaderManager?: ShaderManager): void {
|
|
239
|
+
group.traverse(obj => {
|
|
240
|
+
if (obj instanceof THREE.Mesh) {
|
|
241
|
+
obj.geometry.dispose();
|
|
242
|
+
const disposeMat = (m: THREE.Material) => {
|
|
243
|
+
if (m instanceof ProceduralShaderMaterial) shaderManager?.unregister(m);
|
|
244
|
+
m.dispose();
|
|
245
|
+
};
|
|
246
|
+
if (Array.isArray(obj.material)) obj.material.forEach(disposeMat);
|
|
247
|
+
else disposeMat(obj.material);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dungeon portal mesh — visible archway with glowing light.
|
|
3
|
+
* Zero-asset: Two pillars + top bar + point light.
|
|
4
|
+
*/
|
|
5
|
+
import * as THREE from 'three';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a dungeon portal at the given world position.
|
|
9
|
+
* @param x World X coordinate (center of portal)
|
|
10
|
+
* @param z World Z coordinate (center of portal)
|
|
11
|
+
* @param getTerrainHeight Function to get terrain height at (x, z)
|
|
12
|
+
*/
|
|
13
|
+
export function createDungeonPortal(
|
|
14
|
+
x: number,
|
|
15
|
+
z: number,
|
|
16
|
+
getTerrainHeight: (x: number, z: number) => number,
|
|
17
|
+
): THREE.Group {
|
|
18
|
+
const group = new THREE.Group();
|
|
19
|
+
const groundY = getTerrainHeight(x, z) ?? 28;
|
|
20
|
+
|
|
21
|
+
const pillarMat = new THREE.MeshStandardMaterial({ color: 0x3a2050, roughness: 0.7 });
|
|
22
|
+
const pillarGeo = new THREE.BoxGeometry(0.6, 5, 0.6);
|
|
23
|
+
|
|
24
|
+
// Left pillar
|
|
25
|
+
const leftPillar = new THREE.Mesh(pillarGeo, pillarMat);
|
|
26
|
+
leftPillar.position.set(-2, groundY + 2.5, 0);
|
|
27
|
+
group.add(leftPillar);
|
|
28
|
+
|
|
29
|
+
// Right pillar
|
|
30
|
+
const rightPillar = new THREE.Mesh(pillarGeo, pillarMat);
|
|
31
|
+
rightPillar.position.set(2, groundY + 2.5, 0);
|
|
32
|
+
group.add(rightPillar);
|
|
33
|
+
|
|
34
|
+
// Top bar (lintel)
|
|
35
|
+
const lintelGeo = new THREE.BoxGeometry(5, 0.6, 0.6);
|
|
36
|
+
const lintel = new THREE.Mesh(lintelGeo, pillarMat);
|
|
37
|
+
lintel.position.set(0, groundY + 5.2, 0);
|
|
38
|
+
group.add(lintel);
|
|
39
|
+
|
|
40
|
+
// Rune stones at base
|
|
41
|
+
const runeMat = new THREE.MeshStandardMaterial({ color: 0x6040a0, emissive: 0x301860, roughness: 0.6 });
|
|
42
|
+
const runeGeo = new THREE.BoxGeometry(0.3, 0.8, 0.3);
|
|
43
|
+
for (let i = -1; i <= 1; i++) {
|
|
44
|
+
const rune = new THREE.Mesh(runeGeo, runeMat);
|
|
45
|
+
rune.position.set(i * 1.2, groundY + 0.4, 0.5);
|
|
46
|
+
group.add(rune);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Portal glow (purple/blue)
|
|
50
|
+
const portalLight = new THREE.PointLight(0x8040ff, 4, 25);
|
|
51
|
+
portalLight.position.set(0, groundY + 3, 0);
|
|
52
|
+
group.add(portalLight);
|
|
53
|
+
|
|
54
|
+
// Secondary warm light at base
|
|
55
|
+
const baseLight = new THREE.PointLight(0x6030c0, 1.5, 10);
|
|
56
|
+
baseLight.position.set(0, groundY + 0.5, 0);
|
|
57
|
+
group.add(baseLight);
|
|
58
|
+
|
|
59
|
+
// Glowing center plane (fake portal surface)
|
|
60
|
+
const portalGeo = new THREE.PlaneGeometry(3, 4);
|
|
61
|
+
const portalMat = new THREE.MeshBasicMaterial({
|
|
62
|
+
color: 0x6030c0,
|
|
63
|
+
transparent: true,
|
|
64
|
+
opacity: 0.25,
|
|
65
|
+
side: THREE.DoubleSide,
|
|
66
|
+
});
|
|
67
|
+
const portalPlane = new THREE.Mesh(portalGeo, portalMat);
|
|
68
|
+
portalPlane.position.set(0, groundY + 2.8, 0);
|
|
69
|
+
group.add(portalPlane);
|
|
70
|
+
|
|
71
|
+
group.position.set(x, 0, z);
|
|
72
|
+
return group;
|
|
73
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file renderer/dungeonZone.ts
|
|
3
|
+
* Builds a Three.js Group from a raw GridData (zone terrain).
|
|
4
|
+
*
|
|
5
|
+
* Unlike buildDungeonMesh() which handles a full Dungeon with multiple rooms,
|
|
6
|
+
* this builder takes a single GridData and an (offsetX, offsetZ) world origin.
|
|
7
|
+
* Used by ChunkStreamer to render open-world terrain zones.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as THREE from 'three';
|
|
11
|
+
import type { GridData } from '@loonylabs/gamedev-core';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_FLOOR_COLOR = new THREE.Color(0x3a5a1a);
|
|
14
|
+
|
|
15
|
+
function isFloorLike(cellType: string): boolean {
|
|
16
|
+
return (
|
|
17
|
+
cellType === 'floor' ||
|
|
18
|
+
cellType === 'spawn' ||
|
|
19
|
+
cellType === 'transition' ||
|
|
20
|
+
cellType === 'npc' ||
|
|
21
|
+
cellType === 'fire'
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds a THREE.Group containing InstancedMeshes for floors and obstacles.
|
|
27
|
+
* @param groundColor - Optional CSS hex color string (e.g. '#3a5a1a') for the floor tiles.
|
|
28
|
+
*/
|
|
29
|
+
export function buildDungeonMeshFromGrid(
|
|
30
|
+
grid: GridData,
|
|
31
|
+
offsetX: number,
|
|
32
|
+
offsetZ: number,
|
|
33
|
+
groundColor?: string,
|
|
34
|
+
): THREE.Group {
|
|
35
|
+
const group = new THREE.Group();
|
|
36
|
+
group.position.set(offsetX, 0, offsetZ);
|
|
37
|
+
|
|
38
|
+
const baseColor = groundColor ? new THREE.Color(groundColor) : DEFAULT_FLOOR_COLOR;
|
|
39
|
+
|
|
40
|
+
// 1. Count instances for pre-allocation
|
|
41
|
+
let floorCount = 0;
|
|
42
|
+
let solidWallCount = 0;
|
|
43
|
+
const obstacleCounts: Record<string, number> = { tree: 0, rock: 0, bush: 0, dead_tree: 0, bog_stone: 0 };
|
|
44
|
+
let fireCount = 0;
|
|
45
|
+
|
|
46
|
+
for (const row of grid.cells) {
|
|
47
|
+
for (const cell of row) {
|
|
48
|
+
if (isFloorLike(cell.type)) {
|
|
49
|
+
floorCount++;
|
|
50
|
+
if (cell.type === 'fire') fireCount++;
|
|
51
|
+
} else if (cell.type === 'wall' && cell.obstacleType) {
|
|
52
|
+
floorCount++;
|
|
53
|
+
if (obstacleCounts[cell.obstacleType] !== undefined) {
|
|
54
|
+
obstacleCounts[cell.obstacleType]++;
|
|
55
|
+
}
|
|
56
|
+
} else if (cell.type === 'wall' && !cell.obstacleType) {
|
|
57
|
+
solidWallCount++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2. Render Floors with height-based color tinting
|
|
63
|
+
if (floorCount > 0) {
|
|
64
|
+
const geo = new THREE.BoxGeometry(1, 0.25, 1);
|
|
65
|
+
const mat = new THREE.MeshLambertMaterial({ color: baseColor, vertexColors: false });
|
|
66
|
+
const mesh = new THREE.InstancedMesh(geo, mat, floorCount);
|
|
67
|
+
mesh.receiveShadow = true;
|
|
68
|
+
|
|
69
|
+
const dummy = new THREE.Object3D();
|
|
70
|
+
const tintColor = new THREE.Color();
|
|
71
|
+
let idx = 0;
|
|
72
|
+
for (const row of grid.cells) {
|
|
73
|
+
for (const cell of row) {
|
|
74
|
+
const isObstacleFloor = cell.type === 'wall' && cell.obstacleType;
|
|
75
|
+
if (!isFloorLike(cell.type) && !isObstacleFloor) continue;
|
|
76
|
+
dummy.position.set(cell.x, cell.height + 0.125, cell.y);
|
|
77
|
+
dummy.updateMatrix();
|
|
78
|
+
mesh.setMatrixAt(idx, dummy.matrix);
|
|
79
|
+
|
|
80
|
+
// Height tinting: slight lightening at height 3+
|
|
81
|
+
const t = Math.min(cell.height / 4, 1);
|
|
82
|
+
tintColor.copy(baseColor).lerp(new THREE.Color(0xffffff), t * 0.15);
|
|
83
|
+
mesh.setColorAt(idx, tintColor);
|
|
84
|
+
|
|
85
|
+
idx++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
89
|
+
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
|
|
90
|
+
group.add(mesh);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 3. Render solid walls (e.g. basecamp room walls — W cells without obstacleType)
|
|
94
|
+
if (solidWallCount > 0) {
|
|
95
|
+
const wallGeo = new THREE.BoxGeometry(1, 1.5, 1);
|
|
96
|
+
const wallMat = new THREE.MeshLambertMaterial({ color: 0x8b7355 });
|
|
97
|
+
const wallMesh = new THREE.InstancedMesh(wallGeo, wallMat, solidWallCount);
|
|
98
|
+
wallMesh.castShadow = true;
|
|
99
|
+
wallMesh.receiveShadow = true;
|
|
100
|
+
const wallDummy = new THREE.Object3D();
|
|
101
|
+
let widx = 0;
|
|
102
|
+
for (const row of grid.cells) {
|
|
103
|
+
for (const cell of row) {
|
|
104
|
+
if (cell.type !== 'wall' || cell.obstacleType) continue;
|
|
105
|
+
wallDummy.position.set(cell.x, cell.height + 0.75, cell.y);
|
|
106
|
+
wallDummy.updateMatrix();
|
|
107
|
+
wallMesh.setMatrixAt(widx, wallDummy.matrix);
|
|
108
|
+
widx++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
wallMesh.instanceMatrix.needsUpdate = true;
|
|
112
|
+
group.add(wallMesh);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 4. Render Obstacles
|
|
116
|
+
const dummy = new THREE.Object3D();
|
|
117
|
+
|
|
118
|
+
if (obstacleCounts.tree > 0) {
|
|
119
|
+
const geo = new THREE.CylinderGeometry(0.4, 0.4, 2, 8);
|
|
120
|
+
const mat = new THREE.MeshLambertMaterial({ color: 0x228b22 });
|
|
121
|
+
const mesh = new THREE.InstancedMesh(geo, mat, obstacleCounts.tree);
|
|
122
|
+
mesh.castShadow = true;
|
|
123
|
+
mesh.receiveShadow = true;
|
|
124
|
+
let idx = 0;
|
|
125
|
+
for (const row of grid.cells) {
|
|
126
|
+
for (const cell of row) {
|
|
127
|
+
if (cell.type === 'wall' && cell.obstacleType === 'tree') {
|
|
128
|
+
dummy.position.set(cell.x, cell.height + 1, cell.y);
|
|
129
|
+
dummy.updateMatrix();
|
|
130
|
+
mesh.setMatrixAt(idx, dummy.matrix);
|
|
131
|
+
idx++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
136
|
+
group.add(mesh);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (obstacleCounts.rock > 0) {
|
|
140
|
+
const geo = new THREE.BoxGeometry(0.8, 0.8, 0.8);
|
|
141
|
+
const mat = new THREE.MeshLambertMaterial({ color: 0x808080 });
|
|
142
|
+
const mesh = new THREE.InstancedMesh(geo, mat, obstacleCounts.rock);
|
|
143
|
+
mesh.castShadow = true;
|
|
144
|
+
mesh.receiveShadow = true;
|
|
145
|
+
let idx = 0;
|
|
146
|
+
for (const row of grid.cells) {
|
|
147
|
+
for (const cell of row) {
|
|
148
|
+
if (cell.type === 'wall' && cell.obstacleType === 'rock') {
|
|
149
|
+
dummy.position.set(cell.x, cell.height + 0.4, cell.y);
|
|
150
|
+
dummy.rotation.set(0, 0, 0);
|
|
151
|
+
dummy.updateMatrix();
|
|
152
|
+
mesh.setMatrixAt(idx, dummy.matrix);
|
|
153
|
+
idx++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
158
|
+
group.add(mesh);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (obstacleCounts.bush > 0) {
|
|
162
|
+
const geo = new THREE.SphereGeometry(0.5, 8, 8);
|
|
163
|
+
const mat = new THREE.MeshLambertMaterial({ color: 0x006400 });
|
|
164
|
+
const mesh = new THREE.InstancedMesh(geo, mat, obstacleCounts.bush);
|
|
165
|
+
mesh.castShadow = true;
|
|
166
|
+
mesh.receiveShadow = true;
|
|
167
|
+
let idx = 0;
|
|
168
|
+
for (const row of grid.cells) {
|
|
169
|
+
for (const cell of row) {
|
|
170
|
+
if (cell.type === 'wall' && cell.obstacleType === 'bush') {
|
|
171
|
+
dummy.position.set(cell.x, cell.height + 0.5, cell.y);
|
|
172
|
+
dummy.updateMatrix();
|
|
173
|
+
mesh.setMatrixAt(idx, dummy.matrix);
|
|
174
|
+
idx++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
179
|
+
group.add(mesh);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Dead trees: bare grey cylinder trunk, no crown
|
|
183
|
+
if (obstacleCounts.dead_tree > 0) {
|
|
184
|
+
const geo = new THREE.CylinderGeometry(0.15, 0.2, 2.5, 6);
|
|
185
|
+
const mat = new THREE.MeshLambertMaterial({ color: 0x555555 });
|
|
186
|
+
const mesh = new THREE.InstancedMesh(geo, mat, obstacleCounts.dead_tree);
|
|
187
|
+
mesh.castShadow = true;
|
|
188
|
+
let idx = 0;
|
|
189
|
+
for (const row of grid.cells) {
|
|
190
|
+
for (const cell of row) {
|
|
191
|
+
if (cell.type === 'wall' && cell.obstacleType === 'dead_tree') {
|
|
192
|
+
dummy.position.set(cell.x, cell.height + 1.25, cell.y);
|
|
193
|
+
dummy.rotation.set(0, 0, 0);
|
|
194
|
+
dummy.updateMatrix();
|
|
195
|
+
mesh.setMatrixAt(idx, dummy.matrix);
|
|
196
|
+
idx++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
201
|
+
group.add(mesh);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Bog stones: flat wide box, dark grey, partially sunken
|
|
205
|
+
if (obstacleCounts.bog_stone > 0) {
|
|
206
|
+
const geo = new THREE.BoxGeometry(1.2, 0.4, 1.0);
|
|
207
|
+
const mat = new THREE.MeshLambertMaterial({ color: 0x3a3a3a });
|
|
208
|
+
const mesh = new THREE.InstancedMesh(geo, mat, obstacleCounts.bog_stone);
|
|
209
|
+
mesh.castShadow = true;
|
|
210
|
+
mesh.receiveShadow = true;
|
|
211
|
+
let idx = 0;
|
|
212
|
+
for (const row of grid.cells) {
|
|
213
|
+
for (const cell of row) {
|
|
214
|
+
if (cell.type === 'wall' && cell.obstacleType === 'bog_stone') {
|
|
215
|
+
dummy.position.set(cell.x, cell.height + 0.18, cell.y);
|
|
216
|
+
dummy.rotation.set(0, 0, 0);
|
|
217
|
+
dummy.updateMatrix();
|
|
218
|
+
mesh.setMatrixAt(idx, dummy.matrix);
|
|
219
|
+
idx++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
224
|
+
group.add(mesh);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 5. Campfire cells ('fire'): small cone flame + orange point light
|
|
228
|
+
for (const row of grid.cells) {
|
|
229
|
+
for (const cell of row) {
|
|
230
|
+
if (cell.type !== 'fire') continue;
|
|
231
|
+
const campfire = buildCampfireMesh();
|
|
232
|
+
campfire.position.set(cell.x, cell.height + 0.25, cell.y);
|
|
233
|
+
group.add(campfire);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 6. Cave entrance arches: placed at clusters of transition cells
|
|
238
|
+
const transitionSet = new Set<string>();
|
|
239
|
+
for (const row of grid.cells) {
|
|
240
|
+
for (const cell of row) {
|
|
241
|
+
if (cell.type === 'transition') transitionSet.add(`${cell.x},${cell.y}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const row of grid.cells) {
|
|
245
|
+
for (const cell of row) {
|
|
246
|
+
if (cell.type !== 'transition') continue;
|
|
247
|
+
// Check if this is the top-left of a 3×2 block
|
|
248
|
+
const is3x2TopLeft =
|
|
249
|
+
transitionSet.has(`${cell.x + 1},${cell.y}`) &&
|
|
250
|
+
transitionSet.has(`${cell.x + 2},${cell.y}`) &&
|
|
251
|
+
transitionSet.has(`${cell.x},${cell.y + 1}`) &&
|
|
252
|
+
transitionSet.has(`${cell.x + 1},${cell.y + 1}`) &&
|
|
253
|
+
transitionSet.has(`${cell.x + 2},${cell.y + 1}`);
|
|
254
|
+
if (is3x2TopLeft) {
|
|
255
|
+
const arch = buildCaveEntranceArch();
|
|
256
|
+
arch.position.set(cell.x + 1, cell.height, cell.y);
|
|
257
|
+
group.add(arch);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return group;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Builds a campfire mesh: small orange cone + point light for warm glow.
|
|
267
|
+
*/
|
|
268
|
+
function buildCampfireMesh(): THREE.Group {
|
|
269
|
+
const group = new THREE.Group();
|
|
270
|
+
|
|
271
|
+
// Flame cone
|
|
272
|
+
const flameMat = new THREE.MeshBasicMaterial({ color: 0xff6600 });
|
|
273
|
+
const flame = new THREE.Mesh(new THREE.ConeGeometry(0.2, 0.6, 6), flameMat);
|
|
274
|
+
flame.position.y = 0.3;
|
|
275
|
+
group.add(flame);
|
|
276
|
+
|
|
277
|
+
// Warm point light
|
|
278
|
+
const light = new THREE.PointLight(0xff8833, 2, 8);
|
|
279
|
+
light.position.y = 0.5;
|
|
280
|
+
group.add(light);
|
|
281
|
+
|
|
282
|
+
return group;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Builds a dark stone arch mesh for the cave entrance.
|
|
287
|
+
* Two pillars + a lintel.
|
|
288
|
+
*/
|
|
289
|
+
function buildCaveEntranceArch(): THREE.Group {
|
|
290
|
+
const arch = new THREE.Group();
|
|
291
|
+
const mat = new THREE.MeshLambertMaterial({ color: 0x222222 });
|
|
292
|
+
const pillarGeo = new THREE.BoxGeometry(0.5, 3, 0.5);
|
|
293
|
+
const left = new THREE.Mesh(pillarGeo, mat);
|
|
294
|
+
const right = new THREE.Mesh(pillarGeo, mat);
|
|
295
|
+
const lintel = new THREE.Mesh(new THREE.BoxGeometry(3.5, 0.5, 0.5), mat);
|
|
296
|
+
left.position.set(-1.5, 1.5, 0);
|
|
297
|
+
right.position.set(1.5, 1.5, 0);
|
|
298
|
+
lintel.position.set(0, 3.25, 0);
|
|
299
|
+
arch.add(left, right, lintel);
|
|
300
|
+
return arch;
|
|
301
|
+
}
|