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..7b00d1a 100644 --- a/docs/evac/setup.md +++ b/docs/evac/setup.md @@ -16,7 +16,7 @@ Evac:setup({ lossThresholds = { 25, 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, @@ -65,7 +65,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` diff --git a/docs/evac/usage.md b/docs/evac/usage.md index a370d3e..bb3106c 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, 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..04296b1 --- /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`, `event`, or `menu` + - `value` a time, 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/waves.lua b/src/waves.lua new file mode 100644 index 0000000..bc54c34 --- /dev/null +++ b/src/waves.lua @@ -0,0 +1,283 @@ +Waves = { + Id = 'Gremlin Waves', + Version = '202403.01', + + config = { + adminPilotNames = {}, + waves = {} + }, + + _state = { + alreadyInitialized = false, + paused = false, + }, + _internal = {}, +} + +Waves._internal.menu = { + { + text = 'Pause Waves', + func = Waves._internal.pause, + args = {}, + when = { + func = function() return Waves._state.paused end, + args = {}, + comp = 'equal', + value = true + }, + }, + { + text = 'Resume Waves', + func = Waves._internal.unpause, + args = {}, + when = { + func = function() return Waves._state.paused end, + args = {}, + comp = 'inequal', + 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 Countdowns : %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 + }, +} + +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) + local _yOffset = math.sin(_angle) * math.random(_groupData.scatter) + + table.insert(_units, { + type = _unitType, + name = string.format('%s: %s: %s %i', _name, _groupName, _unitType, i), + skill = 'Excellent', + playerCanDrive = false, + point = { + x = _pos3.x + _xOffset, + y = _pos3.z + _yOffset, + }, + heading = _angle + }) + end + end + + local _group = mist.dynAdd({ + visible = true, + hidden = false, + units = _units, + name = string.format('%s: %s', _name, _groupName), + category = _groupData.category, + country = _groupData.country, + x = _pos3.x, + y = _pos3.z, + }) + + if _group ~= nil then + local _groupObj = Group.getByName(_group.name) + + if _groupObj ~= nil then + trigger.action.activateGroup(_groupObj) + mist.teleportInZone(_group.name, _groupData.zone, true, _groupData.scatter) + + local _controller = _groupObj:getController() + if _controller ~= nil then + for _, _task in ipairs(_groupData.orders) do + _controller.pushTask(_task) + end + end + 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 + table.insert(_units, _name) + 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() 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 + +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..e68c4c3 --- /dev/null +++ b/test/waves.lua @@ -0,0 +1,485 @@ +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 = 15, + orders = {}, + units = { + ['F-14B'] = 3, + }, + }, + ['Ground A'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = _testZone, + scatter = 5, + orders = {}, + units = { + ['Infantry'] = 4, + } + }, + ['Ground B'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = _testZone, + scatter = 5, + 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 = 15, + orders = {}, + units = { + ['F-14B'] = 3, + }, + }, + ['Ground A'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = _testZone, + scatter = 5, + orders = {}, + units = { + ['Infantry'] = 4, + } + }, + ['Ground B'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = _testZone, + scatter = 5, + 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 = 15, + orders = {}, + units = { + ['F-14B'] = 3, + }, + }, + ['Ground A'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = _testZone, + scatter = 5, + orders = {}, + units = { + ['Infantry'] = 4, + } + }, + ['Ground B'] = { + category = Group.Category.GROUND, + country = country.USA, + zone = _testZone, + scatter = 5, + 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 + +TestInternalHandlers = { + 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, +} + +TestInternalMethods = { + setUp = setUp, + testSpawnWave = function() + -- INIT + trigger.action.activateGroup:whenCalled({ with = { ValueMatcher.anyTable }, thenReturn = nil }) + trigger.action.activateGroup:whenCalled({ with = { ValueMatcher.anyTable }, thenReturn = nil }) + trigger.action.activateGroup:whenCalled({ with = { ValueMatcher.anyTable }, thenReturn = nil }) + + -- TEST + lu.assertEquals(Waves._internal.spawnWave(_testWaveTimedName, _testWaveTimed), nil) + + -- SIDE EFFECTS + local _status, _result = pcall( + trigger.action.activateGroup.assertAnyCallMatches, + trigger.action.activateGroup, + { arguments = { matcherForNameId(Group.getByName(string.format('%s: %s', _testWaveTimedName, _testWaveTimedGroupNames[1]))) } } + ) + lu.assertEquals(_status, true, string.format('%s\n%s', inspect(_result), inspect(trigger.action.activateGroup.spy.calls))) + + local _status, _result = pcall( + trigger.action.activateGroup.assertAnyCallMatches, + trigger.action.activateGroup, + { arguments = { matcherForNameId(Group.getByName(string.format('%s: %s', _testWaveTimedName, _testWaveTimedGroupNames[2]))) } } + ) + lu.assertEquals(_status, true, string.format('%s\n%s', inspect(_result), inspect(trigger.action.activateGroup.spy.calls))) + + local _status, _result = pcall( + trigger.action.activateGroup.assertAnyCallMatches, + trigger.action.activateGroup, + { arguments = { matcherForNameId(Group.getByName(string.format('%s: %s', _testWaveTimedName, _testWaveTimedGroupNames[3]))) } } + ) + lu.assertEquals(_status, true, string.format('%s\n%s', inspect(_result), inspect(trigger.action.activateGroup.spy.calls))) + end, + testGetAdminUnits = function() + -- INIT + Waves.config.adminPilotNames = { 'Al Gore' } + + -- TEST + lu.assertEquals(Waves._internal.getAdminUnits(), { _testUnit.unitName, _testUnit3.unitName, _testUnit2.unitName }) + + -- 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 + trigger.action.activateGroup:whenCalled({ with = { ValueMatcher.anyTable }, thenReturn = nil }) + trigger.action.activateGroup:whenCalled({ with = { ValueMatcher.anyTable }, thenReturn = nil }) + trigger.action.activateGroup:whenCalled({ with = { ValueMatcher.anyTable }, thenReturn = nil }) + + -- TEST + lu.assertEquals(Waves._internal.menuWave(_testWaveMenuName), nil) + + -- SIDE EFFECTS + local _status, _result = pcall( + trigger.action.activateGroup.assertAnyCallMatches, + trigger.action.activateGroup, + { arguments = { matcherForNameId(Group.getByName(string.format('%s: %s', _testWaveMenuName, _testWaveMenuGroupNames[1]))) } } + ) + lu.assertEquals(_status, true, + string.format('%s\n%s', inspect(_result), inspect(trigger.action.activateGroup.spy.calls))) + + local _status, _result = pcall( + trigger.action.activateGroup.assertAnyCallMatches, + trigger.action.activateGroup, + { arguments = { matcherForNameId(Group.getByName(string.format('%s: %s', _testWaveMenuName, _testWaveMenuGroupNames[2]))) } } + ) + lu.assertEquals(_status, true, + string.format('%s\n%s', inspect(_result), inspect(trigger.action.activateGroup.spy.calls))) + + local _status, _result = pcall( + trigger.action.activateGroup.assertAnyCallMatches, + trigger.action.activateGroup, + { arguments = { matcherForNameId(Group.getByName(string.format('%s: %s', _testWaveMenuName, _testWaveMenuGroupNames[3]))) } } + ) + lu.assertEquals(_status, true, + string.format('%s\n%s', inspect(_result), inspect(trigger.action.activateGroup.spy.calls))) + end, + testTimeWave = function() + -- INIT + trigger.action.activateGroup:whenCalled({ with = { ValueMatcher.anyTable }, thenReturn = nil }) + trigger.action.activateGroup:whenCalled({ with = { ValueMatcher.anyTable }, thenReturn = nil }) + trigger.action.activateGroup:whenCalled({ with = { ValueMatcher.anyTable }, thenReturn = nil }) + 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) + local _status, _result = pcall( + trigger.action.activateGroup.assertAnyCallMatches, + trigger.action.activateGroup, + { arguments = { matcherForNameId(Group.getByName(string.format('%s: %s', _testWaveTimedName, _testWaveTimedGroupNames[1]))) } } + ) + lu.assertEquals(_status, true, string.format('%s\n%s', inspect(_result), inspect(trigger.action.activateGroup.spy.calls))) + + local _status, _result = pcall( + trigger.action.activateGroup.assertAnyCallMatches, + trigger.action.activateGroup, + { arguments = { matcherForNameId(Group.getByName(string.format('%s: %s', _testWaveTimedName, _testWaveTimedGroupNames[2]))) } } + ) + lu.assertEquals(_status, true, string.format('%s\n%s', inspect(_result), inspect(trigger.action.activateGroup.spy.calls))) + + local _status, _result = pcall( + trigger.action.activateGroup.assertAnyCallMatches, + trigger.action.activateGroup, + { arguments = { matcherForNameId(Group.getByName(string.format('%s: %s', _testWaveTimedName, _testWaveTimedGroupNames[3]))) } } + ) + lu.assertEquals(_status, true, string.format('%s\n%s', inspect(_result), inspect(trigger.action.activateGroup.spy.calls))) + 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, +}