diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 83ee08c..b329f2f 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -30,6 +30,12 @@ - [Setup](./urgency/setup.md) - [Usage](./urgency/usage.md) +# Gremlin Waves + +- [About](./waves.md) + - [Setup](./waves/setup.md) + - [Usage](./waves/usage.md) + # Book-Keeping - [Contributors](./contributors.md) diff --git a/docs/evac/setup.md b/docs/evac/setup.md index 3346e45..0934540 100644 --- a/docs/evac/setup.md +++ b/docs/evac/setup.md @@ -12,11 +12,11 @@ Evac:setup({ }, idStart = 5, loadUnloadPerIndividual = 2, - lossFlags = { 'GremlinEvacRedLoss', 'GremlinEvacBlueLoss' } - lossThresholds = { 25, 25 }, + lossFlags = { 1, 2 } + lossThresholds = { 0, 25 }, maxExtractable = { { - Generic = 12, + ['Downed Pilot'] = 12, Infantry = 12, M249 = 12, RPG = 12, @@ -25,7 +25,7 @@ Evac:setup({ JTAC = 3, }, { - Generic = 12, + ['Downed Pilot'] = 12, Infantry = 12, M249 = 12, RPG = 12, @@ -45,13 +45,14 @@ Evac:setup({ }, }, }, + startingUnits = { 'helicargo1', 'helicargo2', 'MedEvac1', 'MedEvac2', 'MedEvac3' }, startingZones = { { mode = Evac.modes.EVAC, name = "Test 1", smoke = trigger.smokeColor.Green, side = coalition.side.BLUE, active = true }, { mode = Evac.modes.RELAY, name = "Test 2", smoke = trigger.smokeColor.Orange, side = coalition.side.BLUE, active = true }, { mode = Evac.modes.SAFE, name = "Test 3", smoke = trigger.smokeColor.White, side = coalition.side.BLUE }, }, - winFlags = { 'GremlinEvacRedWin', 'GremlinEvacBlueWin' } - winThresholds = { 75, 75 }, + winFlags = { 3, 4 } + winThresholds = { 0, 75 }, }) ``` @@ -65,7 +66,7 @@ Evac:setup({ - Default: `{ ["C-130"] = 90, ["CH-47D"] = 44, ["CH-43E"] = 55, ["Mi-8MT"] = 24, ["Mi-24P"] = 5, ["Mi-24V"] = 5, ["Mi-26"] = 70, ["SH60B"] = 5, ["UH-1H"] = 8, ["UH-60L"] = 11 }` - `idStart`: The lowest ID number that Gremlin Evac will use to create units and groups - - Default: `500` + - Default: `50000` - `loadUnloadPerIndividual`: The amount of time it takes to load/unload a single evacuee onto/from an aircraft, in seconds - Default: `30` @@ -96,6 +97,9 @@ Evac:setup({ - `> 0`: Every `per` periods, until `maxExtractable` is reached - `period`: One of the `Gremlin.Periods` constants indicating how long a single period lasts +- `startingUnits`: Registers units for evacuation purposes during setup + - Default: `{}` + - `startingZones`: Registers zones for evacuation purposes during setup - Default: `{}` - Spec diff --git a/docs/evac/usage.md b/docs/evac/usage.md index a370d3e..ead95f1 100644 --- a/docs/evac/usage.md +++ b/docs/evac/usage.md @@ -37,7 +37,7 @@ Evac:setup({ lossThresholds = { 0, 25 }, maxExtractable = { { - Generic = 12, + ['Downed Pilot'] = 12, Infantry = 12, M249 = 12, RPG = 12, @@ -46,7 +46,7 @@ Evac:setup({ JTAC = 3, }, { - Generic = 12, + ['Downed Pilot'] = 12, Infantry = 12, M249 = 12, RPG = 12, @@ -66,6 +66,7 @@ Evac:setup({ }, }, }, + startingUnits = { 'helicargo1', 'helicargo2', 'MedEvac1', 'MedEvac2', 'MedEvac3' }, startingZones = { { mode = Evac.modes.EVAC, name = "Test 1", smoke = trigger.smokeColor.Green, side = coalition.side.BLUE, active = true }, { mode = Evac.modes.RELAY, name = "Test 2", smoke = trigger.smokeColor.Orange, side = coalition.side.BLUE, active = true }, diff --git a/docs/urgency/setup.md b/docs/urgency/setup.md index 65351e7..0930581 100644 --- a/docs/urgency/setup.md +++ b/docs/urgency/setup.md @@ -86,11 +86,11 @@ Urgency:setup({ - countdown - `reuse` whether to put this countdown back in the queue to be triggered again - `startTrigger` table - - `type` one of `time`, `event`, or `menu` + - `type` one of `time`, `flag`, `event`, or `menu` - `value` the time, event id and handler, or menu text to trigger on - `startFlag` the flag to set true when the countdown starts - `endTrigger` table - - `type` one of `time`, `event`, or `menu` + - `type` one of `time`, `flag`, `event`, or `menu` - `value` the time, event id and handler, or menu text to trigger on - `endFlag` the flag to set true when the countdown ends - `messages` a list of messages to display, keyed by the countdown's seconds since start (or seconds until end, if negative) diff --git a/docs/waves.md b/docs/waves.md new file mode 100644 index 0000000..69c6932 --- /dev/null +++ b/docs/waves.md @@ -0,0 +1,4 @@ + +## About + +Gremlin Waves is a reinforcements script for your missions. Running low on RedFor, but not ready for the fun to end? Call in reinforcements with Gremlin Waves! diff --git a/docs/waves/setup.md b/docs/waves/setup.md new file mode 100644 index 0000000..39a5efd --- /dev/null +++ b/docs/waves/setup.md @@ -0,0 +1,77 @@ + +### Setup + +#### Configuration + +```lua,editable +Waves:setup({ + adminPilotNames = { + 'Steve Jobs', + 'Linus Torvalds', + 'Bill Gates', + }, + waves = { + ['Wave 2'] = { + trigger = { + type = 'time', + value = 12600, -- 3.5 hours + }, + groups = { + ['F-14B'] = { + category = Group.Category.AIRPLANE, + country = country.USA, + zone = 'Reinforcement Staging', + scatter = 15, + orders = {}, + units = { + ['F-14B'] = 3, + }, + }, + ['Ground A'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = 'Reinforcement Staging', + scatter = 5, + orders = {}, + units = { + ['Infantry'] = 4, + } + }, + ['Ground B'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = 'Reinforcement Staging', + scatter = 5, + orders = {}, + units = { + ['RPG'] = 1, + ['Infantry'] = 3, + ['JTAC'] = 1, + } + }, + }, + }, + }, +}) +``` + +- `adminPilotNames` table + - list of pilots who should see the menu +- `waves` table + - collection of reinforcement waves for this mission + - wave + - `trigger` table + - `type` one of `time`, `flag`, `event`, or `menu` + - `value` a time, flag name / number, event id / filter, or menu item text + - `groups` table + - list of groups to spawn + - group + - `category` a member of `Groups.Category` indicating the group's category + - `country` a `country` id + - `zone` where to spawn the group + - `scatter` how far apart, in meters, to scatter units at spawn + - `orders` table + - a list of [DCS AI tasks](https://www.digitalcombatsimulator.com/en/support/faq/1267/#3307680) for the group to perform + - `units` table + - key: the unit type to spawn + - value: how many to spawn in the group diff --git a/docs/waves/usage.md b/docs/waves/usage.md new file mode 100644 index 0000000..80ae2ee --- /dev/null +++ b/docs/waves/usage.md @@ -0,0 +1,4 @@ + +### Usage + +Gremlin Waves is really simple to use. Configure it once (see [the Setup page](./setup.md)), and it handles the rest! diff --git a/src/evac.lua b/src/evac.lua index 1db5c50..c782e1a 100644 --- a/src/evac.lua +++ b/src/evac.lua @@ -1703,7 +1703,7 @@ Evac:setup({ }, idStart = 500, loadUnloadPerIndividual = 30, - lossFlags = { 'GremlinEvacRedLoss', 'GremlinEvacBlueLoss' } + lossFlags = { 'GremlinEvacRedLoss', 'GremlinEvacBlueLoss' }, lossThresholds = { 25, 25 }, maxExtractable = { { @@ -1742,7 +1742,10 @@ Evac:setup({ }, }, spawnWeight = 100, + startingUnits = {}, startingZones = {}, + winFlags = { 'GremlinEvacRedWin', 'GremlinEvacBlueWin' }, + winThresholds = { 75, 75 }, }) ``` @@ -1765,6 +1768,10 @@ zone(s) themselves. Four keys are required: `mode` (one of the constants in attached to). ]] function Evac:setup(config) + if config == nil then + config = {} + end + assert(Gremlin ~= nil, '\n\n** HEY MISSION-DESIGNER! **\n\nGremlin Script Tools has not been loaded!\n\nMake sure Gremlin Script Tools is loaded *before* running this script!\n') @@ -1781,11 +1788,7 @@ function Evac:setup(config) Gremlin.log.info(Evac.Id, string.format('Starting setup of %s version %s!', Evac.Id, Evac.Version)) -- start configuration - if not Evac._state.alreadyInitialized or (config ~= nil and config.forceReload) then - if config == nil then - config = {} - end - + if not Evac._state.alreadyInitialized or config.forceReload then Evac.beaconBatteryLife = config.beaconBatteryLife or 30 Evac.beaconSound = config.beaconSound or 'beacon.ogg' Evac.carryLimits = config.carryLimits or { @@ -1833,6 +1836,12 @@ function Evac:setup(config) }} } + if config.startingUnits ~= nil then + for _, _unit in pairs(config.startingUnits) do + Evac.units.register(_unit) + end + end + if config.startingZones ~= nil then for _, _zone in pairs(config.startingZones) do local _mode = _zone.mode or Evac.modes.EVAC diff --git a/src/urgency.lua b/src/urgency.lua index 4cc5534..2b13677 100644 --- a/src/urgency.lua +++ b/src/urgency.lua @@ -18,58 +18,6 @@ Urgency = { }, } -Urgency._internal.handlers = { - eventTriggers = { - event = -1, - fn = function(_event) - Gremlin.log.trace(Urgency.Id, string.format('Checking Event Against Countdowns : %s', Gremlin.events.idToName[_event.id])) - - for _name, _countdown in pairs(Urgency._state.countdowns.pending) do - if _countdown.startTrigger.type == 'event' - and ( - _countdown.startTrigger.value.id == _event.id - or _countdown.startTrigger.value.id == -1 - ) - and _countdown.startTrigger.value.filter(_event) - then - Urgency._internal.startCountdown(_name) - - Gremlin.log.trace(Urgency.Id, string.format('Started Countdown : %s', _name)) - end - end - - for _name, _countdown in pairs(Urgency._state.countdowns.active) do - if _countdown.endTrigger.type == 'event' - and ( - _countdown.endTrigger.value.id == _event.id - or _countdown.endTrigger.value.id == -1 - ) - and _countdown.endTrigger.value.filter(_event) - then - Urgency._internal.endCountdown(_name) - - Gremlin.log.trace(Urgency.Id, string.format('Ended Countdown : %s', _name)) - end - end - end - }, -} - -Urgency._internal.menu = { - { - text = 'Reset Active Countdowns', - func = Urgency._internal.resetCountdowns, - args = {}, - when = true, - }, - { - text = 'Reset All Countdowns', - func = Urgency._internal.restoreCountdowns, - args = {}, - when = true, - }, -} - Urgency._internal.getAdminUnits = function() Gremlin.log.trace(Urgency.Id, string.format('Scanning For Admin Units')) @@ -81,9 +29,11 @@ Urgency._internal.getAdminUnits = function() if _unit ~= nil and _unit.isExist ~= nil and _unit:isExist() and _unit.getPlayerName ~= nil then local _pilot = _unit:getPlayerName() if _pilot ~= nil and _pilot ~= '' then + Gremlin.log.trace(Urgency.Id, string.format('Found A Pilot : %s (in %s)', _pilot, _name)) + for _, _adminName in pairs(Urgency.config.adminPilotNames) do if _adminName == _pilot then - table.insert(_units, _name) + _units[_name] = _unit break end end @@ -152,9 +102,11 @@ Urgency._internal.doCountdowns = function() local _now = timer.getTime() for _name, _countdown in pairs(Urgency._state.countdowns.pending) do - if _countdown.startTrigger.type == 'time' and _countdown.startTrigger.value <= _now then + if (_countdown.startTrigger.type == 'time' and _countdown.startTrigger.value <= _now) + or (_countdown.startTrigger.type == 'flag' and trigger.misc.getUserFlag(_countdown.startTrigger.value) ~= 0) + then Urgency._internal.startCountdown(_name) - Gremlin.log.trace(Urgency.Id, string.format('Timer-Based Countdown Started : %s', _name)) + Gremlin.log.trace(Urgency.Id, string.format('%s-Based Countdown Started : %s', _countdown.startTrigger.type, _name)) end end @@ -250,6 +202,59 @@ Urgency._internal.restoreCountdowns = function() Gremlin.log.trace(Urgency.Id, string.format('Restored Configured Countdowns')) end +Urgency._internal.menu = { + { + text = 'Reset Active Countdowns', + func = Urgency._internal.resetCountdowns, + args = {}, + when = true, + }, + { + text = 'Reset All Countdowns', + func = Urgency._internal.restoreCountdowns, + args = {}, + when = true, + }, +} + +Urgency._internal.handlers = { + eventTriggers = { + event = -1, + fn = function(_event) + Gremlin.log.trace(Urgency.Id, + string.format('Checking Event Against Countdowns : %s', Gremlin.events.idToName[_event.id])) + + for _name, _countdown in pairs(Urgency._state.countdowns.pending) do + if _countdown.startTrigger.type == 'event' + and ( + _countdown.startTrigger.value.id == _event.id + or _countdown.startTrigger.value.id == -1 + ) + and _countdown.startTrigger.value.filter(_event) + then + Urgency._internal.startCountdown(_name) + + Gremlin.log.trace(Urgency.Id, string.format('Started Countdown : %s', _name)) + end + end + + for _name, _countdown in pairs(Urgency._state.countdowns.active) do + if _countdown.endTrigger.type == 'event' + and ( + _countdown.endTrigger.value.id == _event.id + or _countdown.endTrigger.value.id == -1 + ) + and _countdown.endTrigger.value.filter(_event) + then + Urgency._internal.endCountdown(_name) + + Gremlin.log.trace(Urgency.Id, string.format('Ended Countdown : %s', _name)) + end + end + end + }, +} + function Urgency:setup(config) if config == nil then config = {} diff --git a/src/waves.lua b/src/waves.lua new file mode 100644 index 0000000..e8a3d07 --- /dev/null +++ b/src/waves.lua @@ -0,0 +1,314 @@ +Waves = { + Id = 'Gremlin Waves', + Version = '202403.01', + + config = { + adminPilotNames = {}, + waves = {} + }, + + _state = { + alreadyInitialized = false, + paused = false, + }, + _internal = {}, +} + +Waves._internal.spawnWave = function(_name, _wave) + Gremlin.log.trace(Waves.Id, string.format('Started Spawning Wave : %s', _name)) + + for _groupName, _groupData in pairs(_wave.groups) do + local _spawnZone = trigger.misc.getZone(_groupData.zone) + + if _spawnZone == nil then + Gremlin.log.error(Waves.Id, "Can't find zone called " .. _groupData.zone) + return + end + + local _pos2 = { + x = _spawnZone.point.x, + y = _spawnZone.point.z + } + local _alt = land.getHeight(_pos2) + local _pos3 = { + x = _pos2.x, + y = _alt, + z = _pos2.y + } + ---@diagnostic disable-next-line: deprecated + local _angle = math.atan2(_pos3.z, _pos3.x) + + local _units = {} + for _unitType, _unitCount in pairs(_groupData.units) do + for i = 1, _unitCount do + local _xOffset = math.cos(_angle) * math.random(_groupData.scatter.min, _groupData.scatter.max) + local _yOffset = math.sin(_angle) * math.random(_groupData.scatter.min, _groupData.scatter.max) + + table.insert(_units, { + type = _unitType, + name = string.format('%s: %s: %s %i', _name, _groupName, _unitType, i), + skill = 'Excellent', + playerCanDrive = false, + x = _pos3.x + _xOffset, + y = _pos3.z + _yOffset, + heading = _angle + }) + end + end + + local _groupTask = _groupData.task + if _groupTask == nil then + if _groupData.category == Group.Category.AIRPLANE then + _groupTask = 'CAS' + elseif _groupData.category == Group.Category.GROUND then + _groupTask = 'Ground Nothing' + elseif _groupData.category == Group.Category.HELICOPTER then + _groupTask = 'Transport' + elseif _groupData.category == Group.Category.SHIP then + _groupTask = 'Ground Nothing' -- Maybe? + elseif _groupData.category == Group.Category.TRAIN then + _groupTask = 'Ground Nothing' + else + _groupTask = '' + end + end + + local _group = mist.dynAdd({ + visible = true, + hidden = false, + uncontrolled = false, + uncontrollable = false, + units = _units, + name = string.format('%s: %s', _name, _groupName), + task = _groupTask, + route = _groupData.route or {}, + category = _groupData.category, + country = _groupData.country, + x = _pos3.x, + y = _pos3.z, + }) + + -- Apparently, ships in particular don't like having their AI messed with. + -- We'll leave them be just following their routes. + if _group ~= nil and _groupData.category ~= Group.Category.SHIP and type(_groupData.orders) == 'table' and #_groupData.orders > 0 then + local _groupObj = Group.getByName(_group.name) + + if _groupObj ~= nil then + trigger.action.activateGroup(_groupObj) + + timer.scheduleFunction(function() + local _controller = _groupObj:getController() + if _controller ~= nil then + Gremlin.log.trace(Waves.Id, string.format('Activating Group AI : %s', _group.name)) + + _controller:setOnOff(true) + _controller:setTask({ + id = 'ComboTask', + params = { + tasks = _groupData.orders, + }, + }) + end + end, nil, timer.getTime() + 1) + end + end + end + + Gremlin.log.trace(Waves.Id, string.format('Finished Spawning Wave : %s', _name)) +end + +Waves._internal.getAdminUnits = function() + Gremlin.log.trace(Waves.Id, string.format('Scanning For Connected Admins')) + + local _units = {} + + for _name, _ in pairs(mist.DBs.unitsByName) do + local _unit = Unit.getByName(_name) + + if _unit ~= nil and _unit.isExist ~= nil and _unit:isExist() and _unit.getPlayerName ~= nil then + local _pilot = _unit:getPlayerName() + if _pilot ~= nil and _pilot ~= '' then + for _, _adminName in pairs(Waves.config.adminPilotNames) do + if _adminName == _pilot then + _units[_name] = _unit + break + end + end + end + end + end + + Gremlin.log.trace(Waves.Id, string.format('Scan Complete : Found %i Active Admin Units', Gremlin.utils.countTableEntries(_units))) + + return _units +end + +Waves._internal.initMenu = function() + Gremlin.log.trace(Waves.Id, string.format('Building Menu')) + + for _name, _wave in pairs(Waves.config.waves) do + if _wave.trigger.type == 'menu' then + table.insert(Waves._internal.menu, { + text = _wave.trigger.value or ('Send In Reinforcements : ' .. _name), + func = Waves._internal.menuWave, + args = { _name }, + when = { + func = function(_name) + return not Waves._state.paused and not Waves.config.waves[_name].trigger.fired + end, + args = { _name }, + comp = 'equal', + value = true, + } + }) + end + end + + Gremlin.log.trace(Waves.Id, string.format('Menu Ready')) +end + +Waves._internal.updateF10 = function() + Gremlin.log.trace(Waves.Id, string.format('Updating Menu')) + + timer.scheduleFunction(Waves._internal.updateF10, nil, timer.getTime() + 5) + + Gremlin.menu.updateF10(Waves.Id, Waves._internal.menu, Waves._internal.getAdminUnits()) +end + +Waves._internal.menuWave = function(_name) + if not Waves._state.paused and not Waves.config.waves[_name].trigger.fired then + Gremlin.log.trace(Waves.Id, string.format('Caling In Reinforcements : %s', _name)) + + Waves.config.waves[_name].trigger.fired = true + Waves._internal.spawnWave(_name, Waves.config.waves[_name]) + + Gremlin.log.trace(Waves.Id, string.format('Reinforcements En Route : %s', _name)) + end +end + +Waves._internal.timeWave = function() + timer.scheduleFunction(Waves._internal.timeWave, nil, timer.getTime() + 1) + + if not Waves._state.paused then + Gremlin.log.trace(Waves.Id, string.format('Checking On Next Wave')) + + for _name, _wave in pairs(Waves.config.waves) do + if (_wave.trigger.type == 'time' and not _wave.trigger.fired and _wave.trigger.value <= timer.getTime()) + or (_wave.trigger.type == 'flag' and not _wave.trigger.fired and trigger.misc.getUserFlag(_wave.trigger.value) ~= 0) + then + Waves.config.waves[_name].trigger.fired = true + Waves._internal.spawnWave(_name, _wave) + end + end + end +end + +Waves._internal.pause = function() + Gremlin.log.trace(Waves.Id, string.format('Pausing Reinforcement Waves')) + + Waves._state.paused = true + Gremlin.menu.updateF10(Waves.Id, Waves._internal.menu, Waves._internal.getAdminUnits()) +end + +Waves._internal.unpause = function() + Gremlin.log.trace(Waves.Id, string.format('Releasing Pending Reinforcement Waves')) + + Waves._state.paused = false + Gremlin.menu.updateF10(Waves.Id, Waves._internal.menu, Waves._internal.getAdminUnits()) +end + +Waves._internal.menu = { + { + text = 'Pause Waves', + func = Waves._internal.pause, + args = {}, + when = { + func = function() return Waves._state.paused end, + args = {}, + comp = 'inequal', + value = true + }, + }, + { + text = 'Resume Waves', + func = Waves._internal.unpause, + args = {}, + when = { + func = function() return Waves._state.paused end, + args = {}, + comp = 'equal', + value = true + }, + }, +} + +Waves._internal.handlers = { + eventTriggers = { + event = -1, + fn = function(_event) + if not Waves._state.paused then + Gremlin.log.trace(Waves.Id, + string.format('Checking Event Against Waves : %s', Gremlin.events.idToName[_event.id])) + + for _name, _wave in pairs(Waves.config.waves) do + if _wave.trigger.type == 'event' + and ( + _wave.trigger.value.id == _event.id + or _wave.trigger.value.id == -1 + ) + and _wave.trigger.value.filter(_event) + then + Waves.config.waves[_name].trigger.fired = true + Waves._internal.spawnWave(_name, _wave) + end + end + end + end + }, +} + +function Waves:setup(config) + if config == nil then + config = {} + end + + assert(Gremlin ~= nil, + '\n\n** HEY MISSION-DESIGNER! **\n\nGremlin Script Tools has not been loaded!\n\nMake sure Gremlin Script Tools is loaded *before* running this script!\n') + + if not Gremlin.alreadyInitialized or config.forceReload then + Gremlin:setup(config) + end + + if Waves._state.alreadyInitialized and not config.forceReload then + Gremlin.log.info(Waves.Id, string.format('Bypassing initialization because Waves._state.alreadyInitialized = true')) + return + end + + Gremlin.log.info(Waves.Id, string.format('Starting setup of %s version %s!', Waves.Id, Waves.Version)) + + -- start configuration + if not Waves._state.alreadyInitialized or config.forceReload then + Waves.config.adminPilotNames = config.adminPilotNames or {} + Waves.config.waves = config.waves or {} + + Gremlin.log.debug(Waves.Id, string.format('Configuration Loaded : %s', mist.utils.tableShowSorted(Waves.config))) + end + -- end configuration + + Waves._internal.initMenu() + + timer.scheduleFunction(function() + timer.scheduleFunction(Waves._internal.timeWave, nil, timer.getTime() + 1) + timer.scheduleFunction(Waves._internal.updateF10, nil, timer.getTime() + 1) + end, nil, timer.getTime() + 1) + + for _name, _def in pairs(Waves._internal.handlers) do + Waves._internal.handlers[_name].id = Gremlin.events.on(_def.event, _def.fn) + + Gremlin.log.debug(Waves.Id, string.format('Registered %s event handler', _name)) + end + + Gremlin.log.info(Waves.Id, string.format('Finished setting up %s version %s!', Waves.Id, Waves.Version)) + + Waves._state.alreadyInitialized = true +end diff --git a/test/run.lua b/test/run.lua index 68eca3b..ee98b03 100644 --- a/test/run.lua +++ b/test/run.lua @@ -27,6 +27,10 @@ if testName == "urgency" or testName == "all" then dofile(PATH .. "urgency.lua") testsLoaded = true end +if testName == "waves" or testName == "all" then + dofile(PATH .. "waves.lua") + testsLoaded = true +end if testsLoaded then os.exit(lu.LuaUnit.run()) diff --git a/test/waves.lua b/test/waves.lua new file mode 100644 index 0000000..3e7de46 --- /dev/null +++ b/test/waves.lua @@ -0,0 +1,417 @@ +local lu = require('luaunit_3_4') +local inspect = require('inspect') +local Mock = require('lib.mock.Mock') +local Spy = require('lib.mock.Spy') +local ValueMatcher = require('lib.mock.ValueMatcher') + +table.unpack = table.unpack or unpack +unpack = table.unpack + +require('mocks.DCS') +require('mist_4_5_122') +require('gremlin') +require('waves') + +mist.scheduleFunction = Spy(mist.scheduleFunction) +Gremlin.log.error = Spy(Gremlin.log.error) +Gremlin.log.warn = Spy(Gremlin.log.warn) +Gremlin.log.info = Spy(Gremlin.log.info) +Gremlin.log.debug = Spy(Gremlin.log.debug) +Gremlin.log.trace = Spy(Gremlin.log.trace) + +local _testZone = 'TestZone' +local _testZoneData = { + name = _testZone, + point = { x = 0, y = 0, z = 0 }, + properties = {}, + verticies = { + { x = 100, y = 0, z = 100 }, + { x = -100, y = 0, z = 100 }, + { x = -100, y = 0, z = -100 }, + { x = 100, y = 0, z = -100 }, + }, + x = 0, + y = 0, + z = 0, +} + +local _testUnit = { className_ = 'Unit', groupName = 'Gremlin Troop 1', type = 'UH-1H', unitName = 'TestUnit1', unitId = 1, point = { x = 0, y = 0, z = 0 } } +---@diagnostic disable-next-line: undefined-global +class(_testUnit, Unit) + +local _testUnit2 = { className_ = 'Unit', groupName = 'Gremlin Troop 1', type = 'UH-1H', unitName = 'TestUnit2', unitId = 2, point = { x = 0, y = 0, z = 0 } } +---@diagnostic disable-next-line: undefined-global +class(_testUnit2, Unit) + +local _testUnit3 = { className_ = 'Unit', groupName = 'Gremlin Troop 2', type = 'Ejected Pilot', unitName = 'TestUnit3', unitId = 3, point = { x = 0, y = 0, z = 0 } } +---@diagnostic disable-next-line: undefined-global +class(_testUnit3, Unit) + +local _testGroup = { className_ = 'Group', groupName = 'Gremlin Troop 1', groupId = 1, units = { _testUnit, _testUnit2 } } +---@diagnostic disable-next-line: undefined-global +class(_testGroup, Group) + +local _testGroup2 = { className_ = 'Group', groupName = 'Gremlin Troop 2', groupId = 2, units = { _testUnit3 } } +---@diagnostic disable-next-line: undefined-global +class(_testGroup2, Group) + +local _testWaveTimedName = 'Test Timed Wave' +local _testWaveTimed = { + trigger = { + type = 'time', + value = 0, + }, + groups = { + ['F-14B'] = { + category = Group.Category.AIRPLANE, + country = country.USA, + zone = _testZone, + scatter = { min = 15, max = 30 }, + orders = {}, + units = { + ['F-14B'] = 3, + }, + }, + ['Ground A'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = _testZone, + scatter = { min = 5, max = 10 }, + orders = {}, + units = { + ['Infantry'] = 4, + } + }, + ['Ground B'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = _testZone, + scatter = { min = 5, max = 10 }, + orders = {}, + units = { + ['RPG'] = 1, + ['Infantry'] = 3, + ['JTAC'] = 1, + } + }, + }, +} +local _testWaveTimedGroupNames = {} +for _name, _ in pairs(_testWaveTimed.groups) do + table.insert(_testWaveTimedGroupNames, _name) +end + +local _testWaveMenuName = 'Test Menu Wave' +local _testWaveMenu = { + trigger = { + type = 'menu', + value = 'Spawn The Spanish Inquisition', + }, + groups = { + ['F-14B'] = { + category = Group.Category.AIRPLANE, + country = country.USA, + zone = _testZone, + scatter = { min = 15, max = 30 }, + orders = {}, + units = { + ['F-14B'] = 3, + }, + }, + ['Ground A'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = _testZone, + scatter = { min = 5, max = 10 }, + orders = {}, + units = { + ['Infantry'] = 4, + } + }, + ['Ground B'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = _testZone, + scatter = { min = 5, max = 10 }, + orders = {}, + units = { + ['RPG'] = 1, + ['Infantry'] = 3, + ['JTAC'] = 1, + } + }, + }, +} +local _testWaveMenuGroupNames = {} +for _name, _ in pairs(_testWaveMenu.groups) do + table.insert(_testWaveMenuGroupNames, _name) +end + +local _testWaveEventName = 'Test Event Wave' +local _testWaveEvent = { + trigger = { + type = 'event', + value = { + id = world.event.S_EVENT_AI_ABORT_MISSION, + filter = function(_event) + if _event.initiator:getName() == _testUnit.unitName then + return true + end + + return false + end, + }, + }, + groups = { + ['F-14B'] = { + category = Group.Category.AIRPLANE, + country = country.USA, + zone = _testZone, + scatter = { min = 15, max = 30 }, + orders = {}, + units = { + ['F-14B'] = 3, + }, + }, + ['Ground A'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = _testZone, + scatter = { min = 5, max = 10 }, + orders = {}, + units = { + ['Infantry'] = 4, + } + }, + ['Ground B'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = _testZone, + scatter = { min = 5, max = 10 }, + orders = {}, + units = { + ['RPG'] = 1, + ['Infantry'] = 3, + ['JTAC'] = 1, + } + }, + }, +} +local _testWaveEventGroupNames = {} +for _name, _ in pairs(_testWaveEvent.groups) do + table.insert(_testWaveEventGroupNames, _name) +end + +local setUp = function() + -- MiST DBs + mist.DBs.groupsByName[_testGroup.groupName] = _testGroup + mist.DBs.groupsByName[_testGroup2.groupName] = _testGroup2 + mist.DBs.MEgroupsByName = mist.DBs.groupsByName + mist.DBs.unitsByName[_testUnit.unitName] = _testUnit + mist.DBs.unitsByName[_testUnit2.unitName] = _testUnit2 + mist.DBs.unitsByName[_testUnit3.unitName] = _testUnit3 + mist.DBs.MEunitsByName = mist.DBs.unitsByName + mist.DBs.units = { + Blue = { + USA = { + helicopter = { + [_testUnit:getGroup():getID()] = { + units = { _testUnit, _testUnit2 } + } + } + } + } + } + mist.DBs.zonesByName = { + [_testZone] = _testZoneData + } + + Waves.config.waves = { + [_testWaveTimedName] = mist.utils.deepCopy(_testWaveTimed), + [_testWaveMenuName] = mist.utils.deepCopy(_testWaveMenu), + [_testWaveEventName] = mist.utils.deepCopy(_testWaveEvent), + } +end + +local tearDown = function() + Gremlin.alreadyInitialized = false + Waves._internal.menu = { Waves._internal.menu[1], Waves._internal.menu[2] } + Waves._state.alreadyInitialized = false + Waves._state.paused = false + Waves.config.adminPilotNames = {} + Waves.config.waves = {} + + mist.nextUnitId = 1 + mist.nextGroupId = 1 + + mist.DBs.zonesByName = {} + mist.DBs.units = {} + mist.DBs.unitsByName[_testUnit.unitName] = nil + mist.DBs.unitsByName[_testUnit2.unitName] = nil + mist.DBs.unitsByName[_testUnit3.unitName] = nil + mist.DBs.MEunitsByName = mist.DBs.unitsByName + mist.DBs.groupsByName[_testGroup.groupName] = nil + mist.DBs.MEgroupsByName = mist.DBs.groupsByName + + mist.scheduleFunction:reset() + Gremlin.log.error:reset() + Gremlin.log.warn:reset() + Gremlin.log.info:reset() + Gremlin.log.debug:reset() + Gremlin.log.trace:reset() + timer.scheduleFunction:reset() + trigger.action.activateGroup:reset() + trigger.action.setUnitInternalCargo:reset() + trigger.action.setUserFlag:reset() + trigger.action.outText:reset() + trigger.action.outTextForCoalition:reset() + trigger.action.outTextForCountry:reset() + trigger.action.outTextForGroup:reset() + trigger.action.outTextForUnit:reset() +end + +local function stripFuncs(_tbl) + for _idx, _val in pairs(_tbl) do + if type(_val) == 'table' then + _tbl[_idx] = stripFuncs(_tbl[_idx]) + elseif type(_val) == 'function' then + _tbl[_idx] = nil + end + end + + return _tbl +end + +local function matcherForNameId(_tbl) + return { + isMatcher = true, + match = function(value) + if value.name == _tbl.name and value.id == _tbl.id then + return true + end + + return false, string.format('did not match:\n was: %s\nexpected: %s', inspect(value), inspect(_tbl)) + end + } +end + +TestWavesInternalHandlers = { + setUp = setUp, + testEventTriggers = function() + -- INIT + -- N/A? + + -- TEST + lu.assertEquals(Waves._internal.handlers.eventTriggers.fn({ id = world.event.S_EVENT_INVALID }), nil) + + -- SIDE EFFECTS + -- N/A? + end, + tearDown = tearDown, +} + +TestWavesInternalMethods = { + setUp = setUp, + testSpawnWave = function() + -- INIT + -- N/A? + + -- TEST + lu.assertEquals(Waves._internal.spawnWave(_testWaveTimedName, _testWaveTimed), nil) + + -- SIDE EFFECTS + -- N/A? + end, + testGetAdminUnits = function() + -- INIT + Waves.config.adminPilotNames = { 'Al Gore' } + + -- TEST + lu.assertEquals(Waves._internal.getAdminUnits(), { [_testUnit.unitName] = _testUnit, [_testUnit2.unitName] = _testUnit2, [_testUnit3.unitName] = _testUnit3 }) + + -- SIDE EFFECTS + -- N/A? + end, + testInitMenu = function() + -- INIT + -- N/A? + + -- TEST + lu.assertEquals(Waves._internal.initMenu(), nil) + + -- SIDE EFFECTS + lu.assertEquals(Gremlin.utils.countTableEntries(Waves._internal.menu), 3) + lu.assertEquals(stripFuncs(Waves._internal.menu[3]), { + text = _testWaveMenu.trigger.value, + args = { _testWaveMenuName }, + when = { + args = { _testWaveMenuName }, + comp = 'equal', + value = true, + }, + }) + end, + testUpdateF10 = function() + -- INIT + -- N/A? + + -- TEST + lu.assertEquals(Waves._internal.updateF10(), nil) + + -- SIDE EFFECTS + local _status, _result = pcall( + missionCommands.addCommandForGroup.assertAnyCallMatches, + missionCommands.addCommandForGroup, + { _testGroup.groupId, 'Pause Waves', { Waves.Id }, ValueMatcher.anyFunction, nil } + ) + lu.assertEquals(_status, true, string.format('%s\n%s', inspect(_result), inspect(missionCommands.addCommandForGroup.spy.calls))) + + _status, _result = pcall( + missionCommands.addCommandForGroup.assertAnyCallMatches, + missionCommands.addCommandForGroup, + { _testGroup.groupId, 'Resume Waves', { Waves.Id }, ValueMatcher.anyFunction, nil } + ) + lu.assertEquals(_status, true, string.format('%s\n%s', inspect(_result), inspect(missionCommands.addCommandForGroup.spy.calls))) + end, + testMenuWave = function() + -- INIT + -- N/A? + + -- TEST + lu.assertEquals(Waves._internal.menuWave(_testWaveMenuName), nil) + + -- SIDE EFFECTS + -- N/A? + end, + testTimeWave = function() + -- INIT + lu.assertEquals(Waves.config.waves[_testWaveTimedName].trigger.fired, nil) + + -- TEST + lu.assertEquals(Waves._internal.timeWave(), nil) + + -- SIDE EFFECTS + lu.assertEquals(Waves.config.waves[_testWaveTimedName].trigger.fired, true) + end, + testPause = function() + -- INIT + lu.assertEquals(Waves._state.paused, false) + + -- TEST + lu.assertEquals(Waves._internal.pause(), nil) + + -- SIDE EFFECTS + lu.assertEquals(Waves._state.paused, true) + end, + testUnpause = function() + -- INIT + Waves._state.paused = true + + -- TEST + lu.assertEquals(Waves._internal.unpause(), nil) + + -- SIDE EFFECTS + lu.assertEquals(Waves._state.paused, false) + end, + tearDown = tearDown, +}