wildlifeSpawner#maxCost | / max cost of all animals generated by the spawner |
wildlifeSpawner#checkTimeInterval | / how often creation process is executed (in seconds) |
wildlifeSpawner.area#areaSpawnRadius | / radius of the main spawn circle, testing circles are placed on this radius |
wildlifeSpawner.area#areaMaxRadius | / radius where animals are removed |
wildlifeSpawner.area#spawnCircleRadius | / radius of testing circles |
wildlifeSpawner.area.species#name | / name of the animal to spawn |
wildlifeSpawner.area.species#config | / configuration file of the animal to spawn |
wildlifeSpawner.area.species.spawnRules#hours | / daytimes where we can spawn an animal |
wildlifeSpawner.area.species.spawnRules#onField | / if true, will spawn on field ground |
wildlifeSpawner.area.species.spawnRules#hasTrees | / if true, will spawn if trees in testing circle |
wildlifeSpawner.area.species.cost | / cost of one animal |
wildlifeSpawner.area.species.maxCount | / maximal amount of animal to spawn |
wildlifeSpawner.area.species.spawnCount | / how many animals to spawn in one group |
wildlifeSpawner.area.species.groupSpawnRadius | / radius within which the animals are spawned |
425 | function WildlifeSpawner:checkArea(x, y, z, rules, radius, isInWater) |
426 | local validArea = false |
427 | local currentHour = math.floor(g_currentMission.environment.dayTime / (60 * 60 * 1000)) |
428 | local isOnField |
429 | local hasTrees |
430 | |
431 | for _, rule in pairs(rules) do |
432 | if (currentHour >= rule.hourFrom or currentHour <= rule.hourTo) then |
433 | local validRule = true |
434 | if rule.onField ~= WildlifeSpawner.RULE.DONT_CARE then |
435 | if isOnField == nil then |
436 | isOnField, _ = FSDensityMapUtil.getFieldDataAtWorldPosition(x, y, z) |
437 | end |
438 | validRule = (rule.onField == WildlifeSpawner.RULE.REQUIRED and isOnField) or |
439 | (rule.onField == WildlifeSpawner.RULE.NOT_ALLOWED and not isOnField) |
440 | end |
441 | |
442 | if validRule and rule.hasTrees ~= WildlifeSpawner.RULE.DONT_CARE then |
443 | if hasTrees == nil then |
444 | hasTrees = self:countTrees(x, y, z, radius) > 3 |
445 | end |
446 | validRule = (rule.hasTrees == WildlifeSpawner.RULE.REQUIRED and hasTrees) or |
447 | (rule.hasTrees == WildlifeSpawner.RULE.NOT_ALLOWED and not hasTrees) |
448 | end |
449 | |
450 | if validRule and rule.inWater ~= WildlifeSpawner.RULE.DONT_CARE then |
451 | validRule = (rule.inWater == WildlifeSpawner.RULE.REQUIRED and isInWater) or |
452 | (rule.inWater == WildlifeSpawner.RULE.NOT_ALLOWED and not isInWater) |
453 | end |
454 | |
455 | if validRule then |
456 | validArea = true |
457 | break |
458 | end |
459 | end |
460 | end |
461 | |
462 | return validArea |
463 | end |
388 | function WildlifeSpawner:checkAreas(x, y, z) |
389 | local testX = x |
390 | local testZ = z |
391 | local testY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, testX, 0, testZ) + 0.5 |
392 | |
393 | local isInWater = self:getIsInWater(testX, testY, testZ) |
394 | |
395 | for _, area in pairs(self.areas) do |
396 | local hasSpawned = false |
397 | for _, interestArea in pairs(self.areasOfInterest) do |
398 | local distSq = (testX - interestArea.positionX) * (testX - interestArea.positionX) + (testZ - interestArea.positionZ) * (testZ - interestArea.positionZ) |
399 | if (distSq < (area.areaSpawnRadius * area.areaSpawnRadius)) then |
400 | hasSpawned = self:trySpawnAtArea(area.species, interestArea.radius, testX, testY, testZ, isInWater) |
401 | if hasSpawned then |
402 | break |
403 | end |
404 | end |
405 | end |
406 | if not hasSpawned then |
407 | local angle = math.rad(math.random(0, 360)) |
408 | testX = x + area.areaSpawnRadius * math.cos(angle) - area.areaSpawnRadius * math.sin(angle) |
409 | testZ = z + area.areaSpawnRadius * math.cos(angle) + area.areaSpawnRadius * math.sin(angle) |
410 | testY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, testX, 0, testZ) + 0.5 |
411 | |
412 | self:trySpawnAtArea(area.species, area.spawnCircleRadius, testX, testY, testZ, isInWater) |
413 | end |
414 | end |
415 | end |
562 | function WildlifeSpawner:debugDraw() |
563 | renderText(0.02, 0.95, 0.02, string.format("Wildlife Info\nCost(%d / %d)", self.totalCost, self.maxCost)) |
564 | local passedTest, originX, originY, originZ = self:getPlayerCenter() |
565 | |
566 | if passedTest then |
567 | for _, area in pairs(self.areas) do |
568 | for _, species in pairs(area.species) do |
569 | for _, spawn in pairs(species.spawned) do |
570 | if spawn.spawnId ~= nil then |
571 | local distance = 0.0 |
572 | |
573 | if species.classType == "companionAnimal" then |
574 | distance, _ = getCompanionClosestDistance(spawn.spawnId, originX, originY, originZ) |
575 | elseif species.classType == "lightWildlife" then |
576 | distance = species.lightWildlife:getClosestDistance(originX, originY, originZ) |
577 | distance = math.sqrt(distance) |
578 | end |
579 | local text = string.format("[%s][%d]\n- nearest player distance (%.3f)", species.name, spawn.spawnId, distance) |
580 | |
581 | Utils.renderTextAtWorldPosition(spawn.posX, spawn.posY + 0.12, spawn.posZ, text, getCorrectTextSize(0.012), 0) |
582 | DebugUtil.drawDebugCubeAtWorldPos(spawn.posX, spawn.posY, spawn.posZ, 1, 0, 0, 0, 1, 0, 0.05, 0.05, 0.05, 1.0, 1.0, 0.0) |
583 | DebugUtil.drawDebugCircle(spawn.posX, spawn.posY, spawn.posZ, species.groupSpawnRadius, 10.0, {1.0, 1.0, 0.0}) |
584 | end |
585 | end |
586 | end |
587 | end |
588 | end |
589 | |
590 | for _, area in pairs(self.areas) do |
591 | for _, species in pairs(area.species) do |
592 | if species.classType == "companionAnimal" then |
593 | for _, spawn in pairs(species.spawned) do |
594 | for animalId=0, spawn.count - 1 do |
595 | local showAdditionalInfo = self:isInDebugList(spawn.spawnId, animalId) |
596 | local showId = (self.debugShowId == WildlifeSpawner.DEBUGSHOWIDSTATES.SINGLE and showAdditionalInfo) or self.debugShowId == WildlifeSpawner.DEBUGSHOWIDSTATES.ALL |
597 | companionDebugDraw(spawn.spawnId, animalId, showId, showAdditionalInfo and self.debugShowSteering, showAdditionalInfo and self.debugShowAnimation) |
598 | end |
599 | end |
600 | end |
601 | end |
602 | end |
603 | end |
134 | function WildlifeSpawner:loadMapData(xmlFile) |
135 | local filename = getXMLString(xmlFile, "map.wildlife#filename") |
136 | if filename == nil or filename == "" then |
137 | Logging.xmlInfo(xmlFile, "No wildlife config file defined") |
138 | return false |
139 | end |
140 | |
141 | filename = Utils.getFilename(filename, g_currentMission.baseDirectory) |
142 | |
143 | local wildlifeXmlFile = loadXMLFile("wildlife", filename) |
144 | if wildlifeXmlFile == 0 or wildlifeXmlFile == nil then |
145 | Logging.xmlError(xmlFile, "Could not load wildlife config file '%s'", filename) |
146 | return false |
147 | end |
148 | |
149 | self.maxCost = Utils.getNoNil(getXMLInt(wildlifeXmlFile, "wildlifeSpawner#maxCost"), 0) |
150 | self.checkTimeInterval = Utils.getNoNil(getXMLFloat(wildlifeXmlFile, "wildlifeSpawner#checkTimeInterval"), 1.0) * 1000.0 |
151 | self.maxAreaOfInterest = Utils.getNoNil(getXMLFloat(wildlifeXmlFile, "wildlifeSpawner#maxAreaOfInterest"), 1) |
152 | self.areaOfInterestliveTime = Utils.getNoNil(getXMLFloat(wildlifeXmlFile, "wildlifeSpawner#areaOfInterestliveTime"), 1.0) * 1000.0 |
153 | local i = 0 |
154 | while true do |
155 | local areaBaseString = string.format("wildlifeSpawner.area(%d)", i) |
156 | if not hasXMLProperty(wildlifeXmlFile, areaBaseString) then |
157 | break |
158 | end |
159 | local newArea = {} |
160 | newArea.areaSpawnRadius = Utils.getNoNil(getXMLFloat(wildlifeXmlFile, areaBaseString .. "#areaSpawnRadius"), 1.0) |
161 | newArea.areaMaxRadius = Utils.getNoNil(getXMLFloat(wildlifeXmlFile, areaBaseString .. "#areaMaxRadius"), 1.0) |
162 | newArea.spawnCircleRadius = Utils.getNoNil(getXMLFloat(wildlifeXmlFile, areaBaseString .. "#spawnCircleRadius"), 1.0) |
163 | |
164 | newArea.species = {} |
165 | local j = 0 |
166 | while true do |
167 | local speciesBaseString = string.format("%s.species(%d)", areaBaseString, j) |
168 | if not hasXMLProperty(wildlifeXmlFile, speciesBaseString) then |
169 | break |
170 | end |
171 | local classTypeString = getXMLString(wildlifeXmlFile, speciesBaseString .. "#classType") |
172 | local classType = nil |
173 | |
174 | if classTypeString ~= nil then |
175 | if string.lower(classTypeString) == "companionanimal" then |
176 | classType = "companionAnimal" |
177 | elseif string.lower(classTypeString) == "lightwildlife" then |
178 | classType = "lightWildlife" |
179 | end |
180 | end |
181 | if classType ~= nil then |
182 | local newSpecies = {} |
183 | newSpecies.classType = classType |
184 | newSpecies.name = getXMLString(wildlifeXmlFile, speciesBaseString .. "#name") |
185 | newSpecies.configFilename = getXMLString(wildlifeXmlFile, speciesBaseString .. "#config") |
186 | newSpecies.cost = getXMLFloat(wildlifeXmlFile, speciesBaseString .. ".cost") |
187 | newSpecies.minCount = getXMLInt(wildlifeXmlFile, speciesBaseString .. ".minCount") |
188 | newSpecies.maxCount = getXMLInt(wildlifeXmlFile, speciesBaseString .. ".maxCount") |
189 | newSpecies.currentCount = 0 |
190 | newSpecies.spawnCount = getXMLInt(wildlifeXmlFile, speciesBaseString .. ".spawnCount") |
191 | newSpecies.groupSpawnRadius = getXMLInt(wildlifeXmlFile, speciesBaseString .. ".groupSpawnRadius") |
192 | newSpecies.spawned = {} |
193 | newSpecies.lightWildlife = nil |
194 | if classType == "lightWildlife" then |
195 | if newSpecies.name == "crow" then |
196 | newSpecies.lightWildlife = CrowsWildlife.new() |
197 | newSpecies.lightWildlife:load(Utils.getNoNil(getXMLString(wildlifeXmlFile, speciesBaseString .. "#config"), "")) |
198 | end |
199 | end |
200 | |
201 | newSpecies.spawnRules = {} |
202 | local k = 0 |
203 | while true do |
204 | local spawnRuleBaseString = string.format("%s.spawnRules.rule(%d)", speciesBaseString, k) |
205 | if not hasXMLProperty(wildlifeXmlFile, spawnRuleBaseString) then |
206 | break |
207 | end |
208 | |
209 | local newRule = {} |
210 | newRule.hourFrom = getXMLInt(wildlifeXmlFile, spawnRuleBaseString .. "#hourFrom") |
211 | newRule.hourTo = getXMLInt(wildlifeXmlFile, spawnRuleBaseString .. "#hourTo") |
212 | newRule.onField = self:parseSpawnRule(getXMLString(wildlifeXmlFile, spawnRuleBaseString .. "#onField")) |
213 | newRule.hasTrees = self:parseSpawnRule(getXMLString(wildlifeXmlFile, spawnRuleBaseString .. "#hasTrees")) |
214 | newRule.inWater = self:parseSpawnRule(getXMLString(wildlifeXmlFile, spawnRuleBaseString .. "#inWater")) |
215 | table.insert(newSpecies.spawnRules, newRule) |
216 | k = k + 1 |
217 | end |
218 | |
219 | table.insert(newArea.species, newSpecies) |
220 | end |
221 | j = j + 1 |
222 | end |
223 | table.insert(self.areas, newArea) |
224 | i = i + 1 |
225 | end |
226 | delete(wildlifeXmlFile) |
227 | |
228 | return true |
229 | end |
45 | function WildlifeSpawner.new(customMt) |
46 | local self = setmetatable({}, customMt or WildlifeSpawner_mt) |
47 | |
48 | self.isEnabled = true -- flag to disable all wildlife |
49 | self.collisionDetectionMask = 4096 -- dynamic_objects |
50 | self.maxCost = 0 |
51 | self.checkTimeInterval = 0.0 |
52 | self.nextCheckTime = 0.0 |
53 | self.areas = {} |
54 | self.areasOfInterest = {} |
55 | self.totalCost = 0 |
56 | self.treeCount = 0 |
57 | self.playerNode = nil |
58 | self.avoidDistance = 20 |
59 | |
60 | -- Debug |
61 | -- if g_addCheatCommands then |
62 | self.debugAnimalList = {} |
63 | self.debugShow = false |
64 | self.debugShowId = WildlifeSpawner.DEBUGSHOWIDSTATES.NONE |
65 | self.debugShowSteering = false |
66 | self.debugShowAnimation = false |
67 | addConsoleCommand("gsWildlifeToggle", "Toggle wildlife on map", "consoleCommandToggleEnabled", self) |
68 | addConsoleCommand("gsWildlifeDebug", "Toggle shows/hide all wildlife debug information.", "consoleCommandToggleShowWildlife", self) |
69 | addConsoleCommand("gsWildlifeDebugId", "Toggle shows/hide all wildlife animal id.", "consoleCommandToggleShowWildlifeId", self) |
70 | addConsoleCommand("gsWildlifeDebugSteering", "Toggle shows/hide animal steering information.", "consoleCommandToggleShowWildlifeSteering", self) |
71 | addConsoleCommand("gsWildlifeDebugAnimation", "Toggle shows/hide animal animation information.", "consoleCommandToggleShowWildlifeAnimation", self) |
72 | addConsoleCommand("gsWildlifeDebugAnimalAdd", "Adds an animal to a debug list.", "consoleCommandAddWildlifeAnimalToDebug", self) |
73 | addConsoleCommand("gsWildlifeDebugAnimalRemove", "Removes an animal to a debug list.", "consoleCommandRemoveWildlifeAnimalToDebug", self) |
74 | -- end |
75 | |
76 | return self |
77 | end |
300 | function WildlifeSpawner:removeFarAwayAnimals() |
301 | local passedTest, originX, originY, originZ = self:getPlayerCenter() |
302 | |
303 | if passedTest then |
304 | for _, area in pairs(self.areas) do |
305 | for _, species in pairs(area.species) do |
306 | if species.classType == "companionAnimal" then |
307 | for i=#species.spawned, 1, -1 do |
308 | local spawn = species.spawned[i] |
309 | if spawn.spawnId ~= nil then |
310 | local distance, _ = getCompanionClosestDistance(spawn.spawnId, originX, originY, originZ) |
311 | |
312 | if distance > area.areaMaxRadius then |
313 | delete(spawn.spawnId) |
314 | spawn.spawnId = nil |
315 | species.currentCount = species.currentCount - spawn.count |
316 | self.totalCost = self.totalCost - species.cost * spawn.count |
317 | table.remove(species.spawned, i) |
318 | end |
319 | end |
320 | end |
321 | elseif species.classType == "lightWildlife" and species.lightWildlife ~= nil then |
322 | local removedAnimalsCount = species.lightWildlife:removeFarAwayAnimals(area.areaMaxRadius, originX, originY, originZ) |
323 | if removedAnimalsCount > 0 then |
324 | species.currentCount = species.currentCount - removedAnimalsCount |
325 | self.totalCost = self.totalCost - species.cost * removedAnimalsCount |
326 | for i=#species.spawned, 1, -1 do |
327 | --local spawn = species.spawned[i] |
328 | if species.lightWildlife:countSpawned() == 0 then |
329 | table.remove(species.spawned, i) |
330 | end |
331 | end |
332 | end |
333 | end |
334 | end |
335 | end |
336 | end |
337 | end |
527 | function WildlifeSpawner:spawnAnimals(species, spawnPosX, spawnPosY, spawnPosZ) |
528 | local xmlFilename = Utils.getFilename(species.configFilename, g_currentMission.loadingMapBaseDirectory) |
529 | |
530 | if species.name == nil or |
531 | xmlFilename == nil or |
532 | g_currentMission.terrainRootNode == nil or |
533 | species.currentCount >= species.maxCount then |
534 | return false |
535 | end |
536 | local nbAnimals = self:countAnimalsTobeSpawned(species) |
537 | if nbAnimals == 0 then |
538 | return false |
539 | end |
540 | local id = nil |
541 | if species.classType == "companionAnimal" then |
542 | id = createAnimalCompanionManager(species.name, xmlFilename, "wildlifeAnimal", spawnPosX, spawnPosY, spawnPosZ, g_currentMission.terrainRootNode, g_currentMission:getIsServer(), g_currentMission:getIsClient(), nbAnimals, AudioGroup.ENVIRONMENT) |
543 | setCompanionAvoidPlayer(id, self.playerNode, self.avoidDistance) |
544 | |
545 | local groundMask = CollisionFlag.TERRAIN + CollisionFlag.STATIC_WORLD |
546 | local obstacleMask = CollisionFlag.STATIC_OBJECTS + CollisionFlag.DYNAMIC_OBJECT + CollisionFlag.VEHICLE |
547 | setCompanionCollisionMask(id, groundMask, obstacleMask, CollisionFlag.WATER) |
548 | elseif species.classType == "lightWildlife" then |
549 | id = species.lightWildlife:createAnimals(species.name, spawnPosX, spawnPosY, spawnPosZ, nbAnimals) |
550 | end |
551 | if (id ~= nil) and (id ~= 0) then |
552 | table.insert(species.spawned, {spawnId = id, posX = spawnPosX, posY = spawnPosY, posZ = spawnPosZ, count = nbAnimals, avoidNode = self.playerNode}) |
553 | species.currentCount = species.currentCount + nbAnimals |
554 | self.totalCost = self.totalCost + species.cost * nbAnimals |
555 | return true |
556 | end |
557 | return false |
558 | end |
251 | function WildlifeSpawner:update(dt) |
252 | -- remove outdated areas of interest |
253 | self:updateAreaOfInterest(dt) |
254 | |
255 | if self.isEnabled then |
256 | -- add animals |
257 | self.nextCheckTime = self.nextCheckTime - dt |
258 | if (self.nextCheckTime < 0.0) then |
259 | self.nextCheckTime = self.checkTimeInterval |
260 | self:updateSpawner() |
261 | end |
262 | -- remove animals too far away |
263 | self:removeFarAwayAnimals() |
264 | |
265 | self.playerNode = nil |
266 | if g_currentMission.controlPlayer and g_currentMission.player ~= nil then |
267 | self.playerNode = g_currentMission.player.rootNode |
268 | elseif g_currentMission.controlledVehicle ~= nil then |
269 | self.playerNode = g_currentMission.controlledVehicle.rootNode |
270 | end |
271 | |
272 | -- update animal context |
273 | for _, area in pairs(self.areas) do |
274 | for _, species in pairs(area.species) do |
275 | if species.classType == "companionAnimal" then |
276 | for _, spawn in pairs(species.spawned) do |
277 | if spawn.spawnId ~= nil then |
278 | setCompanionDaytime(spawn.spawnId, g_currentMission.environment.dayTime) |
279 | if spawn.avoidNode ~= self.playerNode then |
280 | setCompanionAvoidPlayer(spawn.spawnId, self.playerNode, self.avoidDistance) |
281 | spawn.avoidNode = self.playerNode |
282 | end |
283 | end |
284 | end |
285 | elseif species.classType == "lightWildlife" then |
286 | species.lightWildlife:update(dt) |
287 | end |
288 | end |
289 | end |
290 | |
291 | if self.debugShow then |
292 | -- debug |
293 | self:debugDraw() |
294 | end |
295 | end |
296 | end |