@@ -19,6 +19,7 @@ export default class DungeonCrawler3DScene extends Scene {
1919 super ( ) ;
2020 this . world = new World ( ) ;
2121 this . moveSpeed = 6.2 ;
22+ this . playerSpawn = { x : - 8.2 , y : 0 , z : 4.2 } ;
2223 this . viewport = {
2324 x : 40 ,
2425 y : 170 ,
@@ -52,16 +53,23 @@ export default class DungeonCrawler3DScene extends Scene {
5253 } ;
5354 this . relicCollected = false ;
5455 this . escaped = false ;
56+ this . runState = 'seek-relic' ;
57+ this . runElapsedSeconds = 0 ;
58+ this . lastCompletionSeconds = 0 ;
59+ this . runCount = 1 ;
60+ this . lastResetReason = 'spawn' ;
61+ this . interactionFlashSeconds = 0 ;
62+ this . resetLatch = false ;
5563 this . lastPhysicsSummary = { movedEntities : 0 , collisionCount : 0 } ;
5664
5765 this . playerId = this . world . createEntity ( ) ;
5866 this . world . addComponent ( this . playerId , 'transform3D' , {
59- x : - 8.2 ,
60- y : 0 ,
61- z : 4.2 ,
62- previousX : - 8.2 ,
63- previousY : 0 ,
64- previousZ : 4.2 ,
67+ x : this . playerSpawn . x ,
68+ y : this . playerSpawn . y ,
69+ z : this . playerSpawn . z ,
70+ previousX : this . playerSpawn . x ,
71+ previousY : this . playerSpawn . y ,
72+ previousZ : this . playerSpawn . z ,
6573 } ) ;
6674 this . world . addComponent ( this . playerId , 'size3D' , { width : 1.0 , height : 1.4 , depth : 1.0 } ) ;
6775 this . world . addComponent ( this . playerId , 'velocity3D' , { x : 0 , y : 0 , z : 0 } ) ;
@@ -133,20 +141,63 @@ export default class DungeonCrawler3DScene extends Scene {
133141 } ) ;
134142 }
135143
144+ resetRun ( reason = 'manual-reset' ) {
145+ const player = this . world . requireComponent ( this . playerId , 'transform3D' ) ;
146+ const velocity = this . world . requireComponent ( this . playerId , 'velocity3D' ) ;
147+ player . x = this . playerSpawn . x ;
148+ player . y = this . playerSpawn . y ;
149+ player . z = this . playerSpawn . z ;
150+ player . previousX = this . playerSpawn . x ;
151+ player . previousY = this . playerSpawn . y ;
152+ player . previousZ = this . playerSpawn . z ;
153+ velocity . x = 0 ;
154+ velocity . y = 0 ;
155+ velocity . z = 0 ;
156+
157+ this . relicCollected = false ;
158+ this . escaped = false ;
159+ this . runState = 'seek-relic' ;
160+ this . runElapsedSeconds = 0 ;
161+ this . interactionFlashSeconds = 0 ;
162+ this . lastResetReason = reason ;
163+ this . runCount += 1 ;
164+
165+ const gateSolid = this . world . requireComponent ( this . gateId , 'solid3D' ) ;
166+ gateSolid . enabled = true ;
167+ const gateRenderable = this . world . requireComponent ( this . gateId , 'renderable3D' ) ;
168+ gateRenderable . color = '#f87171' ;
169+ }
170+
136171 step3DPhysics ( dt , engine ) {
137172 const input = engine . input ;
173+ const resetPressed = input ?. isDown ( 'KeyR' ) === true ;
174+ if ( resetPressed && ! this . resetLatch ) {
175+ this . resetRun ( 'manual-reset' ) ;
176+ }
177+ this . resetLatch = resetPressed ;
178+
179+ this . interactionFlashSeconds = Math . max ( 0 , this . interactionFlashSeconds - dt ) ;
180+
138181 const velocity = this . world . requireComponent ( this . playerId , 'velocity3D' ) ;
139- const axisX = ( input ?. isDown ( 'KeyD' ) ? 1 : 0 ) - ( input ?. isDown ( 'KeyA' ) ? 1 : 0 ) ;
140- const axisZ = ( input ?. isDown ( 'KeyW' ) ? 1 : 0 ) - ( input ?. isDown ( 'KeyS' ) ? 1 : 0 ) ;
141- const length = Math . hypot ( axisX , axisZ ) || 1 ;
182+ if ( ! this . escaped ) {
183+ const axisX = ( input ?. isDown ( 'KeyD' ) ? 1 : 0 ) - ( input ?. isDown ( 'KeyA' ) ? 1 : 0 ) ;
184+ const axisZ = ( input ?. isDown ( 'KeyW' ) ? 1 : 0 ) - ( input ?. isDown ( 'KeyS' ) ? 1 : 0 ) ;
185+ const length = Math . hypot ( axisX , axisZ ) || 1 ;
142186
143- velocity . x = ( axisX / length ) * this . moveSpeed ;
144- velocity . z = ( axisZ / length ) * this . moveSpeed ;
145- velocity . y = 0 ;
187+ velocity . x = ( axisX / length ) * this . moveSpeed ;
188+ velocity . z = ( axisZ / length ) * this . moveSpeed ;
189+ velocity . y = 0 ;
146190
147- this . lastPhysicsSummary = stepWorldPhysics3D ( this . world , dt , {
148- worldBounds : this . worldBounds ,
149- } ) ;
191+ this . lastPhysicsSummary = stepWorldPhysics3D ( this . world , dt , {
192+ worldBounds : this . worldBounds ,
193+ } ) ;
194+ this . runElapsedSeconds += dt ;
195+ } else {
196+ velocity . x = 0 ;
197+ velocity . y = 0 ;
198+ velocity . z = 0 ;
199+ this . lastPhysicsSummary = { movedEntities : 0 , collisionCount : 0 } ;
200+ }
150201
151202 const player = this . world . requireComponent ( this . playerId , 'transform3D' ) ;
152203 const playerSize = this . world . requireComponent ( this . playerId , 'size3D' ) ;
@@ -161,6 +212,8 @@ export default class DungeonCrawler3DScene extends Scene {
161212
162213 if ( ! this . relicCollected && isAabbColliding3D ( playerAabb , this . relic ) ) {
163214 this . relicCollected = true ;
215+ this . runState = 'escape' ;
216+ this . interactionFlashSeconds = 1.1 ;
164217 const gateSolid = this . world . requireComponent ( this . gateId , 'solid3D' ) ;
165218 gateSolid . enabled = false ;
166219 const gateRenderable = this . world . requireComponent ( this . gateId , 'renderable3D' ) ;
@@ -169,17 +222,27 @@ export default class DungeonCrawler3DScene extends Scene {
169222
170223 if ( this . relicCollected && isAabbColliding3D ( playerAabb , this . exitGoal ) ) {
171224 this . escaped = true ;
225+ this . runState = 'complete' ;
226+ this . lastCompletionSeconds = this . runElapsedSeconds ;
227+ this . interactionFlashSeconds = 1.6 ;
172228 }
173229
174230 this . syncCamera ( ) ;
175231 }
176232
177233 render ( renderer ) {
234+ const objectiveLine =
235+ this . runState === 'seek-relic'
236+ ? 'Objective: collect the relic to unlock the gate.'
237+ : this . runState === 'escape'
238+ ? 'Objective: pass the unlocked gate and reach the exit.'
239+ : `Run complete in ${ this . lastCompletionSeconds . toFixed ( 1 ) } s. Press R to restart.` ;
240+
178241 drawFrame ( renderer , theme , [
179242 'Sample 1608 - 3D Dungeon Crawler' ,
180- 'Explore rooms, collect the relic, and unlock the gate to escape .' ,
181- 'Move: W A S D' ,
182- this . escaped ? 'Escape complete: dungeon cleared.' : 'Collect relic first, then reach the green exit marker.' ,
243+ 'Explore rooms, collect the relic, then escape through the unlocked route .' ,
244+ 'Move: W A S D | Restart run: R ' ,
245+ objectiveLine ,
183246 ] ) ;
184247
185248 renderer . strokeRect ( this . viewport . x , this . viewport . y , this . viewport . width , this . viewport . height , '#d8d5ff' , 2 ) ;
@@ -226,24 +289,28 @@ export default class DungeonCrawler3DScene extends Scene {
226289 ) ;
227290 }
228291
229- drawWireBox (
230- renderer ,
231- this . exitGoal ,
232- { width : this . exitGoal . width , height : this . exitGoal . height , depth : this . exitGoal . depth } ,
233- cameraState ,
234- projectionViewport ,
235- '#4ade80' ,
236- 2 ,
237- ) ;
292+ const exitColor = this . relicCollected ? '#4ade80' : '#475569' ;
293+ drawWireBox ( renderer , this . exitGoal , { width : this . exitGoal . width , height : this . exitGoal . height , depth : this . exitGoal . depth } , cameraState , projectionViewport , exitColor , 2 ) ;
238294
239295 const player = this . world . requireComponent ( this . playerId , 'transform3D' ) ;
240- drawPanel ( renderer , 620 , 34 , 300 , 126 , 'Dungeon Runtime' , [
296+ const statusLine =
297+ this . runState === 'seek-relic'
298+ ? 'Seek relic'
299+ : this . runState === 'escape'
300+ ? 'Exit route open'
301+ : 'Escaped' ;
302+ const flashLine = this . interactionFlashSeconds > 0 ? 'Interaction: event pulse active' : 'Interaction: idle' ;
303+
304+ drawPanel ( renderer , 620 , 34 , 300 , 236 , 'Dungeon Runtime' , [
241305 `Explorer: x=${ player . x . toFixed ( 2 ) } y=${ player . y . toFixed ( 2 ) } z=${ player . z . toFixed ( 2 ) } ` ,
306+ `Run: ${ this . runCount } | State: ${ statusLine } ` ,
242307 `Relic: ${ this . relicCollected ? 'collected' : 'missing' } ` ,
243308 `Gate: ${ this . relicCollected ? 'unlocked' : 'locked' } ` ,
244309 `Escaped: ${ this . escaped ? 'yes' : 'no' } ` ,
245310 `Resolved collisions: ${ this . lastPhysicsSummary . collisionCount } ` ,
311+ `Run time: ${ this . runElapsedSeconds . toFixed ( 1 ) } s | Last clear: ${ this . lastCompletionSeconds . toFixed ( 1 ) } s` ,
312+ flashLine ,
313+ `Last reset: ${ this . lastResetReason } ` ,
246314 ] ) ;
247315 }
248316}
249-
0 commit comments