Game mechanics¶
base/content/game_mechanics/game_mechanics/ holds the gameplay systems of the base game: time and weather, towns, the company, loans, industries, emissions, notifications, subsidies, achievements, celebrations and the fun elements (blimps, UFOs, fireworks). Each system is a game script plus resources that the script collects at run time, and a mod can add resources of the same kind. This page lists the base game scripts and explains the systems a mod is most likely to extend: notifications, subsidies, company perks and economies. How to write a game script yourself is on Scripting.
The base game scripts¶
The base game has 28 game scripts (.gs.lua), the deluxe DLC adds one. Folders are below base/content/ (DLC: dlcs/urbangames_deluxe_upgrade_pack/content/), shortened by the doubled zip folder. The descriptions come from the events each script subscribes to and from its state records.
| Folder | File | What it runs |
|---|---|---|
game_mechanics/achievements/ |
achievements.gs.lua |
achievements; listens to construction, delivery, level-up, line and build proposal events |
game_mechanics/arrival_tracker/ |
arrivaltracker.gs.lua |
counts people and cargo arriving at their destinations, for filters that other code creates with the onCreatePersonFilter and onCreateCargoFilter events |
game_mechanics/celebrations/ |
celebrations.gs.lua |
celebrations when a town or the company ranks up (OnLevelUp) and for new line connections |
game_mechanics/company/ |
company.gs.lua |
the company state: permits, greening industries (MakeGreen), marketing campaigns, spawning industries (spawnIndustry) |
game_mechanics/company/ |
company_progression.gs.lua |
company experience and rank (company_growth.script), and the ticket price multiplier (OnCalcTicketPrice) |
game_mechanics/company/explorations/ |
exploration_plane.gs.lua |
the Prospection Plane of an exploration (startProspection, endProspection) |
game_mechanics/emission/ |
emissions.gs.lua |
noise and pollution of industries, stations and warehouses |
game_mechanics/finance/ |
loan.gs.lua |
loans (Obtain, Repay) |
game_mechanics/fun_elements/ |
fireworks.gs.lua |
fireworks over towns (startFireworkForTown) |
game_mechanics/fun_elements/ |
fun_elements.gs.lua |
blimps for marketing campaigns and UFOs every two to four years |
game_mechanics/game_time/ |
game_time.gs.lua |
time of day and cloud cover (SetMode, SkipPhase) |
game_mechanics/industries/ |
industries.gs.lua |
industry extensions and closures |
game_mechanics/notifications/ |
notifications.gs.lua |
the list of notifications |
game_mechanics/notifications/ |
availability_notifications.gs.lua |
once a year, notifications about newly available vehicles and constructions |
game_mechanics/subventions/ |
subventions.gs.lua |
subsidies: spawning, progress, completion and failure |
game_mechanics/towns/ |
town.gs.lua |
town rating, marketing campaigns and reputation (towns.script) |
game_mechanics/towns/ |
town_cargo.gs.lua |
town growth and cargo demand (town_growth.script) |
gui/context_helper/ |
context_helper.gs.lua |
one flag: whether to show the welcome screen after initNewGameFromMap |
gui/hud/ |
cargo_flow.gs.lua |
cargo loaded and unloaded, for the HUD display |
gui/hud/ |
greenification_display.gs.lua |
HUD display of greened industries |
gui/hud/ |
happy_change_display.gs.lua |
HUD display of changes in happiness (OnHappyChange) |
gui/hud/ |
marketing_display.gs.lua |
HUD display of running marketing campaigns |
industries/ |
industry_workers.gs.lua |
industry productivity boost from arriving workers |
landmarks/ |
landmarks.gs.lua |
landmark progress from delivered cargo (ItemsAboutToBeConsumed) |
mission/ |
mission.gs.lua |
the mission script, mission_sim.script with gameScriptMode = "Mission" |
mission/ |
guide_system.gs.lua |
the same script with gameScriptMode = "GuideSystem", for the guide system |
terrain/ |
reforestation.gs.lua |
regrowing trees, when advancedOptions.isReforestationActive is set |
vehicle/ |
vehicle_modifier.gs.lua |
speed, noise, pollution and comfort modifiers on vehicles (ChangeModifier) |
DLC fun_elements/ |
balloon.gs.lua |
the hot air balloon (spawnBalloon) and its notification |
Many of these scripts list console commands in a comment at the top, for example api.cmd.sendCommand(api.cmd.makeScriptingSendEventCmd("", "Subvention", "_debugSpawn", { })) in subventions.script.tl. These are a quick way to try a system in the in-game console.
Notifications¶
A notification type is two files with the same base name: a generic resource of type notification and a .script.tl that returns a NotificationType. The game has 33 of them. 21 are in game_mechanics/notifications/types/, the others next to the system that sends them (company/, landmarks/, towns/, mission/); mission 05 adds three and the deluxe DLC one.
The deluxe DLC's balloon is a complete example. The resource, dlcs/urbangames_deluxe_upgrade_pack/content/fun_elements/fun_elements/balloon_notification.res.lua:
function data()
return {
type = "notification",
data = {
guiType = "Info",
label = _("Hot Air Balloon Sighting"),
initiallyIgnoredType = false,
},
}
end
| Field | Meaning |
|---|---|
guiType |
"Info", "Caution", "Problem", "Opportunity" or "Achievement" (NotificationGuiData.Type); a missing resource counts as "Unknown" |
label |
name of the type in the filter of the Notifications log (notification_log.tl) |
initiallyIgnoredType |
true makes the type start out ignored in a new game |
simUpdateScript |
optional, "<file>@updateData": a NotificationSimType function that the notifications game script calls in the engine state; its result is stored as the notification's simParams |
industry_close.res.lua is a base example with simUpdateScript = "::/game_mechanics/notifications/types/industry_close.sim.script@updateData"; its updateData remembers the industry's name while the industry still exists. Mission 05's resources have only guiType.
The script, shortened from balloon_notification.script.tl in the same folder:
local data : NotificationType<BalloonSpottedParam> = {}
data.useDataState = function(notificationParam : BalloonSpottedParam) : NotificationGuiData
local nameState = engine_react_util.useStepStateTimer(function() : string
-- ... the name of notificationParam.townEntity ...
end)
return {
title = _("Hot Air Balloon Sighting"),
description = lang_util.format(_("People report that they have spotted a hot air balloon near {townName}"), {
townName = nameState:old()
}),
icon = resolve("vehicle_balloon.tga"),
wouldClick = notification_util.makeDefaultWouldClick({notificationParam.townEntity}),
onClick = notification_util.makeDefaultOnClick({notificationParam.townEntity}),
}
end
return data
useDataState(params, simParams) runs in the GUI as part of a recipe and returns a NotificationGuiData: title, text, icon, and optionally a progress bar, a HUD icon, sounds and the click handlers. The comment in the definition says to use it as the last state in a recipe and with no other dependent state.
To show a notification, send the event Notifications/add. From balloon.script.tl:
api.cmd.sendCommand(api.cmd.makeScriptingSendEventCmd("", "Notifications", "add", {
type = resolve("balloon_notification.script"),
params = {
entity = entity_util.makeEntityAndRevision0(balloon.entity),
townEntity = entity_util.makeEntityAndRevision0(townEntity),
},
autoDismissDuration = notification_util.defaultAutoDismissDurationMs,
}))
type is the game path of the script without .lua. notification_util.tl finds the resource by replacing .script with .res, which is why both files need the same base name. params arrive in useDataState. autoDismissDuration hides the notification after that many milliseconds (60000 by default); according to the comment in Notification that is not the same as dismissing it. The fields are in Notification.
The notifications game script drops add events while it is paused. It starts paused when a mission begins (initMission), and the mission game script sends Notifications/pause with true and then with false in its first two updates. A notification sent before that is lost. Missions can also hide types with notificationFilter (Missions), which the mission script passes on as updateIgnoredTypes.
Mission 05's drilling notification shows the persistent kind, which stays while a condition lasts and is bound to entities. mods/release/urbangames_campaign_mission_05/content/mission/mission/tasks/drilling/drilling.tl sends it with updatePersistent and removes it with removePersistent:
local persistentNotifications : PersistentNotifications = {
type = "urbangames_campaign_mission_05::/mission/tasks/drilling/mission_drilling_notification.script",
entitiesAndParam = {{
entities = entity_util.makeEntityAndRevision0Array(getEntititesForNotification(entity)),
param = notificationParams,
}},
passOnlyParam = true,
}
api.cmd.sendCommand(api.cmd.makeScriptingSendEventCmd("", "Notifications", "updatePersistent", persistentNotifications))
When the drilling ends, the task removes the persistent notification and sends a normal one with add. The records are PersistentNotifications and RemovePersistentNotifications; the helpers are in notification_util.
Subsidies¶
A subsidy type is a generic resource of type subvention that points at a script. The base game has four in game_mechanics/subventions/ (deliver_cargo, deliver_cargo_town, deliver_passengers, deliver_workers), and mission 06 adds deliver_visitors. From mods/release/urbangames_campaign_mission_06/content/subventions/subventions/deliver_visitors/deliver_visitors.res.lua:
function data()
return {
type = "subvention",
data = {
scriptFile = "urbangames_campaign_mission_06::/subventions/deliver_visitors/deliver_visitors.script@deliver_visitors",
icons = {
fallback = "urbangames_campaign_mission_06::/subventions/deliver_visitors/deliver_visitors.tga",
}
}
}
end
scriptFile names a table in the script file; the script ends with return { deliver_visitors = ret }. icons (SubventionIcons) can also have proposed, active, complete and failed; the shipped files only set fallback.
The table is a SubventionScript. The subsidies game script calls its functions by name:
| Function | Called |
|---|---|
canSpawn() |
to decide whether the type may spawn now; deliver_visitors returns true while a landmark has no subsidy |
onSpawn() |
for a random spawn; returns the subsidy data and a uid |
spawnSpecific(param) |
when a script asks for this type with given targets (see below) |
isComplete, onComplete, onFailure, onAccept, onUpdate, shouldDespawn, onDespawn |
while the subsidy is proposed or active |
eventsToHandle, handleEvent |
for game events the type needs, for example arrivals |
getNotificationParams, getNotificationData, getNotificationMissedData |
for the notifications about the subsidy |
getCardData, getRelevantEntities, getRelevantEntityNames |
for the subsidy window |
The data a type returns follows ISubvention.ISubventionData (ISubvention): upfront, complete and failure rewards or penalties (money, reputation, town experience), toDeliver, delivered and the durations.
The game script spawns subsidies at random. It collects every subvention resource with getAllOfType("subvention"), keeps those whose canSpawn() returns true and picks one. The interval depends on advancedOptions.subventionMode in the base config (0 turns subsidies off; 1, 2, 3 and 4 give about 12, 6, 3 and 1 months, up to twice as long at the highest company rank; subvention_util.tl) and doesn't run in the Map Editor. Mission 06 shows that a mod's type joins this pool, since deliver_visitors has a working canSpawn.
A script can also spawn one type on purpose. The base mission task subsidies (base/content/mission/mission/tasks/subsidies/subsidies.tl) does it like this, and mission 06 passes it deliver_visitors with a landmark as target:
api.cmd.sendCommand(api.cmd.makeScriptingSendEventCmd("", "Subvention", "subventionSpawn", { name = subsidy[1], specific = subsidy[2] }))
name is the resource ("urbangames_campaign_mission_06::/subventions/deliver_visitors/deliver_visitors.res"), specific the parameters for spawnSpecific; expireDurationProposed = -1 means the offer does not expire. The parameter records of the base types are in specific_subvention. The event Subvention/setPause with true or false stops and resumes random spawning.
Company perks¶
The Perks menu unlocks things by company rank. Each perk has a permit key, and rankAndPermits lists {rank, permits} pairs: at each rank the company gets that many more permits, counted together (company_util.tl). A permit is used up when the player builds or starts the perk. Landmarks and the headquarters are constructions with this metadata and are described under Landmarks. Three other kinds exist.
Explorations are generic resources of type company_exploration, 12 of them in game_mechanics/company/explorations/. From exploration_clay.res.lua:
function data()
return {
type = "company_exploration",
data = {
cargoType = "::/cargos/clay/clay.cargo",
icon = "game_mechanics/company/explorations/exploration_clay.tga",
previewIcon = nil,
lockedIcon = "game_mechanics/company/explorations/exploration_clay_locked.tga",
rankAndPermits = {
{3, 1},
},
requiredTags = {
"OUTPUT_CLAY",
},
menuCategory = {
categories = {
{
category = "prospections",
order = 2000,
},
},
},
}
}
end
The build menu (construction_react_util.tl) offers an exploration only if an industry in the current economy has every tag in requiredTags in its placementParams tags. The exploration then works as a perk of category PROSPECTION, and exploration_plane.gs.lua flies the Prospection Plane. Instead of its own rankAndPermits, an exploration can share a permit key: exploration_grain.res.lua and exploration_vegetables.res.lua set permitKey = "::/game_mechanics/company/permitKeys/permit_key_farm.res". The fields are in Exploration.
Permit keys are generic resources of type company_permit_key, three of them in game_mechanics/company/permitKeys/. A key gives several perks one shared count of permits. From marketing.res.lua:
function data()
return {
type = "company_permit_key",
data = {
name = _("Marketing"),
rankAndPermits = {{4,1}},
cooldownInSeconds = 5 * 4 * 365.25, -- 5 years
}
}
end
hq.res.lua (name "Headquarters", {{1,1}}) is the key of landmarks/hq/hq/headquarter.con.lua, marketing.res.lua that of the Marketing Campaign tool, permit_key_farm.res.lua (name "Industry") that of the two farm explorations. cooldownInSeconds makes a used permit come back after that time. A perk names its key with permitKey in the company metadata; without one, the perk's own resource name is its key (PermitKeyDesc).
Mechanics perks are tools or constructions with perkCategory = "MECHANICS" in their company metadata (CompanyMetadata.ConstructionDesc). The Cleaning Plant is a construction, from game_mechanics/game_mechanics/emission/pollution_cleanup_facility.con.lua:
The Industry Greenification and Marketing Campaign tools (gui/gui/construction/tools/industry_greenify_tool.script.tl, marketing_campaign_tool.script.tl) are construction tools that set the same metadata with company_metadata.constructionDesc.make{ perkCategory = "MECHANICS", ... }. The Industry Greenification tool uses a plain string as its key (permitKey = "ECO_INDUSTRY") and gains a permit at ranks 3, 6, 9, 12 and 15.
The base game collects explorations and permit keys with getAllOfType, so a mod's resource of the same type should appear next to the base ones. No shipped mod adds one, so this is untested.
Economies¶
An economy (.eco.lua) decides which industries and cargo types a game uses and how the map generator places industries. The base game has five in base/content/economy/economy/: all, temperate, dry, tropic and subarctic. Missions 02, 04, 06, 07 and 08 ship their own. A climate names its default economy in its economy field (Climates); the economy chosen for the game ends up in baseConfig.economyId (base/content/base/base/mod.script.tl). Scripts read it with api.res.economyRep.get(api.res.economyRep.find(api.res.getBaseConfig().economyId)).
The end of base/content/economy/economy/temperate.eco.lua:
return {
desc = {
name = _("Temperate"),
icon = "temperate.tga"
},
category = "temperate.eco",
cargoCategories = cargoCategories,
waterIndustryFraction = 6.0 / 41.0,
placementParams = placementParams,
cargoRequiredIndustries = cargoRequiredIndustries,
companyRankRequiredIndustries = companyRankRequiredIndustries,
townLevels2CargoInfos = placementparamsutil.getDefaultTownLevels2CargoInfos(),
defaultIndustryProductivity = 0.58,
industryPlacementFallbacks = {
oil_platform = "oil_well",
sand_excavator = "sand_pit",
},
}
| Field | Meaning |
|---|---|
desc, order, visible |
name and icon, sort order and whether the economy can be chosen in the menu; mission 04's economy sets visible = false |
category |
the tag that industries and cargo types list in their categoryList to exist in this economy |
placementParams |
one entry per industry, keyed by the industry's placementTag (see below) |
cargoCategories |
one entry per cargo category: tier, landUses, requiredIndustryTags, townConsumptionFactor |
townLevels2CargoInfos |
per town level, the cargo tiers and land use that towns start to demand |
cargoRequiredIndustries |
per cargo, the industries of its supply chain with a factor each |
companyRankRequiredIndustries |
per company rank, industries that the rank needs on the map |
industryPlacementFallbacks |
industry to place instead when one can't be placed, here a land oil well for the oil platform |
waterIndustryFraction, defaultIndustryProductivity |
share of industries on water, starting productivity |
The meaning of the last five rows comes from the field names and values; the definition (EconomyDesc) has comments only for desc, order, visible and category.
placementParams comes from placementparamsutil.getAllParams() in base/content/economy/economy/placementparamsutil.lua. Its first entry:
brewery = {
buildOrder = 190,
initWeight = 10,
tags = { "INPUT_GRAIN", "INPUT_GLASS" },
distanceWeights = {
TOWN = CLOSE,
},
weights = {
biome0 = BIOME_VERY_FAR,
biome1 = BIOME_CLOSE,
biome2 = BIOME_CLOSE,
biome3 = BIOME_CLOSE,
biome4 = BIOME_VERY_FAR,
},
requiredIndustryTags = {"glass_works", "farm"},
tier = 1,
},
The key brewery is the placementTag of industries/brewery/brewery/brewery.con.lua (Industries). tags describe what the industry takes and gives (INPUT_*, OUTPUT_*); explorations match against them. distanceWeights pull the industry towards towns or tagged industries (positive) or push it away (negative); the file defines VERY_CLOSE = 2 down to VERY_FAR = -2. weights rate the biome masks biome0 to biome4, from BIOME_BEST = -8000 to BIOME_VERY_FAR = 4000. requiredIndustryTags names the industries that must exist for this one, here its suppliers. buildOrder and initWeight set the order and the starting weight of placement; allowedOnLand and allowedOnWater mark water industries such as oil_platform. What tier does is not documented. The record is PlacementParams.
Each economy starts from the full list and removes what it doesn't use. From temperate.eco.lua:
local placementParams = placementparamsutil.getAllParams()
placementParams.cement_plant = nil
placementParams.paper_mill = nil
-- ...
local cargoCategories = placementparamsutil.getCargoCategories()
cargoCategories.cement = nil
cargoCategories.books = nil
-- ...
placementparamsutil.removeTags(placementParams, {"OUTPUT_RUBBER"})
Removing an entry keeps the map generator from placing that industry. Which constructions and cargo types the player sees is decided separately: the base game's postRunFn hides every industry and cargo type whose categoryList doesn't contain the economy's category, except in the all economy (mod.script.tl). A cargo type's categoryList is described under Cargo types.
A new industry needs both: its .con.lua with isIndustry = true, a placementTag and the economy categories in categoryList, and a placementParams entry under that tag. Without the entry the industry exists but the economy doesn't place it; mission 04's pharmacy.con.lua has placementTag = "pharmacy" and no entry in the mission's economy. No shipped mod adds an entry to a base economy. Judging by the file shape, a loadEconomy modifier in runFn should work; this sketch is not tested:
function data()
return {
runFn = function(captureParams, configDict, allModParams)
addModifier("loadEconomy", function(fileName, data)
if data.category == "temperate.eco" and data.placementParams then
data.placementParams.jdoe_cider_press = {
buildOrder = 195,
initWeight = 10,
tags = { "INPUT_FRUIT" },
distanceWeights = { TOWN = 1 },
requiredIndustryTags = { "farm" },
tier = 1,
}
end
return data
end)
end,
}
end
Mission 04's own economy keeps category = "temperate.eco", so the base industries and cargo types that list this category stay visible in it. A mod that ships a whole economy of its own, as the missions do, can start from placementparamsutil.getAllParams() with require "::/economy/placementparamsutil.lua" and change the table. The repository is api.res.economyRep.
Official wiki: Climates, Construction types, Cargo.