@@ -154,7 +154,8 @@ public override void Update(Task caller)
154154 }
155155
156156 // Try to deploy a pending spawns
157- if ( spawnInProgress ) {
157+ if ( spawnInProgress )
158+ {
158159 TryPlacement ( ) ;
159160 GameManager . Instance . RaiseOnEncounterEvent ( ) ;
160161 }
@@ -179,7 +180,7 @@ void CreatePendingFoeSpawn(Foe foe)
179180 pendingFoesSpawned = 0 ;
180181 }
181182
182- bool TryPlacement ( bool placeEnemyBehindPlayer = true )
183+ void TryPlacement ( )
183184 {
184185 PlayerEnterExit playerEnterExit = GameManager . Instance . PlayerEnterExit ;
185186
@@ -188,25 +189,25 @@ bool TryPlacement(bool placeEnemyBehindPlayer = true)
188189 if ( isSendAction )
189190 {
190191 if ( ! GameManager . Instance . PlayerGPS . IsPlayerInLocationRect )
191- return false ;
192+ return ;
192193 }
193194
194195 // Place in world near player depending on local area
195196 if ( playerEnterExit . IsPlayerInsideBuilding )
196197 {
197- return PlaceFoeBuildingInterior ( pendingFoeGameObjects , playerEnterExit . Interior , placeEnemyBehindPlayer ) ;
198+ PlaceFoeBuildingInterior ( pendingFoeGameObjects , playerEnterExit . Interior ) ;
198199 }
199200 else if ( playerEnterExit . IsPlayerInsideDungeon )
200201 {
201- return PlaceFoeDungeonInterior ( pendingFoeGameObjects , playerEnterExit . Dungeon , placeEnemyBehindPlayer ) ;
202+ PlaceFoeDungeonInterior ( pendingFoeGameObjects , playerEnterExit . Dungeon ) ;
202203 }
203204 else if ( ! playerEnterExit . IsPlayerInside && GameManager . Instance . PlayerGPS . IsPlayerInLocationRect )
204205 {
205- return PlaceFoeExteriorLocation ( pendingFoeGameObjects , GameManager . Instance . StreamingWorld . CurrentPlayerLocationObject , placeEnemyBehindPlayer ) ;
206+ PlaceFoeExteriorLocation ( pendingFoeGameObjects , GameManager . Instance . StreamingWorld . CurrentPlayerLocationObject ) ;
206207 }
207208 else
208209 {
209- return PlaceFoeWilderness ( pendingFoeGameObjects , placeEnemyBehindPlayer ) ;
210+ PlaceFoeWilderness ( pendingFoeGameObjects ) ;
210211 }
211212 }
212213
@@ -216,7 +217,7 @@ bool TryPlacement(bool placeEnemyBehindPlayer = true)
216217
217218 // Place foe somewhere near player when inside a building
218219 // Building interiors have spawn nodes for this placement so we can roll out foes all at once
219- bool PlaceFoeBuildingInterior ( GameObject [ ] gameObjects , DaggerfallInterior interiorParent , bool placeEnemyBehindPlayer = true )
220+ void PlaceFoeBuildingInterior ( GameObject [ ] gameObjects , DaggerfallInterior interiorParent )
220221 {
221222 // Must have a DaggerfallLocation parent
222223 if ( interiorParent == null )
@@ -228,127 +229,115 @@ bool PlaceFoeBuildingInterior(GameObject[] gameObjects, DaggerfallInterior inter
228229 // Always place foes around player rather than use spawn points
229230 // Spawn points work well for "interior hunt" quests but less so for "directly attack the player"
230231 // Feel just placing freely will yield best results overall
231- return PlaceFoeFreely ( gameObjects , interiorParent . transform , 5 , 20 , placeEnemyBehindPlayer ) ;
232+ PlaceFoeFreely ( gameObjects , interiorParent . transform ) ;
233+ return ;
232234 }
233235
234236 // Place foe somewhere near player when inside a dungeon
235237 // Dungeons interiors are complex 3D environments with no navgrid/navmesh or known spawn nodes
236- bool PlaceFoeDungeonInterior ( GameObject [ ] gameObjects , DaggerfallDungeon dungeonParent , bool placeEnemyBehindPlayer = true )
238+ void PlaceFoeDungeonInterior ( GameObject [ ] gameObjects , DaggerfallDungeon dungeonParent )
237239 {
238- return PlaceFoeFreely ( gameObjects , dungeonParent . transform , 5 , 20 , placeEnemyBehindPlayer ) ;
240+ PlaceFoeFreely ( gameObjects , dungeonParent . transform ) ;
239241 }
240242
241243 // Place foe somewhere near player when outside a location navgrid is available
242244 // Navgrid placement helps foe avoid getting tangled in geometry like buildings
243- bool PlaceFoeExteriorLocation ( GameObject [ ] gameObjects , DaggerfallLocation locationParent , bool placeEnemyBehindPlayer = true )
245+ void PlaceFoeExteriorLocation ( GameObject [ ] gameObjects , DaggerfallLocation locationParent )
244246 {
245- return PlaceFoeFreely ( gameObjects , locationParent . transform , 5 , 20 , placeEnemyBehindPlayer ) ;
247+ PlaceFoeFreely ( gameObjects , locationParent . transform ) ;
246248 }
247249
248250 // Place foe somewhere near player when outside and no navgrid available
249251 // Wilderness environments are currently open so can be placed on ground anywhere within range
250- bool PlaceFoeWilderness ( GameObject [ ] gameObjects , bool placeEnemyBehindPlayer = true )
252+ void PlaceFoeWilderness ( GameObject [ ] gameObjects )
251253 {
252254 // TODO this false will need to be true when start caching enemies
253255 GameManager . Instance . StreamingWorld . TrackLooseObject ( gameObjects [ pendingFoesSpawned ] , false , - 1 , - 1 , true ) ;
254- return PlaceFoeFreely ( gameObjects , null , 8f , 25f , placeEnemyBehindPlayer , false ) ;
256+ PlaceFoeFreely ( gameObjects , null , 8f , 25f ) ;
255257 }
256258
257259 // Uses raycasts to find next spawn position just outside of player's field of view
258- bool PlaceFoeFreely ( GameObject [ ] gameObjects , Transform parent , float minDistance = 5f , float maxDistance = 20f , bool placeEnemyBehindPlayer = true , bool lineOfSight = true , int spawnTries = 15 )
260+ void PlaceFoeFreely ( GameObject [ ] gameObjects , Transform parent , float minDistance = 5f , float maxDistance = 20f )
259261 {
260- const float maxFloorDistance = 4f ;
261262 const float overlapSphereRadius = 0.65f ;
262-
263- // override min/max distance in small interior spaces.
264- var dfInterior = GameObject . FindObjectOfType < DaggerfallInterior > ( ) ;
265- if ( dfInterior ) {
266- Bounds bounds = new Bounds ( dfInterior . transform . position , Vector3 . one ) ;
267- Renderer [ ] renderers = dfInterior . GetComponentsInChildren < Renderer > ( ) ;
268- foreach ( Renderer renderer in renderers )
269- {
270- bounds . Encapsulate ( renderer . bounds ) ;
271- }
272- Vector3 playerPos = GameManager . Instance . PlayerController . transform . position ;
273- Vector3 boundsMin = bounds . min ;
274- Vector3 boundsMax = bounds . max ;
275- boundsMin . y = boundsMax . y = playerPos . y ;
276- maxDistance = Mathf . Max ( Vector3 . Distance ( playerPos , boundsMin ) , Vector3 . Distance ( playerPos , boundsMax ) ) ;
277- minDistance = Mathf . Min ( minDistance , maxDistance / 4f ) ;
278- }
263+ const float separationDistance = 1.25f ;
264+ const float maxFloorDistance = 4f ;
279265
280266 // Must have received a valid array
281267 if ( gameObjects == null || gameObjects . Length == 0 )
282- return false ;
283-
284- // Skip this foe if destroyed (e.g. player left building where pending)
285- if ( ! gameObjects [ pendingFoesSpawned ] )
286- {
287- pendingFoesSpawned ++ ;
288- return true ;
289- }
268+ return ;
290269
291270 // Set parent - otherwise caller must set a parent
292271 if ( parent )
293272 gameObjects [ pendingFoesSpawned ] . transform . parent = parent ;
294273
295- var enemyCC = gameObjects [ pendingFoesSpawned ] . GetComponent < CharacterController > ( ) ;
296- var playerCC = GameManager . Instance . PlayerController ;
297- // Set parent if none specified already
298- if ( ! gameObjects [ pendingFoesSpawned ] . transform . parent )
299- gameObjects [ pendingFoesSpawned ] . transform . parent = GameObjectHelper . GetBestParent ( ) ;
300-
301- for ( int i = 0 ; i < spawnTries ; ++ i ) {
302- float fov = GameManager . Instance . MainCamera . fieldOfView ;
303- float randomAngle ;
304- if ( ! lineOfSight || i > spawnTries / 3 ) // give up on line of sight checks after we've tried a few times
305- randomAngle = UnityEngine . Random . Range ( - 180 , 180 ) ;
306- else if ( ! placeEnemyBehindPlayer )
307- randomAngle = UnityEngine . Random . Range ( - fov , fov ) ;
308- else
309- randomAngle = UnityEngine . Random . Range ( fov , 180f ) * ( UnityEngine . Random . value < 0.5f ? - 1f : 1f ) ;
310- Quaternion rot = Quaternion . Euler ( 0 , randomAngle , 0 ) ;
311- Vector3 angle = ( rot * Vector3 . forward ) . normalized ;
312- Vector3 spawnDirection = GameManager . Instance . PlayerObject . transform . TransformDirection ( angle ) . normalized ;
313- Vector3 playerFootPosition = playerCC . transform . position + playerCC . center - playerCC . height / 2f * Vector3 . down ;
314- Vector3 spawnPos = playerFootPosition + spawnDirection * UnityEngine . Random . Range ( minDistance , maxDistance ) ;
315- enemyCC . transform . rotation = playerCC . transform . rotation ;
316- spawnPos += enemyCC . height / 2f * Vector3 . up + enemyCC . center ;
317-
318- // Must be able to find a surface below
319- if ( ! Physics . Raycast ( spawnPos , Vector3 . down , out RaycastHit floorHit , maxFloorDistance , DFULayerMasks . CorporealMask ) )
320- continue ;
321-
322- // Ensure this is open space
323- spawnPos = floorHit . point + enemyCC . center + ( enemyCC . height / 2f + .5f ) * Vector3 . up ;
324- // Collider[] colliders = Physics.OverlapCapsule(spawnPos - enemyCC.height/2f * Vector3.up, spawnPos + enemyCC.height/2f * Vector3.up, enemyCC.radius, DFULayerMasks.CorporealMask);
325- Collider [ ] colliders = Physics . OverlapSphere ( spawnPos , overlapSphereRadius ) ;
326- if ( colliders . Length > 0 )
327- continue ;
328-
329- Debug . Log ( $ "CreateFoe: Found an enemy spawn point in { i } tries") ;
330-
331- // This looks like a good spawn position
332- pendingFoeGameObjects [ pendingFoesSpawned ] . transform . position = spawnPos ;
333- FinalizeFoe ( pendingFoeGameObjects [ pendingFoesSpawned ] ) ;
334- gameObjects [ pendingFoesSpawned ] . transform . LookAt ( GameManager . Instance . PlayerObject . transform . position ) ;
335-
336- // Send msg message on first spawn only
337- if ( msgMessageID != - 1 )
338- {
339- ParentQuest . ShowMessagePopup ( msgMessageID , oncePerQuest : true ) ;
340- msgMessageID = - 1 ;
341- }
274+ // Select a left or right direction outside of camera FOV
275+ Quaternion rotation ;
276+ float directionAngle = GameManager . Instance . MainCamera . fieldOfView ;
277+ directionAngle += UnityEngine . Random . Range ( 0f , 4f ) ;
278+ if ( UnityEngine . Random . Range ( 0f , 1f ) > 0.5f )
279+ rotation = Quaternion . Euler ( 0 , - directionAngle , 0 ) ;
280+ else
281+ rotation = Quaternion . Euler ( 0 , directionAngle , 0 ) ;
282+
283+ // Get direction vector and create a new ray
284+ Vector3 angle = ( rotation * Vector3 . forward ) . normalized ;
285+ Vector3 spawnDirection = GameManager . Instance . PlayerObject . transform . TransformDirection ( angle ) . normalized ;
286+ Ray ray = new Ray ( GameManager . Instance . PlayerObject . transform . position , spawnDirection ) ;
287+
288+ // Check for a hit
289+ Vector3 currentPoint ;
290+ RaycastHit initialHit ;
291+ if ( Physics . Raycast ( ray , out initialHit , maxDistance ) )
292+ {
293+ float cos_normal = Vector3 . Dot ( - spawnDirection , initialHit . normal . normalized ) ;
294+ if ( cos_normal < 1e-6 )
295+ return ;
296+ float separationForward = separationDistance / cos_normal ;
297+
298+ // Must be greater than minDistance
299+ float distanceSlack = initialHit . distance - separationForward - minDistance ;
300+ if ( distanceSlack < 0f )
301+ return ;
302+
303+ // Separate out from hit point
304+ float extraDistance = UnityEngine . Random . Range ( 0f , Mathf . Min ( 2f , distanceSlack ) ) ;
305+ currentPoint = initialHit . point - spawnDirection * ( separationForward + extraDistance ) ;
306+ }
307+ else
308+ {
309+ // Player might be in an open area (e.g. outdoors) pick a random point along spawn direction
310+ currentPoint = GameManager . Instance . PlayerObject . transform . position + spawnDirection * UnityEngine . Random . Range ( minDistance , maxDistance ) ;
311+ }
312+
313+ // Must be able to find a surface below
314+ RaycastHit floorHit ;
315+ ray = new Ray ( currentPoint , Vector3 . down ) ;
316+ if ( ! Physics . Raycast ( ray , out floorHit , maxFloorDistance ) )
317+ return ;
318+
319+ // Ensure this is open space
320+ Vector3 testPoint = floorHit . point + Vector3 . up * separationDistance ;
321+ Collider [ ] colliders = Physics . OverlapSphere ( testPoint , overlapSphereRadius ) ;
322+ if ( colliders . Length > 0 )
323+ return ;
324+
325+ // This looks like a good spawn position
326+ pendingFoeGameObjects [ pendingFoesSpawned ] . transform . position = testPoint ;
327+ FinalizeFoe ( pendingFoeGameObjects [ pendingFoesSpawned ] ) ;
328+ gameObjects [ pendingFoesSpawned ] . transform . LookAt ( GameManager . Instance . PlayerObject . transform . position ) ;
342329
343- // Increment count
344- pendingFoesSpawned ++ ;
345- return true ;
330+ // Send msg message on first spawn only
331+ if ( msgMessageID != - 1 )
332+ {
333+ ParentQuest . ShowMessagePopup ( msgMessageID , oncePerQuest : true ) ;
334+ msgMessageID = - 1 ;
346335 }
347336
348- // Couldn't find a spawn point
349- Debug . Log ( $ "CreateFoe: Couldn't find a valid enemy spawn point after { spawnTries } tries.") ;
350- return false ;
337+ // Increment count
338+ pendingFoesSpawned ++ ;
351339 }
340+
352341 // Fine tunes foe position slightly based on mobility and enables GameObject
353342 void FinalizeFoe ( GameObject go )
354343 {
0 commit comments