Manual

Overview

This page shows how to make new missions in Powerplay using LUA. The whole campaign and the skirmish behavior is made using this same system that is available to you as a player.

Getting Started

Every script is a plain .lua text file. The engine injects different global objects and calls different lifecycle functions on it — see Structure below for the full breakdown.

To attach a script to a mission, in the map editor:

1 The Map Script panel in the map editor, with Browse File and Create New File options

Click the script icon in the map editor's toolbar, then Create New File (or Browse File to link an existing .lua file).

2 The Map Script panel showing a linked script file, map_script.lua

The panel now shows the linked file. Edit it in your favorite IDE. The engine picks up the changes when you press play in the editor. Press save to persist the changes into the map.

A minimal script that adds one objective:

function start()
    game:show_map_pointer({ x = 40, y = 65 }, "BUILD A MINE HERE")

    game:add_objective({
        title = "BUILD A MINE",
        condition = function()
            return count(game:query_buildings({ type = "mine", player = 1 })) > 0
        end,
        on_completed = function()
            game:hide_pointer()
            game:set_mission_complete()
        end,
    })
end

Structure

There is one top-level mission script per map. Drives objectives, dialog, scripted events, and overall mission flow.

Flow 2
start()

Runs once at the start. Typical place to define mission objectives.

update()

Runs once per second. You should avoid it and try to use the objective system for most things.

State 2
game:set_value(key, value)

Stores a string, number, boolean, or table value under key, persisted for the duration of the mission and across save games.

  • key string
  • value string | number | boolean | table
game:get_value(key, default_value)

Reads a value previously stored with set_value.

  • key string
  • default_value any — optional, returned when key has never been set

value or default_value

Objective System

The Objective System is the intended way to write missions: a declarative system, continuously run by the engine every second, instead of procedural code and loops. Every field in an objective is optional except condition, which represents completion. Once an objective is completed, it is removed. An objective with no title is never shown in the UI, which is a common trick for using the objective system purely to sequence dialog or watch a fail-condition in the background (see mission1.lua for several examples of this).

Objectives are typically added from the script's top-level start() function.

function start()
    game:add_objective({
        -- Shown in the objectives UI. Omit title to make a "silent" objective —
        -- useful for gating dialog or watching a fail-condition without cluttering the UI.
        title = "BUILD MINE",

        -- Only becomes active once this returns true (checked every tick until it does).
        -- Omit to activate immediately.
        prerequisite = function() return game:get_value("intro_completed") end,

        -- Runs once, the tick the objective becomes active.
        on_activated = function()
            game:show_map_pointer(mine_pos, "BUILD MINE")
        end,

        -- Checked every tick once active. Objective completes the first time this is true.
        condition = function()
            return count(game:query_buildings({ type = "mine", player = 1 })) > 0
        end,

        -- Optional: live status text shown next to the objective, e.g. "3/5".
        status = function() return tostring(some_progress) .. "/5" end,

        -- Runs once, the tick condition() first returns true.
        on_completed = function()
            game:hide_pointer()
        end,

        -- Optional alternate failure path, checked alongside condition().
        failure_condition = function() return player_has_no_base() end,
        on_failed = function() game:set_mission_failed() end,

        -- Optional: played automatically when the objective activates.
        dialog = {
            { character = "faction01_lieutenant", message = "Let's get that mine built." },
        },
    })
end

Game API

This is the API available through the game keyword. It covers spawning, querying units/buildings, objectives, dialog, UI, camera control, and mission state.

Every function here runs synchronously, except game:delayed_call, which schedules a function to run later without blocking.

Units 5
game:spawn_unit(props)

Spawns a unit and returns it.

  • props table
    {
      type,
      pos = { x, y },
      player = 1,  -- optional, defaults to the script's player
      tag,  -- optional string
    }

    type is a Unit Types

unit

local trooper = game:spawn_unit({ type = "trooper", pos = { x = 40, y = 65 }, player = 2, tag = "wave_1" })
game:get_unit(unit_id)

Looks up a unit by id.

  • unit_id string

unit or nil

game:query_units(filter)

Returns every unit matching filter.

  • filter table
    {
      player = 1,  -- optional, defaults to script's player
      global = true,  -- optional, search all players
      tag = nil,  -- optional
      type = 'trooper',  -- optional
      is_selected = false,  -- optional
      is_moving = false,  -- optional
    }

    type is a Unit Types

array of units

local troopers = game:query_units({ type = "trooper", player = 1 })
print(count(troopers) .. " troopers")
game:move_units(units, coords)

Moves a group of units to coords, automatically spacing them out so they don't stack.

  • units array of units
  • coords {x, y}
game:move_units_along_path(units, path, move_type)

Moves a group of units along a sequence of waypoints, keeping formation spacing.

  • units array of units
  • path array of {x, y}
  • move_type string — "move" (default) or "attack_move"
Buildings 3
game:spawn_building(props)

Spawns a building and returns it.

  • props table
    {
      type,
      pos = { x, y },
      player = 1,  -- optional
      tag,  -- optional string
    }

    type is a Building Types

building

game:get_building(building_id)

Looks up a building by id.

  • building_id string

building or nil

game:query_buildings(filter)

Returns every building matching filter.

  • filter table
    {
      player = 1,  -- optional
      global = false,
      tag = nil,
      type = nil,
      output = nil,  -- filter by current production output
    }

    type is a Building Types

array of buildings

World & Map 11
game:get_time()

Seconds elapsed in the current mission.

number

game:get_real_time()

Seconds elapsed in real time (not affected by game speed).

number

game:get_cell_type(coords)

Type of whatever occupies a map cell (a building or resource type string), or nil if empty.

  • coords {x, y}

string or nil

game:world_to_map(world_pos)

Converts a 3D world position into map cell coordinates.

  • world_pos {x, y, z}

{x, y}

game:map_to_world(map_coords)

Converts map cell coordinates into a 3D world position.

  • map_coords {x, y}

{x, y, z}

game:get_sector_width()

Width (and height) of a sector, in cells. The map is divided into a grid of sectors for spatial queries like scan_map.

number

game:get_sector_coords(map_coords)

Sector coordinates containing the given map cell.

  • map_coords {x, y}

{x, y}

game:get_map_resolution()

Number of sectors per dimension (e.g. 8 for an 8x8 grid of sectors).

number

game:is_sector_valid(sector_coords)

Whether a sector is within the map bounds.

  • sector_coords {x, y}

boolean

game:scan_map(origin, filter)

Spirals outward sector by sector from origin, calling filter(sector_coords) until it returns true. Returns the matching sector, or nil if none match.

  • origin {x, y} — sector coords to start from
  • filter function(sector_coords) -> boolean

{x, y} or nil

local nearest = game:scan_map(origin, function(sector_coords)
    if not game:is_sector_valid(sector_coords) then return false end
    return not state.explored[sector_key(sector_coords)]
end)
game:is_map_explored(coords)

Whether the fog of war has been lifted at the given map cell.

  • coords {x, y}

boolean

Economy 4
game:get_costs(item_type)

The resource cost to produce a unit or building type.

array of { resource, amount }

game:add_resource(item_type, amount)

Adds resources to the current script's player inventory.

  • item_type string
  • amount number
game:get_resource(item_type)

Current amount of a resource in the current player's inventory.

  • item_type string

number

game:remove_resource(item_type, amount)

Removes resources from the current script's player inventory.

  • item_type string
  • amount number
Players & Factions 6
game:set_player_team(player, team)

Assigns a player to a team.

  • player number — 1-based player index
  • team number — 1-based team index
game:set_player_color(player, color)

Recolors a player's units and buildings, and adopts the team of any other player already using that color (so same-colored players don't fight each other).

  • player number — 1-based
  • color number — 1-based
game:set_available_units(units)

Sets the full list of unit types the player is allowed to build.

game:add_available_unit(unit_type)

Unlocks a single additional buildable unit type.

game:set_available_buildings(buildings)

Sets the full list of building types the player is allowed to build.

game:add_available_building(building_type)

Unlocks a single additional buildable building type.

Objectives 5
game:add_objective(objective)

Registers a single objective. See the Objectives section below for worked examples.

  • objective table
    {
      title = nil,  -- optional, omit for a silent objective
      prerequisite = function() end,  -- optional, checked every tick until true — omit to activate immediately
      on_activated = function() end,  -- optional, runs once when the objective activates
      condition = function() end,  -- required, objective completes the first time this returns true
      status = function() end,  -- optional, live status text shown next to the objective
      on_completed = function() end,  -- optional, runs once the tick condition first passes
      failure_condition = function() end,  -- optional, checked alongside condition for an alternate fail path
      on_failed = function() end,  -- optional, runs once the tick failure_condition first passes
      dialog = nil,  -- optional, array of dialog line tables — played automatically on activation, see Dialog lines below
    }
game:add_objectives(objectives)

Registers a list of objectives at once (calls add_objective for each).

  • objectives array of objective tables
game:are_objectives_completed()

Whether every currently active objective has been completed.

boolean

game:set_mission_complete()

Marks the campaign mission as won.

game:set_mission_failed()

Marks the campaign mission as lost.

Fog of War 1
game:fog_reveal(props)

Temporarily (or permanently) reveals an area of the map, independent of any unit's vision.

  • props table
    {
      pos = { x, y },
      radius,
      duration = 0,  -- 0 or omitted = permanent
    }
game:fog_reveal({ pos = enemy_base:get_pos(), radius = 10, duration = 5 })
Camera 5
game:set_camera_zoom(zoom, duration)

Sets the camera zoom level, optionally animated.

  • zoom number
  • duration number — seconds, default 0 (instant)
game:set_camera_pos(coords, immediate)

Moves the camera to look at a map position.

  • coords {x, y}
  • immediate boolean — skip the pan animation, default false
game:camera_follow_unit(unit_id)

Locks the camera to follow a unit. Pass nil (or an empty string) to stop following.

  • unit_id string or nil
game:enable_camera_pan(enabled)

Enables/disables player camera panning.

  • enabled boolean
game:enable_camera_zoom(enabled)

Enables/disables player camera zoom.

  • enabled boolean
UI 16
game:show_map_pointer(coords, data)

Shows an on-screen pointer/arrow toward a map location, guiding the player's attention.

  • coords {x, y}
  • data string or table — a label string, or { label, icon = (optional) }
game:show_map_pointer(iron_mine_pos, "BUILD MINE")
game:show_map_pointers(data)

Shows several map pointers at once.

  • data array of { pos = {x, y}, label, icon = (optional) }
game:hide_pointer(index)

Hides a single map/UI pointer.

  • index number — default 0
game:hide_pointers()

Hides every active pointer.

game:set_ui_highlight(ui_id, highlighted)

Highlights (or un-highlights) a UI element, e.g. a build button, to draw attention to it.

  • ui_id string
  • highlighted boolean
game:show_ui_pointer(ui_id, label, icon)

Shows a pointer aimed at a UI element instead of a map location.

  • ui_id string
  • label string
  • icon string — optional
game:set_building_panel_section(section)

Switches the build menu to a category tab, e.g. "industry" or "military".

  • section string
game:trigger_ui_click(ui_id)

Programmatically triggers a click on a UI element.

  • ui_id string
game:enable_ui_section(ui_id, enabled)

Shows/hides a UI section.

  • ui_id string
  • enabled boolean
game:enable_ui_crafting(item_type, enabled)

Enables/disables crafting UI for an item type.

  • item_type string
  • enabled boolean
game:enable_ui_output(item_type, enabled)

Enables/disables the output selector UI for an item type.

  • item_type string
  • enabled boolean
game:enable_building_panel(enabled)

Shows/hides the entire building panel.

  • enabled boolean
game:show_error_message(message)

Shows a global on-screen error/toast message.

  • message string
game:show_ghost_building(building_type, coords)

Shows a translucent preview of a building at a map location (no gameplay effect, purely visual).

game:hide_ghost_building(coords)

Hides a single ghost building preview.

  • coords {x, y}
game:hide_ghost_buildings()

Hides every ghost building preview.

Input & Selection 10
game:enable_input(enabled)

Enables/disables all player input to the map (used to lock the player during cutscenes).

  • enabled boolean
game:enable_multi_selection(enabled)

Enables/disables box/drag multi-selection.

  • enabled boolean
game:enable_deselection(allow)

Enables/disables the ability to deselect the current selection.

  • allow boolean
game:get_selection_type()

What kind of thing is currently selected.

string: "none" | "unit" | "building"

game:set_right_click_filter(filter)

Restricts what the player is allowed to right-click on. Pass nil to clear the filter.

  • filter function(target_type) -> boolean, or nil
game:set_selection_filter(filter)

Restricts what the player is allowed to select. Pass nil to clear the filter.

  • filter function(target_type) -> boolean, or nil
game:deselect()

Clears the current selection.

game:set_build_filter(filter)

Restricts where buildings can be placed. Pass nil to clear the filter.

  • filter function(coords) -> boolean, or nil
game:set_required_building_connection(building_type, connection_type)

Forces a specific connection type to be required when placing a building type (used to steer tutorial-style connection prompts).

game:clear_required_building_connection()

Clears any forced connection requirement set by set_required_building_connection.

Dialog & Events 3
game:radio_dialog(dialog)

Plays a sequence of radio dialog lines. See the Objectives & Dialog section for the dialog line schema.

  • dialog array of dialog line tables
game:hide_radio()

Hides the radio dialog UI immediately.

game:delayed_call(seconds, fn)

Calls fn() once, after a delay. Unlike wait(), this works from anywhere (not just inside start()'s coroutine).

  • seconds number
  • fn function
State Storage 2
game:set_value(key, value)

Stores a string, number, boolean, or table value under key, persisted for the duration of the mission and across save games.

  • key string
  • value string | number | boolean | table
game:get_value(key, default_value)

Reads a value previously stored with set_value.

  • key string
  • default_value any — optional, returned when key has never been set

value or default_value

Units API

Methods available on any unit object — returned by game:spawn_unit(), game:get_unit(), game:query_units().

State 17
unit:get_id()

The unit's unique id.

string

unit:get_type()

The unit's type, e.g. "trooper".

unit type

unit:is_player()

Whether this unit belongs to the human player (Player 1).

boolean

unit:is_moving()

Whether the unit currently has a move order.

boolean

unit:is_selected()

Whether the unit is currently selected.

boolean

unit:is_alive()

Whether the unit is still alive.

boolean

unit:can_attack()

Whether this unit type is capable of attacking.

boolean

unit:is_attacking()

Whether the unit is currently engaged in an attack.

boolean

unit:get_spawn_time()

game:get_time() value at the moment this unit was spawned.

number

unit:get_hit_points()

Current hit points.

number

unit:get_speed()

Current max movement speed.

number

unit:get_pos()

Current map cell coordinates.

{x, y}

unit:get_dest()

Current pathfinding destination, in map cell coordinates.

{x, y}

unit:get_extractor()

Truck only — the resource building it's currently hauling from.

building or nil

unit:get_processor()

Truck only — the building it's currently delivering to.

building or nil

unit:has_units()

Chinook only — whether it's currently carrying units.

boolean

unit:is_ejecting_units()

Chinook only — whether it's currently in the middle of unloading its carried units.

boolean

Modifiers 6
unit:set_hit_points(hp)

Sets current hit points.

  • hp number
unit:enable_collision(enabled)

Enables/disables collision for this unit.

  • enabled boolean
unit:set_speed(speed)

Overrides max movement speed.

  • speed number
unit:set_vision(enabled)

Enables/disables this unit's ongoing vision radius.

  • enabled boolean
unit:set_invincible(enabled)

Makes the unit immune to damage.

  • enabled boolean
unit:set_units(units)

Chinook only — loads a manifest of unit types to transport.

  • units array of { type }
Actions 14
unit:select()

Selects this unit (as if the player clicked it).

unit:move(coords)

Issues a move order to a map position.

  • coords {x, y}
unit:stop()

Clears the unit's current action/order.

unit:move_along_path(path)

Moves along a sequence of waypoints.

  • path array of {x, y}
unit:attack_move(coords)

Moves toward a map position, engaging any enemies encountered on the way.

  • coords {x, y}
unit:attack_move_along_path(path)

Attack-moves along a sequence of waypoints.

  • path array of {x, y}
unit:attack(target_id)

Orders the unit to attack a specific unit or building by id.

  • target_id string
unit:fog_reveal()

Reveals fog of war around this unit for the local player, once.

unit:kill()

Kills the unit (plays death effects).

unit:remove()

Silently removes the unit — no death effects, no on_killed callback noise.

unit:fill_cargo(resource_type)

Truck only — fills its cargo with a resource type.

  • resource_type string
unit:eject_units()

Chinook only — unloads/drops its carried units at its current position.

unit:unpack()

BEV only — deploys the vehicle into an Exphub building.

building or nil

unit:explore()

LRV only — starts the vehicle's built-in autonomous exploration behavior.

Buildings API

Methods available on any building object — returned by game:spawn_building(), game:get_building(), game:query_buildings().

State 22
building:get_id()

The building's unique id.

string

building:get_type()

The building's type, e.g. "foundry".

building type

building:is_selected()

Whether the building is currently selected.

boolean

building:is_active()

Whether the building is currently active/operating.

boolean

building:is_ready()

Whether the building has finished spawning/emerging and its GameObject is active.

boolean

building:get_output()

The resource type this building currently outputs (mines/wells/etc).

string

building:get_spawn_time()

game:get_time() value at the moment this building was spawned.

number

building:get_hit_points()

Current hit points.

number

building:is_extractor()

Whether this building type extracts a raw resource (e.g. a mine).

boolean

building:is_research_building()

Whether this building type is a research lab.

boolean

building:is_defense_building()

Whether this building type is a defensive structure (e.g. a turret).

boolean

building:is_unit_building()

Whether this building type trains/produces units.

boolean

building:is_base()

Whether this building type is a construction base (HQ/Exphub) that other buildings attach to.

boolean

building:is_starved()

Factory only — whether production is currently blocked due to missing input resources.

boolean

building:is_depleted()

Resource-extracting building only — whether the resource patch it's built on has run dry.

boolean

building:is_alive()

Whether the building is still standing.

boolean

building:is_enemy()

Whether the building is hostile to the local player.

boolean

building:get_pos()

Current map cell coordinates.

{x, y}

building:get_buildable_area()

HQ/Exphub only — the placement bounds within which the player can construct new buildings.

{ min_x, min_y, max_x, max_y } or nil

building:get_base()

Finds the nearest owned HQ or Exphub this building is attached to.

building or nil

building:get_connected_buildings()

Resource-extracting building only — ids of the buildings connected downstream of it (e.g. a mine's connected factories).

array of building ids

building:get_queued_units()

Unit-producing building only — its current production queue.

table of { [unit_type] = amount }

Modifiers 4
building:set_hit_points(hp)

Sets current hit points.

  • hp number
building:set_can_salvage(can_salvage)

Enables/disables whether the player is allowed to salvage/sell this building.

  • can_salvage boolean

Used heavily in campaign scripts to lock scripted starting buildings so players can't sell their tutorial base.

building:set_disconnected(disconnected)

Forces this building's supply-chain connection state.

  • disconnected boolean

boolean

building:set_invincible(enabled)

Makes the building immune to damage.

  • enabled boolean
Actions 6
building:select()

Selects this building.

building:salvage()

Sells/salvages this building immediately, as if the player triggered it.

building:connect(building_id)

Attempts to connect this building to another (e.g. a mine to a factory via conveyor).

  • building_id string

boolean (success)

building:queue_unit(unit_type)

Queues a unit for production at this specific building.

boolean

building:pack()

Exphub only — packs it back into a mobile BEV.

building:upgrade()

Exphub only — upgrades it.

boolean

Global Callbacks

Callbacks don't share local variables — to pass data between them (or from start()), store it with game:set_value and read it back with game:get_value.

Global Callbacks 15
on_ui_clicked(ui_id)

Fired when the player clicks a tracked UI element.

on_built(building_id)

Fired when a building finishes construction.

on_building_selected(building_id)

Fired when the player selects a single building.

on_unit_selected(unit_id)

Fired when the player selects a single unit.

on_units_selected(unit_ids)

Fired when the player selects multiple units.

on_unit_killed(unit_id)

Fired when any unit dies.

on_building_destroyed(building, connections)

Fired when any building is destroyed.

  • building Building
  • connections — the list of building ids it was connected to
on_selection_changed(selection_type)

Fired on every selection change, with "none" | "unit" | "building".

on_inventory_item_added(item_type, amount)

Fired when a resource is added to the player's inventory.

on_command(unit_id, command_name, target_type, map_coords)

Fired when the player issues a raw command to a unit.

on_buildings_connected(building_id_1, building_id_2)

Fired when two buildings become connected.

on_connection_start(building_id)

Fired when the player starts dragging a connection from a building.

on_building_revealed(building_id)

Fired when fog of war first reveals a building.

on_building_failed(building_type, reason)

Fired when a building placement attempt fails.

on_resource_revealed(resource_type)

Fired when fog of war first reveals a resource patch.

Glossary

Unit and building type strings used throughout this reference — e.g. the type field in game:spawn_unit(), game:query_units(), and similar functions.

Unit Types 22
aircraft_carrier

Aircraft Carrier — Massive capital ship that can carry and refuel aircraft.

apc

Armored Personnel Carrier — Armored infantry carrier with a cannon that engages aircraft and light vehicles.

chopper

Attack Helicopter — Fast attack helicopter effective against infantry and light vehicles.

submarine

Attack Submarine — Stealthy attack submarine that ambushes enemy vessels with long-range torpedoes.

bev

Base Expansion Vehicle — Armored vehicle that deploys into a Base Expansion Hub, extending your construction radius.

truck

Cargo Truck — Heavy military truck for transporting resources across the battlefield.

destroyer

Destroyer — Heavily armored surface warship delivering devastating railgun fire against sea and coastal targets.

engineer

Engineer — Army engineer capable of repairing vehicles, infantry, and structures.

fighter

Fighter Jet — Next-gen stealth fighter jet for air superiority and ground strikes.

joint_strike_ship

Joint Strike Ship — Long-range missile warship that can strike both ground and sea targets from a distance.

patrol_boat

Light Combat Ship — Fast combat ship armed with an autocannon for naval combat and coastal interdiction.

lrv

Light Recon Vehicle — Fast scout vehicle armed with a dual remote-controlled machinegun.

tank

Main Battle Tank — Powerful main battle tank that excels at destroying enemy vehicles.

mlrs

MLRS — Long-range rocket artillery that saturates an area with multiple rockets.

artillery

Mobile Artillery — Long-range artillery vehicle that engages targets from a safe distance.

frigate

Multirole Frigate — Versatile escort frigate armed with anti-air missiles and anti-naval torpedoes.

rocket_trooper

Rocket Trooper — Anti-armor infantry armed with compact homing rocket launchers.

sniper

Sniper — Long-range infantry specialist that picks off enemy personnel from extreme distances.

bomber

Stealth Bomber — Fast stealth bomber that delivers heavy explosive payloads.

strike_drone

Strike Drone — Autonomous UCAV that delivers precision explosive ground strikes.

chinook

Transport Helicopter — Transport helicopter that carries up to 10 soldiers over any terrain.

trooper

Trooper — Standard infantry armed with an advanced assault rifle and next-gen armor.

Building Types 21
airfield

Airfield — Airfield that builds, houses, and rearms up to 6 air units.

armory

Armory — Field armory that refines weaponry and unlocks advanced military upgrades.

exphub

Base Expansion Hub — Lightweight forward construction hub that enables basic building in remote locations.

hq

Construction Center — Permanent construction center that builds and supports advanced structures.

electronics_fab

Electronics Fab — Miniaturized factory that produces microchips and electronics for military hardware.

foundry

Foundry — Smelts raw ore into steel for construction and unit production.

gate

Gate — Reinforced gate that automatically opens for allied units and blocks enemies.

quarters

Infantry Quarters — Combined barracks and training facility for producing infantry units.

logibay

Logibay — Logistics hub where Cargo Trucks dock to load and unload resources.

mine

Mine — Miniaturized mining facility that extracts iron, rare earths, and uranium.

rocket_pod

Missile Turret — Anti-air defense turret that targets all kinds of aircraft.

reactor

Nuclear Enrichment Plant — Advanced plant that enriches uranium for nuclear weapons production.

silo

Nuclear Silo — Subterranean bunker that constructs and launches tactical nuclear ICBMs.

oil_derrick

Oil Derrick — Modular oil extraction unit that pumps crude oil for fuel production.

radar

Radar — Sensor tower that reveals the fog of war and detects cloaked units.

refinery

Refinery — Miniaturized refinery that converts crude oil into vehicle and aircraft fuel.

research_lab

Research Center — AI-powered facility that develops and deploys upgrades and new technologies.

shipyard

Shipyard — Massive coastal facility for building and deploying naval units.

turret

Turret — Automated defense turret that independently targets and engages ground enemies.

assembly

Vehicle Assembly — Advanced facility for producing a wide array of military vehicles.

wall

Wall — Reinforced barrier that blocks enemy movement into your base.

Global Helpers

Injected into every script's global scope.

Global Helpers 16
loop(table, action)

Iterates an array-like table, calling action(element, index) for each entry.

  • table table — array-like table
  • action function(element, index) — called once per element

Shorthand for ipairs(). Used throughout the campaign scripts instead of writing for loops.

count(table)

Returns the number of elements in an array-like table (#table).

  • table table

number

to_string(value)

Like tostring(), but returns the string "nil" instead of erroring/returning nothing when value is nil.

  • value any

string

contains(list, value)

Returns true if value is present anywhere in list.

  • list table
  • value any

boolean

find_index(list, value)

Returns the 1-based index of value in list, or -1 if not found.

  • list table
  • value any

number or nil

distance2D(pos1, pos2)

Euclidean distance between two {x, y} points.

  • pos1 {x, y}
  • pos2 {x, y}

number

distance3D(pos1, pos2)

Euclidean distance between two {x, y, z} points.

  • pos1 {x, y, z}
  • pos2 {x, y, z}

number

normalize2D(vector)

Returns vector scaled to unit length.

  • vector {x, y}

{x, y}

normalize3D(vector)

Returns vector scaled to unit length.

  • vector {x, y, z}

{x, y, z}

add2D(pos1, pos2)

Component-wise addition.

  • pos1 {x, y}
  • pos2 {x, y}

{x, y}

add3D(pos1, pos2)

Component-wise addition.

  • pos1 {x, y, z}
  • pos2 {x, y, z}

{x, y, z}

sub2D(pos1, pos2)

Component-wise subtraction (pos1 - pos2).

  • pos1 {x, y}
  • pos2 {x, y}

{x, y}

sub3D(pos1, pos2)

Component-wise subtraction (pos1 - pos2).

  • pos1 {x, y, z}
  • pos2 {x, y, z}

{x, y, z}

mul2D(pos, scalar)

Component-wise scale.

  • pos {x, y}
  • scalar number

{x, y}

mul3D(pos, scalar)

Component-wise scale.

  • pos {x, y, z}
  • scalar number

{x, y, z}

round(value)

Rounds to the nearest integer.

  • value number

number

Examples

Full, unedited scripts from the GDA campaign.

Mission 1

mission1.lua


local iron_mine_pos = { x = 40, y = 65 }
local enemy_spawn_pos = { x = 51, y = 119 }

function count_produced_troopers()
    local troopers = game:query_units({ type = "trooper", player = 1 })
    local trooper_count = count(troopers)
    local produced = trooper_count - game:get_value("initial_trooper_count", 0)
    return produced
end

local exphub = nil
local foundry = nil

function remaining_time(time_key, duration)
    local time_value = game:get_value(time_key)
    local delta_time = duration - (game:get_time() - time_value)
    if delta_time < 0 then
        return 0
    end
    return delta_time
end

function has_time_passed(time_key, duration)    
    local remaining = remaining_time(time_key, duration)
    return remaining <= 0
end

function lock_enemy_walls(lock)
    local enemy_walls = game:query_buildings({ type = "wall", player = 2 })
    loop(enemy_walls, function(wall)
        wall:set_invincible(lock)
    end)
    local enemy_gates = game:query_buildings({ type = "gate", player = 2 })
    loop(enemy_gates, function(gate)
        gate:set_invincible(lock)
    end)
end

function spawn_enemies(type, pos, count)
    for i = 1, count do
        local unit = game:spawn_unit({ type = type, pos = pos, player = 2 })
        unit.attack_move(exphub:get_pos())
    end
end

function send_major_wave()
    local draft = {
        { type = "trooper",        count = 5 },
        { type = "rocket_trooper", count = 3 },
        { type = "lrv",            count = 3 }
    }
    for _, unit in ipairs(draft) do
        spawn_enemies(unit.type, enemy_spawn_pos, unit.count)
    end
end

local transport = nil
function start()
    foundry = game:query_buildings({ type = "factory", player = 1 })[1]
    foundry:set_can_salvage(false)

    exphub = game:query_buildings({ type = "exphub", player = 1 })[1]
    exphub:set_can_salvage(false)

    lock_enemy_walls(true)

    local transports = game:query_units({ type = "chinook", player = 1 })
    if (count(transports) > 0) then
        transport = transports[1]
        transport:enable_collision(false)
    end

    game:add_objectives({
        {
            prerequisite = function() return transport ~= nil end,
            on_activated = function()
                transport:move({ x = 48, y = 55 })
                game:camera_follow_unit(transport:get_id())
            end,
            condition = function()
                if not transport:is_moving() then
                    game:camera_follow_unit(nil)
                    game:set_value("ejecting_units", true)
                    return true
                end
                return false
            end
        },
        {
            prerequisite = function() return game:get_value("ejecting_units") end,
            on_activated = function()
                transport:eject_units()
            end,
            condition = function()
                return not transport:is_ejecting_units()
            end,
            on_completed = function()
                game:set_value("transport_exiting", true)
            end
        },
        {
            prerequisite = function() return game:get_value("transport_exiting") end,
            on_activated = function()
                transport:move({ x = 0, y = 0 })
            end,
            condition = function() return not transport:is_moving() end,
            on_completed = function()
                transport:remove()
                transport = nil
            end
        },
        {
            dialog = {
                {
                    character = "faction01_general",
                    message =
                    "Welcome back, Commander."
                },
                {
                    character = "faction01_general",
                    message = "I assume you went through basic training already, so we will get right to the point. Lieutenant Gonzalez?"
                },
                {
                    character = "faction01_lieutenant",
                    message =
                    "Gonzalez reporting. Commander, let's pick up where we left off.",
                    on_spoken = function()
                        game:set_value("intro_completed", true)
                    end
                }
            },
            condition = function() return game:get_value("intro_completed") end
        },
        {
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Fortunately, this training area has a common ore deposit. Let's setup a mine to harvest it."                    
                }
            },
            prerequisite = function() return game:get_value("intro_completed") end,
            title = "BUILD MINE",
            on_activated = function()
                game:show_map_pointer(iron_mine_pos, "BUILD MINE")
                game:set_building_panel_section("industry")
                game:set_ui_highlight("building_mine", true)
            end,
            condition = function()
                local mines = game:query_buildings({ type = "mine", player = 1 });
                if count(mines) > 0 then
                    local mine = mines[1]
                    mine:set_can_salvage(false)
                    print("mine_id " .. tostring(mine:get_id()))
                    game:set_value("mine_id", mine:get_id())
                    return true
                else
                    return false
                end
            end,
            on_completed = function()
                game:hide_pointer()
            end,
        },
        {
            title = "CONNECT FOUNDRY",
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Excellent. Select the mine, then right-click on the foundry, or use the connect command, to connect them."
                }
            },
            on_activated = function()
                game:set_value("expect_foundry_connection", true)
                local mine = game:get_building(game:get_value("mine_id"))
                if not mine:is_selected() then                        
                    game:show_map_pointer(mine:get_pos(), "SELECT MINE") 
                    game:set_camera_pos(mine:get_pos())
                else
                    game:show_map_pointer(foundry:get_pos(), "CONNECT FOUNDRY")
                    game:set_camera_pos(foundry:get_pos())
                    game:set_ui_highlight("action_connect", true)
                end
            end,
            prerequisite = function() return game:get_value("mine_id") ~= nil end,
            condition = function() return game:get_value("foundry_connected") end,
            on_completed = function()
                game:hide_pointer()
            end
        },        
        {
            prerequisite = function() return game:get_value("foundry_connected") end,
            title = "BUILD QUARTERS",
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Nice job Commander. It's time to build a quarters to train some troops."
                }
            },
            on_activated = function()
                game:set_building_panel_section("military")
                game:set_ui_highlight("building_quarters", true)
            end,
            condition = function()
                local quarters = game:query_buildings({ type = "quarters", player = 1 })
                if (count(quarters) > 0) then
                    local quarter = quarters[1]
                    quarter:set_can_salvage(false)
                    game:set_value("quarter_id", quarter:get_id())
                    return true
                else
                    return false
                end
            end            
        },
        {
            title = "TRAIN TROOPERS",
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "You got it. We need at least 5 troopers to repel the incoming attackers."
                }
            },
            prerequisite = function() return game:get_value("quarter_id") ~= nil end,
            on_activated = function()
                local troopers = game:query_units({ type = "trooper", player = 1 })
                local initial_trooper_count = count(troopers)
                game:set_value("initial_trooper_count", initial_trooper_count)
                local quarter = game:get_building(game:get_value("quarter_id"))
                game:show_map_pointer(quarter.get_pos(), "TRAIN TROOPERS")
                game:set_camera_pos(quarter:get_pos())
            end,
            status = function() 
                local produced = count_produced_troopers()
                if (produced > 1 and not game:get_value("attack_warning_shown", false)) then
                    game:set_value("attack_warning_shown", true)
                    local enemy_helicopter = game:query_units({ type = "chinook", player = 2 })[1]
                    enemy_helicopter:eject_units()
                    game:set_camera_pos(enemy_helicopter:get_pos())
                    game:fog_reveal({ pos = enemy_helicopter:get_pos(), radius = 10, duration = 5 })                    
                end
                return tostring(produced) .. "/5" 
            end,
            condition = function() return count_produced_troopers() >= 5 end,            
            on_completed = function()
                local enemy_helicopter = game:query_units({ type = "chinook", player = 2 })[1]
                enemy_helicopter:remove()
                game:set_value("troopers_trained", true)
                game:hide_pointer()
            end
        },
        {
            prerequisite = function () return game:get_value("attack_warning_shown") end,                
            dialog = {
                {
                    character = "faction01_major",
                    message = "Lieutenant, Major Williams here. We have received intel of incoming Rebel troops on the way to your location. They bear the insigna of the Los Víboras Cartel."
                },
                {
                    character = "faction01_lieutenant",
                    message = "The Los Víboras Cartel? They're being led by that ruthless viper, Valencia. We've never been able to put a face to her, but know that she's not to be messed with."
                },
                {
                    character = "faction01_lieutenant",
                    message = "Wait, how'd they even cross the border? Crap...We don't have time to figure this out."
                },
                {
                    character = "faction01_lieutenant",
                    message = "Well, this is it, Commander, this is your moment to shine. Let's get those troops ready.",
                    on_activated = function()
                        game:set_camera_pos(exphub:get_pos())
                    end
                }
            }
        },
        {
            failure_condition = function()
                local base_count = count(game:query_buildings({ player = 1, type = "exphub" }))
                local bev_count = count(game:query_units({ player = 1, type = "bev" }))
                return base_count == 0 and bev_count == 0
            end,
            on_failed = function()
                game:set_mission_failed()
            end,
        },
        {
            prerequisite = function() return game:get_value("troopers_trained") end,
            title = "REPEL THE ATTACKERS",
            condition = function() 
                local attackers = game:query_units({ player = 2 })
                return count(attackers) == 0
            end,
            on_completed = function()
                game:set_value("attackers_repelled", true)
            end,
            dialog = {
                {
                    character = "faction01_major",
                    message = "Commander, Valencia's Rebel cell is at the outskirts of your base - get ready for a brawl!",
                    can_skip = false
                },
                {
                    character = "faction02_cartelboss_obfuscated",
                    message = "Attention, GDA weaklings! This land belongs to the Víboras Cartel now. Surrender now or we will open fire upon your pitiful base and bury your men...Or perhaps I'll just enjoy hunting you all murderers down.",
                    on_activated = function()
                        local attackers = game:query_units({ player = 2 })
                        loop(attackers, function(attacker)
                            attacker:attack_move(exphub:get_pos())
                        end)
                        game:set_camera_pos(attackers[1]:get_pos())
                        game:fog_reveal({ pos = attackers[1]:get_pos(), radius = 10, duration = 5 })
                    end
                },
                {
                    character = "faction01_lieutenant",
                    message = "All your troops are ready. It's time to show those rebels who's boss.",
                    on_activated = function()
                        game:set_camera_pos(exphub:get_pos())
                    end
                }
            }
        },
        {
            prerequisite = function() return game:get_value("attackers_repelled") end,
            dialog = {
                {
                    character = "faction01_general",
                    message = "Well done, Commander."
                },
                {
                    character = "faction01_general",
                    message = "This could have turned out much worse. We're are lucky to.. Lieutenant, do you have something to say?"
                },
                {
                    character = "faction01_lieutenant",
                    message = "I am sorry General, but this was just a diversion. New satellite imagery revealed enemy presence in the North.",
                    on_activated = function()
                        local _enemy_quarters = game:query_buildings({ type = "quarters", player = 2 })
                        if (count(_enemy_quarters) > 0) then
                            local enemy_quarters = _enemy_quarters[1]
                            game:fog_reveal({ pos = enemy_quarters:get_pos(), radius = 10, duration = 5 })
                            game:set_camera_pos(enemy_quarters:get_pos())
                        end
                    end
                },
                {
                    character = "faction01_general",
                    message = "This is why I hate celebrating things. Lieutenant, you know what to do.",
                },
                {
                    character = "faction01_lieutenant",
                    message = "Yes, General, I will take care of it. Commander, we need to rally and preemptively strike them before it's too late."
                },
                {
                    character = "faction01_lieutenant",
                    message = "Destroy the enemy quarters before they can attack us.",
                    on_activated = function()
                        game:set_value("preemptive_strike", true)
                        game:set_value("major_attack_warning_time", 60*3)
                    end
                }
            }
        },
        {
            prerequisite = function() return game:get_value("preemptive_strike") end,
            title = "PREEMPTIVE STRIKE",
            on_activated = function()
                lock_enemy_walls(false)
                local _enemy_quarters = game:query_buildings({ type = "quarters", player = 2 })
                if (count(_enemy_quarters) > 0) then
                    local enemy_quarters = _enemy_quarters[1]
                    game:show_map_pointer(enemy_quarters:get_pos(), "DESTROY ENEMY QUARTERS")
                end
            end,
            condition = function() return count(game:query_buildings({ type = "quarters", player = 2 })) == 0 end,
            on_completed = function()
                game:set_mission_complete()
            end
        },
        {
            title = "MAJOR ATTACK INCOMING",
            prerequisite = function() return game:get_value("preemptive_strike") end,
            status = function(delta_time) 
                local remaining = game:get_value("major_attack_warning_time")
                remaining = remaining - delta_time;                
                if remaining < 0 then
                    remaining = 0
                end
                game:set_value("major_attack_warning_time", remaining)
                return tostring(math.floor(remaining + 0.5)) .. "s" 
            end,
            condition = function() return game:get_value("major_attack_warning_time") == 0 end,
            on_completed = function()
                send_major_wave()
            end
        }
    })
end

function on_selection_changed(selection_type)
    if game:get_value("expect_foundry_connection") then
        local mine = game:get_building(game:get_value("mine_id"))
        if mine ~= nil then
            if selection_type == "building" then
                if mine:is_selected() then
                    game:delayed_call(.3, function() game:set_ui_highlight("action_connect", true) end)
                    game:show_map_pointer(foundry:get_pos(), "CONNECT FOUNDRY")
                    game:set_camera_pos(foundry:get_pos())
                else
                    game:show_map_pointer(mine:get_pos(), "SELECT MINE")
                    game:set_camera_pos(mine:get_pos())
                end
            else
                game:show_map_pointer(mine:get_pos(), "SELECT MINE")
                game:set_camera_pos(mine:get_pos())
            end
        end
    end
end

function on_buildings_connected(building1_id, building2_id)
    local building1 = game:get_building(building1_id)
    local building2 = game:get_building(building2_id)
    print("on_buildings_connected " .. building1.get_type() .. " " .. building2.get_type())
    if building1.get_type() == "mine" and building2.get_type() == "factory" then
        game:set_value("foundry_connected", true)
        game:set_value("expect_foundry_connection", false)
        game:hide_pointer()
    end
end

Mission 2

mission2.lua


local iron_mine1_pos = { x = 56, y = 245 }

local reinforcements1_pos = { x = 250, y = 137 }
local reinforcements1_move_to = { x = 229, y = 138 }

local reinforcements2_pos = { x = 171, y = 8 }
local reinforcements2_move_to = { x = 171, y = 28 }

local reinforcements3_pos = { x = 7, y = 48 }
local reinforcements3_move_to = { x = 27, y = 53 }

local truck = nil
local foundry = nil
local quarters = nil
local mine = nil
local enemy_base = nil

function count_produced_troopers()
    local troopers = game:query_units({ type = "trooper", player = 1 })
    local rocket_troopers = game:query_units({ type = "rocket_trooper", player = 1 })
    local snipers = game:query_units({ type = "sniper", player = 1 })
    local trooper_count = count(troopers) + count(rocket_troopers) + count(snipers)
    local produced = trooper_count - game:get_value("initial_trooper_count", 0)
    return produced
end

function count_units_with_tag(tag)
    local units = game:query_units({ player = 2, tag = tag })
    return count(units)
end

function count_buildings_with_tag(tag)
    local buildings = game:query_buildings({ player = 2, tag = tag })
    return count(buildings)
end

function get_average_pos(playerIndex, tag)
    local units = game:query_units({ player = playerIndex, tag = tag })
    local total_x = 0
    local total_y = 0
    for i, unit in ipairs(units) do
        total_x = total_x + unit:get_pos().x
        total_y = total_y + unit:get_pos().y
    end
    return { x = total_x / count(units), y = total_y / count(units) }
end

function spawn_reinforcements(pos, units)
    local spawned_units = {}
    for i, unit in ipairs(units) do
        for j = 1, unit.count do
            local instance = game:spawn_unit({ type = unit.type, pos = pos, player = 1 })
            table.insert(spawned_units, instance)
        end
    end
    return spawned_units
end

function reinforce(pos, move_to, type)
    local draft = nil
    if type == "heavy" then
        draft = { 
            { type = "chopper", count = 6 }, 
            { type = "artillery", count = 2 }, 
            { type = "tank", count = 2 }
        }
    else
        draft = { 
            { type = "trooper", count = 5 }, 
            { type = "rocket_trooper", count = 3 }, 
            { type = "tank", count = 2 }
        }
    end

    local spawned_units = spawn_reinforcements(pos, draft)
    game:move_units(spawned_units, move_to)
    game:delayed_call(.5, function()
        game:set_camera_pos(pos)
    end)
end

function start()
    truck = game:query_units({ type = "truck", player = 1 })[1]
    truck:set_invincible(true)
    foundry = game:query_buildings({ type = "factory", player = 1 })[1]
    foundry:set_can_salvage(false)
    quarters = game:query_buildings({ type = "quarters", player = 1 })[1]
    quarters:set_can_salvage(false)
    enemy_base = game:query_buildings({ type = "hq", player = 2 })[1]

    game:add_objectives({
        {
            dialog = {
                {
                    character = "faction01_general",
                    message = "Commander, we've arrived at the outskirts of Valencia's compound. Let's cut to the chase."
                },
                {
                    character = "faction01_general",
                    message =
                    "The Cartel has gotten its hands on some serious firepower. They have their own private standing army. Lieutenant, get the Commander briefed on base operations."
                },
                {
                    character = "faction01_lieutenant",
                    message = "Yes, sir!"
                },
                {
                    character = "faction01_lieutenant",
                    message = "There is a nearby common ore deposit, that we need to secure to set up military production.",
                    on_activated = function()
                        game:set_value("intro_completed", true)                        
                    end
                }
            },
        },
        {
            title = "SECURE COMMON ORE DEPOSIT",
            prerequisite = function() return game:get_value("intro_completed") end,
            on_activated = function()
                game:show_map_pointer(iron_mine1_pos, "SECURE COMMON ORE DEPOSIT")
            end,            
            condition = function()
                local enemies = game:query_units({ player = 2, tag = "1" })
                return count(enemies) == 0
            end,
            on_completed = function()
                game:hide_pointer()
                game:set_value("iron_mine1_secured", true)
            end
        },        
        {
            title = "BUILD MINE",
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Perfect. You know the drill, Commander. I mean, let's build a mine."
                }
            },
            prerequisite = function() return game:get_value("iron_mine1_secured") end,
            on_activated = function()
                game:show_map_pointer(iron_mine1_pos, "BUILD MINE")
            end,
            on_completed = function()
                game:hide_pointer()
                game:set_value("mine_built", true)
            end,
            condition = function()
                local mines = game:query_buildings({ type = "mine", player = 1 })
                if (count(mines) > 0) then
                    mine = mines[1]
                    mine:set_can_salvage(false)
                    return true
                else
                    return false
                end
            end
        },        
        {
            prerequisite = function() return game:get_value("mine_built") end,
            on_activated = function()
                game:set_camera_pos(truck:get_pos())
                truck:set_invincible(false)
            end,
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Excellent. This deposit will be enough for primary production. Whenever you are ready, use these trucks to bring common ore to the foundry."
                },
                {
                    character = "faction01_lieutenant",
                    message = "Time to start troop production. You will need it!",
                    on_activated = function()
                        game:set_value("truck_hint_given", true)
                    end
                }
            }
        },
        {
            title = "TRAIN TROOPERS",
            -- prerequisite = function() return game:get_value("mine_connected") end,
            prerequisite = function() return game:get_value("truck_hint_given") end,
            on_activated = function()
                local troopers = game:query_units({ type = "trooper", player = 1 })
                local rocket_troopers = game:query_units({ type = "rocket_trooper", player = 1 })
                local snipers = game:query_units({ type = "sniper", player = 1 })
                local initial_trooper_count = count(troopers) + count(rocket_troopers) + count(snipers)
                game:set_value("initial_trooper_count", initial_trooper_count)
                game:show_map_pointer(quarters:get_pos(), "TRAIN TROOPERS")
                game:set_camera_pos(quarters:get_pos())
            end,
            condition = function()
                return count_produced_troopers() >= 5
            end,
            status = function() return tostring(count_produced_troopers()) .. "/5" end,
            on_completed = function()
                game:hide_pointer()
                game:set_value("troopers_trained", true)
            end,
            dialog = {                
                {
                    character = "faction01_lieutenant",
                    message = "Great work, commander! Time to start troop production. You will need it!"
                }
            }
        },
        {
            dialog = {
                {
                    character = "faction01_general",
                    message = "Commander, it's go time. Secure all nearby common ore deposits. Beware, the rebels are around and they are not exactly thrilled to see you."
                }                
            },
            title = "SECURE COMMON ORE DEPOSIT",
            prerequisite = function() return game:get_value("troopers_trained") end,
            on_activated = function()
                game:show_map_pointer(get_average_pos(2, "2"), "SECURE COMMON ORE")
            end,
            condition = function()
                return count_units_with_tag("2") == 0
            end,
            on_completed = function()
                game:hide_pointer()
                game:set_value("common_ore_deposit_secured", true)
            end
        },
        {
            prerequisite = function() return game:get_value("common_ore_deposit_secured") end,
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "You are doing great, commander!"
                },
                {
                    character = "faction01_lieutenant",
                    message = "The general has formally approved sending you additional reinforcements."
                },
                {
                    character = "faction01_lieutenant",
                    message = "But you will need to secure more common ore deposits to degrade the rebels industrial capacity.",
                    on_activated = function()
                        game:set_value("intro2_completed", true)                        
                    end
                }
            }
        },
        {            
            title = "SECURE COMMON ORE DEPOSIT",
            prerequisite = function() return game:get_value("intro2_completed") end,
            on_activated = function()
                game:show_map_pointer(get_average_pos(2, "3"), "SECURE COMMON ORE DEPOSIT")
            end,
            condition = function()
                return count_units_with_tag("3") == 0
            end,
            on_completed = function()
                game:hide_pointer()
                game:set_value("common_ore_deposit_secured2", true)
            end
        },
        {
            prerequisite = function() return game:get_value("common_ore_deposit_secured2") end,
            dialog = {
                {
                    character = "faction01_general",
                    message = "Lieutenant, please coordinate the arrival of reinforcements to a secure location."
                },
                {
                    character = "faction01_lieutenant",
                    message = "Roger that, sir. Reinforcements are on the way.",
                    on_activated = function()
                        reinforce(reinforcements1_pos, reinforcements1_move_to)
                        game:set_value("reinforcements1_sent", true)
                    end
                }
            },   
        },
        {
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Commander, the rebels are still holding more common ore deposits, according to intelligence reports."
                },
                {
                    character = "faction01_lieutenant",
                    message = "We need to take a shot at their last potential source of common ore."
                }
            },
            title = "SECURE COMMON ORE DEPOSIT",
            prerequisite = function() return game:get_value("reinforcements1_sent") end,
            on_activated = function()
                game:show_map_pointer(get_average_pos(2, "4"), "SECURE COMMON ORE DEPOSIT")
            end,
            condition = function()
                local units = count_units_with_tag("4")
                local buildings = count_buildings_with_tag("4")
                return units == 0 and buildings == 0
            end,
            on_completed = function()
                game:hide_pointer()
                game:set_value("common_ore_deposit_secured3", true)
            end
        },        
        {
            prerequisite = function() return game:get_value("common_ore_deposit_secured3") end,
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "That was a close one, commander. The rebels have a lot of firepower."
                },
                {
                    character = "faction01_general",
                    message = "This mission is unfortunately taking a heavier toll than expected. But we need to push through."
                },
                {
                    character = "faction01_lieutenant",
                    message = "Roger that, sir. Additional reinforcements are on the way.",
                    on_activated = function()
                        reinforce(reinforcements2_pos, reinforcements2_move_to)
                        game:set_value("reinforcements2_sent", true)
                    end
                }
            },   
        },
        {
            prerequisite = function() return game:get_value("reinforcements2_sent") end,
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Wait a minute. Reports are coming in that there may be air defense structures in the area."
                },
                {
                    character = "faction01_general",
                    message = "We can't send you advanced, mission critical reinforcements until you take down their air defense.",
                    on_activated = function()
                        game:set_value("intro3_completed", true)
                    end
                }
            }
        },
        {
            title = "DESTROY MISSILE TURRETS",
            prerequisite = function() return game:get_value("intro3_completed") end,
            on_activated = function()
                local rocket_pods = game:query_buildings({ type = "rocket_pod", player = 2 })
                local remaining = count(rocket_pods)
                game:set_value("initial_missile_turret_count", remaining)
                game:set_value("previous_missile_turret_count", remaining)
                game:show_map_pointer(rocket_pods[1]:get_pos(), "DESTROY MISSILE TURRET")
            end,
            condition = function()                
                local rocket_pods = game:query_buildings({ type = "rocket_pod", player = 2 })
                return count(rocket_pods) == 0
            end,
            status = function()
                local rocket_pods = game:query_buildings({ type = "rocket_pod", player = 2 })
                local remaining = count(rocket_pods)
                local previous_count = game:get_value("previous_missile_turret_count")

                if remaining ~= previous_count then
                    if remaining > 0 then
                        game:show_map_pointer(rocket_pods[1]:get_pos(), "DESTROY MISSILE TURRET")
                    end
                    game:set_value("previous_missile_turret_count", remaining)
                end

                local initial_count = game:get_value("initial_missile_turret_count")
                local destroyed_count = initial_count - remaining
                return destroyed_count .. "/" .. initial_count
            end,
            on_completed = function()
                game:hide_pointers()
                game:set_value("missile_turrets_destroyed", true)
            end
        },
        {
            prerequisite = function() return game:get_value("missile_turrets_destroyed") end,
            dialog = {
                {
                    character = "faction01_general",
                    message = "Fantastic work, commander."
                },
                {
                    character = "faction01_general",
                    message = "The enemy base is in sight. We are sending additional reinforcements to finish this off.",
                    on_activated = function()
                        reinforce(reinforcements3_pos, reinforcements3_move_to, "heavy")
                        game:set_value("reinforcements3_sent", true)
                    end
                }
            },   
        },
        {
            title = "DESTROY ENEMY BASE",
            prerequisite = function() return game:get_value("reinforcements3_sent") end,
            on_activated = function()                
                game:show_map_pointer(enemy_base:get_pos(), "DESTROY ENEMY BASE")
            end,
            condition = function()
                return not enemy_base:is_alive()
            end,
            on_completed = function()
                game:hide_pointer()
                game:set_value("enemy_base_destroyed", true)
                game:set_mission_complete()
            end
        }
    })
end

Mission 3

mission3.lua

local mine1_pos = { x = 56, y = 138 }
local oilrig1_pos = { x = 40, y = 95 }
local area_to_scout = { x = 157, y = 101 }
local area_reveal = { x = 179, y = 104 }

function get_units_average_pos()    
    local units = game:query_units({ player = 1 })
    if count(units) == 0 then
        return nil
    end
    local total_x = 0
    local total_y = 0
    for i, unit in ipairs(units) do
        total_x = total_x + unit:get_pos().x
        total_y = total_y + unit:get_pos().y
    end
    return { x = total_x / count(units), y = total_y / count(units) }
end

function is_close(pos1, pos2, threshold)
    local distance = distance2D(pos1, pos2)
    return distance < threshold
end

function target_lab(lab)
    game:show_map_pointer(lab:get_pos(), "DESTROY RESEARCH LAB")
    game:fog_reveal({ pos = lab:get_pos(), radius = 10, duration = 5 })
end

function start()
    local exphubs = game:query_buildings({ type = "exphub", player = 1 })
    local exphub = exphubs[1]
    
    local playerBuildings = game:query_buildings({ player = 1 })
    loop(playerBuildings, function(building)
        building:set_disconnected(true)
    end)
    game:enable_building_panel(false)

    game:add_objectives({
        {
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Welcome back, Commander! Your troops are landing here, near the beachhead. This is about as close as we can get you by air. Let's hope someone's still alive out there..."
                },
                {
                    duration = 2,
                    on_activated = function()                        
                        game:set_camera_pos(exphub:get_pos())
                    end
                },
                {
                    character = "faction01_lieutenant",
                    message = "Copperhead Actual, respond, this GDA Military Command aboard the GDS Endeavour! Copperhead Actual, please respond..."
                },
                {
                    character = "faction01_lieutenant",
                    message = "Well, it seems like we might be too late. Commander, clear the area of hostiles and see if we can't get that base up and running again, at the very least.",
                    on_activated = function()
                        game:set_value("intro_completed", true)
                    end
                }
            }
        },
        {
            title = "SECURE THE BASE",
            prerequisite = function() return game:get_value("intro_completed") end,
            on_activated = function()
                game:show_map_pointer(exphub:get_pos(), "SECURE THE BASE")
                game:delayed_call(2, function()
                    local average_pos = get_units_average_pos()
                    if average_pos ~= nil then
                        game:set_camera_pos(average_pos)
                    end
                end)                
            end,
            condition = function()
                local enemies = game:query_units({ player = 2, tag = "1" })
                return count(enemies) == 0
            end,
            failure_condition = function()
                local units = game:query_units({ player = 1 })
                return count(units) == 0
            end,
            on_completed = function()
                game:hide_pointer()
                game:set_value("base_secured", true)
            end,
            on_failed = function()
                game:set_mission_failed()
            end
        },
        {
            prerequisite = function() return game:get_value("base_secured") end,
            on_activated = function()
                loop(playerBuildings, function(building)
                    building:set_disconnected(false)
                end)
                game:enable_building_panel(true)
            end,
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Excellent work. I was able to get back control of the Base's systems. It appears that Copperhead's platoon is gone, and the servers mention Rebel Research Labs as well as a new weapons technology called 'Lightbringer'"
                },
                {
                    character = "faction01_lieutenant",
                    message = "You'll have to clear those labs out."
                },
                {
                    character = "faction01_major",
                    message = "Sir, I'll get to finding where those labs are located while you set up your base."
                },
                {
                    character = "faction01_lieutenant",
                    message = "Thanks, Major. Let's set up our resource and unit production, in the meantime. Build up a Mine first, Commander.",
                    on_activated = function()
                        game:set_value("intro2_completed", true)
                    end
                }
            }
        },
        {
            title = "BUILD A MINE",
            prerequisite = function() return game:get_value("intro2_completed") end,
            on_activated = function()
                game:show_map_pointer(mine1_pos, "BUILD A MINE")
                game:set_building_panel_section("industry")
                game:set_ui_highlight("building_mine", true)
            end,
            condition = function()
                local mines = game:query_buildings({ type = "mine", player = 1 })
                return count(mines) > 0
            end,
            on_completed = function()
                game:hide_pointer()
                game:set_value("mine_built", true)
            end
        },
        {
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Now build a Foundry."
                }
            },
            title = "BUILD A FOUNDRY",
            prerequisite = function() return game:get_value("mine_built") end,
            on_activated = function()
                game:set_building_panel_section("industry")
                game:set_ui_highlight("building_factory", true)
            end,
            condition = function()
                local foundries = game:query_buildings({ type = "factory", player = 1 })
                return count(foundries) > 0
            end,
            on_completed = function()
                game:set_value("foundry_built", true)
            end
        },
        {
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Now that that's taken care of, let's build an Oil Derrick to provide oil to our Refinery."
                }
            },
            title = "BUILD AN OIL DERRICK",
            prerequisite = function() return game:get_value("foundry_built") end,
            on_activated = function()
                game:show_map_pointer(oilrig1_pos, "BUILD AN OIL DERRICK")
                game:set_building_panel_section("industry")
                game:set_ui_highlight("building_oil_derrick", true)
            end,
            condition = function()
                local oilrigs = game:query_buildings({ type = "oil_derrick", player = 1 })
                return count(oilrigs) > 0
            end,
            on_completed = function()
                game:hide_pointer()
                game:set_value("oil_derrick_built", true)
            end
        },
        {
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Excellent! Now select the Oil Derrick, click the 'Connect' button in the BCI interface, then click on the Refinery."
                }
            },
            title = "CONNECT REFINERY",
            on_activated = function()
                game:set_value("expect_refinery_connection", true)
            end,
            prerequisite = function() return game:get_value("oil_derrick_built") end,
            condition = function()
                return game:get_value("refinery_connected")
            end,
            on_help = function()
                game:deselect();
                local oilrigs = game:query_buildings({ type = "oil_derrick", player = 1 })
                if (count(oilrigs) > 0) then
                    oilrigs[1].select();
                end
            end
        },
        {
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Perfect. With fuel, we'll now be able to build vehicles. Our next order of business is to do some scouting to find more resources. Let's build ourselves a Vehicle Assembly. You can find it in the Military Production Tab on your BCI."
                }
            },
            title = "BUILD A VEHICLE ASSEMBLY",
            prerequisite = function() return game:get_value("refinery_connected") end,
            on_activated = function()
                game:set_building_panel_section("military")
                game:set_ui_highlight("building_assembly", true)
            end,
            condition = function()
                local assemblies = game:query_buildings({ type = "assembly", player = 1 })
                return count(assemblies) > 0
            end,
            on_completed = function()
                game:set_value("vehicle_assembly_built", true)
            end
        },
        {
            prerequisite = function() return game:get_value("vehicle_assembly_built") end,
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Great! Light Recon Vehicles, or LRVs for short, are good for scouting thanks to their speed and vision."
                },
                {
                    character = "faction01_lieutenant",
                    message = "Let's build an LRV to scout the area."
                }
            },
            title = "BUILD AN LRV",
            on_activated = function()
                game:deselect();
                local assemblies = game:query_buildings({ type = "assembly", player = 1 })
                if (count(assemblies) > 0) then
                    assemblies[1].select();
                    game:delayed_call(.3, function() game:set_ui_highlight("unit_lrv", true) end)
                end
            end,
            condition = function()
                local lrvs = game:query_units({ type = "lrv", player = 1 })
                return count(lrvs) > 0
            end,
            on_completed = function()
                game:set_value("lrv_built", true)
            end
        },
        {
            title = "SCOUT THE AREA",
            prerequisite = function() return game:get_value("lrv_built") end,
            on_activated = function()
                game:show_map_pointer(area_to_scout, "SCOUT THE AREA")
                game:set_camera_pos(area_to_scout)
            end,
            condition = function()
                local lrvs = game:query_units({ type = "lrv", player = 1 })
                if count(lrvs) > 0 and not lrvs[1]:is_moving() then
                    if (is_close(lrvs[1]:get_pos(), area_to_scout, 20)) then
                        return true
                    end
                end
                return false
            end,
            on_completed = function()
                game:hide_pointer()
                game:set_value("area_scouted", true)
            end,
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "Now that we have an LRV, let's scout out this area. Intel reports that there is a Rebel presence up ahead and that they've seemingly dug something out there."
                }
            }
        },
        {
            prerequisite = function() return game:get_value("area_scouted") end,
            on_activated = function()
                game:set_camera_pos(area_reveal)
                game:fog_reveal({ pos = area_reveal, radius = 20, duration = 5 })
            end,
            dialog = {
                {
                    character = "faction01_lieutenant",
                    message = "It looks like the rebels have drilled up another Oil Field. Hold on, I'm receiving a transmission from the Major. Patching it through."
                },
                {
                    character = "faction01_major",
                    message = "Commander, I've been able to track the labs' locations. I'll ping them on your map and through the Fog of War."
                },
                {
                    character = "faction01_lieutenant",
                    message = "Build up your army and get to crushing those labs, Commander!",
                    on_activated = function()
                        game:set_value("intro3_completed", true)
                    end
                }
            }
        },
        {
            title = "DESTROY ENEMY RESEARCH LABS",
            prerequisite = function() return game:get_value("intro3_completed") end,
            on_activated = function()
                local labs = game:query_buildings({ type = "research_lab", player = 2 })
                local current_time = game:get_time()
                game:set_value("lab_destruction_start_time", current_time)
                local lab_count = count(labs)
                game:set_value("previous_lab_count", lab_count)
                game:set_value("initial_lab_count", lab_count)
                if lab_count > 0 then
                    local first_lab = labs[1]
                    target_lab(first_lab)
                    game:set_camera_pos(first_lab:get_pos())
                end
            end,
            status = function()

                local abbot_warning_sent = game:get_value("abbot_warning_sent")
                if not abbot_warning_sent then
                    local current_time = game:get_time()
                    local start_time = game:get_value("lab_destruction_start_time")
                    local elapsed_time = current_time - start_time
                    if elapsed_time > 60 then
                        game:set_value("abbot_warning_sent", true)
                    end
                end
                
                local labs = game:query_buildings({ type = "research_lab", player = 2 })
                local lab_count = count(labs)
                local initial_lab_count = game:get_value("initial_lab_count")
                local destroyed = initial_lab_count - lab_count

                local previous_lab_count = game:get_value("previous_lab_count")
                if previous_lab_count ~= lab_count then
                    if lab_count > 0 then
                        local first_lab = labs[1]
                        target_lab(first_lab)
                    end
                    game:set_value("previous_lab_count", lab_count)
                end

                return tostring(destroyed) .. " / " .. initial_lab_count
            end,
            condition = function()
                local labs = game:query_buildings({ type = "research_lab", player = 2 })
                return count(labs) == 0
            end,
            on_completed = function()
                game:set_value("lab_destruction_completed", true)
            end
        },
        {
            prerequisite = function() return game:get_value("abbot_warning_sent") end,
            dialog = {
                {
                    character = "faction02_intelofficer",
                    message = "GDA is sending recruits to command troops in the field of battle? Good, I will make short work of this runt."
                },
                {
                    character = "faction01_major",
                    message = "I can't believe it, that's Ethan Abbott! I suppose leaking national state secrets and putting England in danger wasn't enough for him...He's been on Interpol's list since, Commander. I don't like this..."
                },
                {
                    character = "faction01_lieutenant",
                    message = "He'll have a lot to answer for when we get him, that much I can assure you, Major. Let's focus on destroying those labs, for now.",
                    on_activated = function()
                        game:set_value("begin_enemy_waves", true)
                    end
                },
                {
                    character = "faction01_major",
                    message = "Commander, be sure to leave some troops to defend your base. I'm sure the Rebels will send forces to attack you soon, as well."
                }
            }
        },
        {
            prerequisite = function() return game:get_value("begin_enemy_waves") end,
            on_activated = function()
                game:set_value("enemy_waves_started", true)
            end,
            status = function()
                -- todo send waves ?
                return false
            end
        },
        {
            prerequisite = function() return game:get_value("lab_destruction_completed") end,
            dialog = {
                {
                    character = "faction01_major",
                    message = "Sir, we've cleaned out their base, there's no sign of the enemy Commanding Officer here. We'll gather what intel we can find from these wrecked Labs and be on our way."
                },
                {
                    character = "faction01_lieutenant",
                    message = "Abbott has always had a thing for evading capture. But we'll catch him."
                },
                {
                    character = "faction01_general",
                    message = "Good job on destroying those labs, Commander! We're waiting back at HQ for you.",
                    on_activated = function()
                        game:set_mission_complete()
                    end
                }
            }
        }
    })
end

function on_buildings_connected(building1_id, building2_id)
    local building1 = game:get_building(building1_id)
    local building2 = game:get_building(building2_id)
    if building1.get_type() == "oil_derrick" and building2.get_type() == "refinery" then
        game:set_value("refinery_connected", true)
        game:set_value("expect_refinery_connection", false)
        game:hide_pointer()
    end
end

function on_selection_changed(selection_type)
    if game:get_value("expect_refinery_connection") then
        if selection_type == "building" then
            local oilrigs = game:query_buildings({ type = "oil_derrick", player = 1 })
            if count(oilrigs) > 0 and oilrigs[1]:is_selected() then
                game:delayed_call(.3, function() game:set_ui_highlight("action_connect", true) end)
            end
        end
    end
end

function on_ui_clicked(ui_id)
    print("on_ui_clicked " .. ui_id)
    if game:get_value("expect_refinery_connection") then
        if ui_id == "action_connect" then
            local refineries = game:query_buildings({ type = "refinery", player = 1 })
            if count(refineries) > 0 then
                game:show_map_pointer(refineries[1]:get_pos(), "CONNECT REFINERY")
            end
        end
    end
end