Skip to content

Commit

Permalink
Add Gremlin Waves!
Browse files Browse the repository at this point in the history
  • Loading branch information
danhunsaker committed Mar 28, 2024
1 parent a34d1e9 commit 0211050
Show file tree
Hide file tree
Showing 7 changed files with 800 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
4 changes: 4 additions & 0 deletions docs/waves.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!-- markdownlint-disable MD041 -->
## 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!
77 changes: 77 additions & 0 deletions docs/waves/setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!-- markdownlint-disable MD041 -->
### 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
4 changes: 4 additions & 0 deletions docs/waves/usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!-- markdownlint-disable MD041 -->
### Usage

Gremlin Waves is really simple to use. Configure it once (see [the Setup page](./setup.md)), and it handles the rest!
279 changes: 279 additions & 0 deletions src/waves.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
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
trigger.action.activateGroup(_group.name)
mist.teleportInZone(_group.name, _groupData.zone, true, _groupData.scatter)

local _controller = Group.getByName(_group.name):getController()
if _controller ~= nil then
for _, _task in ipairs(_groupData.orders) do
_controller.pushTask(_task)
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
4 changes: 4 additions & 0 deletions test/run.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Loading

0 comments on commit 0211050

Please sign in to comment.