@@ -32,13 +32,18 @@ export class OwnerGoal {
3232 private colliders : any [ ] = [ ]
3333 private ownerRoot : Group | null = null
3434 private ownerMixer : AnimationMixer | null = null
35+ private ownerBaseOffsetY = 0
36+ private yardFallback : Group | null = null
37+ private houseRoot : Group | null = null
38+ private fenceRoot : Group | null = null
3539
3640 constructor (
3741 private readonly terrain : Terrain ,
3842 private readonly physics : Physics ,
3943 ) {
40- this . build ( )
44+ this . buildFallbackYard ( )
4145 void this . loadOwnerModel ( )
46+ void this . loadYardModels ( )
4247 this . reset ( )
4348 }
4449
@@ -75,7 +80,16 @@ export class OwnerGoal {
7580
7681 if ( this . ownerRoot ) {
7782 this . ownerRoot . position . copy ( this . humanOffset )
78- this . ownerRoot . position . y = this . ownerPosition . y - this . group . position . y
83+ this . ownerRoot . position . y = this . ownerPosition . y - this . group . position . y + this . ownerBaseOffsetY
84+ }
85+
86+ if ( this . houseRoot ) {
87+ this . houseRoot . position . copy ( this . houseOffset )
88+ }
89+
90+ if ( this . fenceRoot ) {
91+ this . fenceRoot . position . set ( 0 , 0 , 0 )
92+ this . updateFenceToTerrain ( )
7993 }
8094
8195 placed = true
@@ -93,7 +107,7 @@ export class OwnerGoal {
93107 return this . group . position . clone ( )
94108 }
95109
96- private build ( ) {
110+ private buildFallbackYard ( ) {
97111 const fenceMat = new MeshStandardMaterial ( { color : 0x7c5a34 , roughness : 0.9 } )
98112 const fencePostGeo = new BoxGeometry ( 0.3 , 1.3 , 0.3 )
99113 const fenceRailGeo = new BoxGeometry ( this . yardSize , 0.22 , 0.18 )
@@ -171,7 +185,8 @@ export class OwnerGoal {
171185 porch . position . set ( this . houseOffset . x , 0.35 , this . houseOffset . z + 3.2 )
172186 porch . receiveShadow = true
173187
174- this . group . add (
188+ this . yardFallback = new Group ( )
189+ this . yardFallback . add (
175190 fenceGroup ,
176191 houseBase ,
177192 roof ,
@@ -182,6 +197,8 @@ export class OwnerGoal {
182197 porch ,
183198 )
184199
200+ this . group . add ( this . yardFallback )
201+
185202 this . colliderDefs . push (
186203 {
187204 offset : new Vector3 ( this . houseOffset . x , 1.7 , this . houseOffset . z ) ,
@@ -247,8 +264,10 @@ export class OwnerGoal {
247264 }
248265
249266 const boxAfter = new Box3 ( ) . setFromObject ( root )
250- root . position . y += - boxAfter . min . y
267+ const baseOffset = - boxAfter . min . y
268+ root . position . y += baseOffset
251269
270+ this . ownerBaseOffsetY = baseOffset
252271 this . ownerRoot = root
253272 this . ownerMixer = gltf . animations . length ? new AnimationMixer ( root ) : null
254273 if ( this . ownerMixer && gltf . animations [ 0 ] ) {
@@ -261,6 +280,120 @@ export class OwnerGoal {
261280 }
262281 }
263282
283+ private async loadYardModels ( ) {
284+ try {
285+ const loader = new GLTFLoader ( )
286+ const [ houseGltf , fenceGltf ] = await Promise . all ( [
287+ loader . loadAsync ( `${ import . meta. env . BASE_URL } assets/yard/House.glb` ) ,
288+ loader . loadAsync ( `${ import . meta. env . BASE_URL } assets/yard/Fence.glb` ) ,
289+ ] )
290+
291+ const house = houseGltf . scene
292+ const fence = fenceGltf . scene
293+
294+ this . prepareYardModel ( house )
295+ this . prepareYardModel ( fence )
296+
297+ const houseBounds = this . groundModel ( house )
298+ const fenceBounds = new Box3 ( ) . setFromObject ( fence )
299+
300+ const houseScale = this . scaleModelToWidth ( houseBounds , 6.2 )
301+ house . scale . setScalar ( houseScale )
302+ this . groundModel ( house )
303+
304+ const fenceGroup = this . buildFencePerimeter ( fence , fenceBounds )
305+
306+ house . position . copy ( this . houseOffset )
307+
308+ this . houseRoot = house
309+ this . fenceRoot = fenceGroup
310+
311+ if ( this . yardFallback ) {
312+ this . yardFallback . visible = false
313+ }
314+
315+ this . group . add ( fenceGroup , house )
316+ this . updateFenceToTerrain ( )
317+ this . rebuildColliders ( )
318+ } catch ( error ) {
319+ console . warn ( 'Yard model load failed' , error )
320+ }
321+ }
322+
323+ private prepareYardModel ( root : Group ) {
324+ root . traverse ( ( obj ) => {
325+ const mesh = obj as any
326+ if ( mesh . isMesh ) {
327+ mesh . castShadow = true
328+ mesh . receiveShadow = true
329+ }
330+ } )
331+ }
332+
333+ private groundModel ( root : Group ) {
334+ const bounds = new Box3 ( ) . setFromObject ( root )
335+ root . position . y += - bounds . min . y
336+ return bounds
337+ }
338+
339+ private scaleModelToWidth ( bounds : Box3 , targetWidth : number ) {
340+ const size = bounds . getSize ( new Vector3 ( ) )
341+ const width = Math . max ( size . x , size . z )
342+ if ( width > 0.0001 ) {
343+ const scale = targetWidth / width
344+ return scale
345+ }
346+ return 1
347+ }
348+
349+ private buildFencePerimeter ( fenceRoot : Group , bounds : Box3 ) {
350+ const fenceGroup = new Group ( )
351+ const fenceScale = this . scaleModelToWidth ( bounds , this . yardSize )
352+
353+ const openingFraction = 0.28
354+ const gapStart = this . yardSize * 0.5 - this . yardSize * openingFraction
355+
356+ const createFenceSection = ( position : Vector3 , yaw : number ) => {
357+ const section = fenceRoot . clone ( true )
358+ section . scale . setScalar ( fenceScale )
359+ section . position . copy ( position )
360+ section . rotation . y = yaw
361+ this . prepareYardModel ( section )
362+ this . groundModel ( section )
363+ fenceGroup . add ( section )
364+ }
365+
366+ // North side (leave opening on the east end).
367+ if ( gapStart > - this . yardSize * 0.5 ) {
368+ createFenceSection ( new Vector3 ( ( gapStart - this . yardSize * 0.5 ) * 0.5 , 0 , this . yardSize / 2 ) , 0 )
369+ }
370+
371+ // South side.
372+ createFenceSection ( new Vector3 ( 0 , 0 , - this . yardSize / 2 ) , 0 )
373+
374+ // West side.
375+ createFenceSection ( new Vector3 ( - this . yardSize / 2 , 0 , 0 ) , Math . PI / 2 )
376+
377+ // East side (leave opening on the north end).
378+ if ( gapStart > - this . yardSize * 0.5 ) {
379+ createFenceSection ( new Vector3 ( this . yardSize / 2 , 0 , ( gapStart - this . yardSize * 0.5 ) * 0.5 ) , Math . PI / 2 )
380+ }
381+
382+ return fenceGroup
383+ }
384+
385+ private updateFenceToTerrain ( ) {
386+ if ( ! this . fenceRoot ) return
387+
388+ for ( const child of this . fenceRoot . children ) {
389+ const section = child as Group
390+ const worldPos = new Vector3 ( )
391+ section . getWorldPosition ( worldPos )
392+ const groundY = this . terrain . getHeightAt ( worldPos . x , worldPos . z )
393+ section . position . y = groundY - this . group . position . y
394+ }
395+ }
396+
264397 private rebuildColliders ( ) {
265398 const { RAPIER , world } = this . physics
266399
0 commit comments