Zum Inhalt

Missionen

Die Kampagne besteht aus Mods: urbangames_campaign legt die Kampagne fest, und jeder der Mods urbangames_campaign_mission_01 bis _08 ist eine Mission. Eine Mission ist ein Spielstand plus eine Reihe von Aufgaben. Die Aufgaben laufen im Missions-Framework des Basisspiels (base/content/mission/), einem Game-Script. Diese Seite geht Mission 01 durch: ihre Beschreibungsdatei, ihren Start, die Definition der Aufgaben und ein Aufgabenskript.

Ein Mod kann eine ganze Kampagne hinzufügen oder einzelne Missionen zu einer bestehenden. Beides erscheint im Hauptmenü unter „Kampagne“ (Campaign) (Wiki: Missions). Laut Wiki besteht eine Mission aus einem steuernden Game-Script und dem Spielstand, auf dem sie spielt.

Die Teile eines Missions-Mods

Datei in mods/release/urbangames_campaign_mission_01/ Aufgabe
mod.json "visible": false, "cosmetic": true, Pre-, Run- und Post-Skripte in mod.script
content/savegames/savegame.sav der Spielstand, auf dem die Mission spielt (savegame = "/savegames/savegame.sav" in info.mission.lua; siehe Der Spielstand)
content/info.mission.lua die Missionsbeschreibung: Name, Bilder, Sterne, Medaillen, Figuren, Musik, Kampagne
content/mod.script.tl preRunFn und postRunFn passen BaseConfig und Ressourcen für diese Mission an
content/mission/mission/mission.res.lua registriert die Startfunktion der Mission (missiontask_boot)
content/mission/mission/mission.script.lua macht das Missionsmodul für Skriptverweise verfügbar
content/mission/mission/mission.tl baut das Modul mit mission_boot_util.makeMission
content/mission/mission/mission_story.tl definiert jede Aufgabe über ihren Schlüssel (3.019 Zeilen)
content/mission/mission/mission_params_01.tl Entitäts-IDs, Zonen und andere Konstanten des Spielstands
content/mission/mission/tasks/everything/everything.tl ein Aufgabenskript, das eigens für diese Mission geschrieben ist
content/mission/mission/keyframes/languages.script.tl das languageScript: pro Sprache, welche Keyframe-Datei jeder Dialog und jede Zwischensequenz verwendet
content/mission/mission/keyframes/en/*.lua, de/*.lua Keyframes für Dialoge und Zwischensequenzen (Porträts, Untertitel, Sprachausgabe, Kamera) pro Sprache
content/mission/mission/hud_replacement.* Ersatz für ein GUI-Recipe (siehe GUI)
content/init/init/init.gs.lua ein Game-Script, das bei initMission den Spielstand nachbessert
content/guide_system/guide_system/guide_system.res.lua registriert den Spielführer der Mission (guide_system_boot)
tealdef/mission/mission_params_01.d.tl der Teal-Typ der Parametertabelle

Das Wiki nennt als Mindestausstattung eines Missionsordners info.mission.lua, savegame.sav, mission.res.lua, mission.script.lua, mission.tl und mission_story.tl. Alles andere in der Tabelle gibt es nur bei den offiziellen Missionen.

Die offiziellen Missions-Mods setzen in mod.json "visible": false. Warum, erklärt die Wiki-Seite zur Mod-Definition: Ein versteckter Mod erscheint nicht in der Mod-Liste. Das passt zu Mods, die nur Inhalte für einen Kampagnen-Spielstand liefern und nicht im Freien Spiel verwendet werden sollen. Mit "cosmetic": true bleiben Erfolge verfügbar. Ob ein Mod die Simulation beeinflusst, muss laut Wiki der Modder selbst beurteilen. Die übrigen Felder von mod.json stehen unter Aufbau eines Mods.

Die Beschreibungsdateien

Die Kampagne selbst ist eine einzige Datei, mods/release/urbangames_campaign/content/info.campaign.lua:

function data()
return {
    name = _("CAMPAIGN_NAME"),
    icon = "campaign.tga",
    description = _("CAMPAIGN_DESC"),
    order = 1,
    unlockingProgression = true,
}
end

Laut Wiki braucht die Kampagnendatei die Endung .campaign.lua und kann in einem der Missions-Mods oder in einem eigenen Mod liegen. Die offizielle Kampagne hat einen eigenen Mod, urbangames_campaign. Ihre Felder, wie das Wiki sie beschreibt:

Feld Bedeutung
name, description Titel und Text im Auswahlmenü der Kampagnen
icon Bildschaltfläche zur Auswahl der Kampagne; die doppelt große <name>@2x.tga sollte 675 × 1080 Pixel haben
order Position im Kampagnenmenü; die offizielle Kampagne hat 1
unlockingProgression false schaltet alle Missionen frei, true schaltet sie nacheinander frei

Bei unlockingProgression = true wird laut Wiki die nächste Mission freigeschaltet, sobald der Spieler mindestens einen Stern verdient hat. Der Code des Basisspiels prüft stattdessen den Abschluss: base/content/gui/gui/menu/savegame_react_util.tl gibt eine Mission frei, wenn index <= highestUnlocked + 1 gilt, wobei highestUnlocked die letzte Mission ist, für die app.getUserProfile():isMissionCompleted(campaign, missionName) wahr ist. Ob „abgeschlossen“ dasselbe heißt wie „mindestens ein Stern“, geht aus den Dateien nicht hervor.

Jede Mission verweist in ihrer info.mission.lua mit campaign = "urbangames_campaign::/info.campaign" auf die Kampagne. Aus Mission 01 (gekürzt):

function data()
    return {
        name = _("MISSION_01_NAME"),
        description = _("MISSION_01_DESC"),
        image = "/gui/mission/m01_preview.tga",
        languageScript = "urbangames_campaign_mission_01::/mission/keyframes/languages.script@get",
        loadscreen = "/gui/mission/m01_loadscreen.tga",
        savegame = "/savegames/savegame.sav",
        stars = {
            {
                id = "MISSION_01_STAR_1",
                name = _("MISSION_01_STAR_1"),
                iconLocked = "::/gui/menu/icons/star_1.tga",
                iconCompleted = "::/gui/menu/icons/star_1.tga",
            },
            -- STAR_2, STAR_3
        },
        medals = {
            { id = "MISSION_01_MEDAL_1", name = _("MISSION_01_MEDAL"), nameLocked = _("MISSION_01_MEDAL_LOCKED"), ... },
        },
        characters = {
            { name = _("MISSION_01_CHARACTER_1_NAME"), info = _("MISSION_01_CHARACTER_1_INFO"), portrait = "urbangames_campaign_mission_01::/mission/dialogue/major_neutral.tga", },
            -- ...
        },
        location = _("MISSION_01_LOCATION"),
        year = 1906,
        campaign = "urbangames_campaign::/info.campaign",
        order = 1,
        musicTracks = {
            { "urbangames_campaign_mission_01::/audio/music/m1_intro.ogg", false },
            { "urbangames_campaign_mission_01::/audio/music/m1_theme_chapter_1.ogg", true },
        },
    }
end

Mission 01 setzt außerdem imagePreview, imageSuccess und imageFailure (Bilder für das Endfenster), loadscreenDescription und loadscreenVoiceOver (Text und Sprachausgabe auf dem Ladebildschirm) sowie starsDescription. Drei Felder hebt das Wiki eigens hervor: Jede Mission muss ihre campaign nennen, stars braucht es, wenn die Kampagne den Fortschritt verfolgen soll, und languageScript ist für Dialogaufgaben nötig, mit mindestens einer Sprache (siehe Übersetzungen).

Die Felder entsprechen MissionDesc und CampaignDesc in api/tealdef/api/type.d.tl. Die Anwendung hält sie in app.res.campaignRep und app.res.missionRep, und app.loadMission(saveId, campaign, mission) startet eine Mission (App).

Der Spielstand

Das Wiki empfiehlt folgendes Vorgehen: Starte ein neues Spiel oder lade ein bestehendes, während der Missions-Mod aktiv ist, damit die Game-Scripts der Mission geladen sind. Dann speichere und kopiere den Spielstand in den Missions-Mod. Alle acht offiziellen Missionen verweisen auf savegame = "/savegames/savegame.sav". Ein Pfad, der mit / beginnt, zeigt in den content/-Ordner des Mods (image = "/gui/mission/m01_preview.tga" aus Mission 01 ist content/gui/mission/m01_preview.tga). Die Datei gehört also nach content/savegames/savegame.sav. In den entpackten offiziellen Mods fehlt sie, und ihre _content.json führt sie nicht auf. Die Aufgaben sprechen Entitäten dieses Spielstands über ihre ID an (siehe Missionsparameter).

Übersetzungen

Missionstexte gibt es in zwei Arten.

Strings in _(), etwa _("MISSION_01_NAME") oder die Aufgabennamen in mission_story.tl, sind gewöhnliche Übersetzungsschlüssel. Wie ein Mod sie übersetzt, steht unter Übersetzungen in einem Mod.

Dialoge und Zwischensequenzen sind Keyframe-Dateien, und die gibt es pro Sprache einmal, weil sich das Timing der Untertitel nach der Sprachausgabe richtet. Das languageScript in info.mission.lua gibt eine Tabelle zurück, die jedem Sprachcode eine Zuordnung von Skriptname zu Keyframe-Datei zuweist. Aus content/mission/mission/keyframes/languages.script.tl (je ein Eintrag):

local function data() : any
return {
    ["en"] = {
        ["urbangames_campaign_mission_01::/mission/keyframes/MISSION_01_CUTSCENE_INTRO.script@get"] = "urbangames_campaign_mission_01::/mission/keyframes/en/MISSION_01_CUTSCENE_INTRO.lua",
        -- ...
    },
    ["de"] = {
        ["urbangames_campaign_mission_01::/mission/keyframes/MISSION_01_CUTSCENE_INTRO.script@get"] = "urbangames_campaign_mission_01::/mission/keyframes/de/MISSION_01_CUTSCENE_INTRO.lua",
        -- ...
    },
}
end
return { get = data() }

Eine Keyframe-Datei gibt camera, environmentParams, fades, music, portraits, subtitles und voiceOvers zurück. Die Untertitel sind wieder _()-Schlüssel. Die englische und die deutsche Fassung von MISSION_01_CUTSCENE_BRIDGE_COMPLETE.lua unterscheiden sich nur in den Zeitangaben. base/content/mission/mission/mission_react_util.tl wählt die Tabelle für app.getUserProfile():getLanguage().code und fällt auf "en" zurück. Fehlt languageScript, schreibt die Datei einen Fehler ins Log und greift auf die alte Dialogbehandlung zurück.

So startet eine Mission

Das Missionsskript des Basisspiels ist ein Game-Script, base/content/mission/mission/mission.gs.lua. Alle seine Funktionen liegen in mission_sim.script und laufen mit params = { gameScriptMode = "Mission" }. Beim Start sucht es nach Boot-Ressourcen. Aus base/content/mission/mission/mission_sim.script.tl:

if gameScriptMode == "Mission" then
    local missionTaskBoot = api.res.genericRep.getAllOfType("missiontask_boot")
    if #missionTaskBoot > 0 then
        for __, missionTaskId in ipairs(missionTaskBoot) do
            subscribeIfNeeded()
            registerTask(missionTaskId)
        end
    else
        for __, missionTaskId in ipairs(api.res.genericRep.getAllOfType("tutorial_boot")) do
            subscribeIfNeeded()
            registerTask(missionTaskId)
        end
    end
elseif gameScriptMode == "GuideSystem" then
    for __, guideId in ipairs(api.res.genericRep.getAllOfType("guide_system_boot")) do
        subscribeIfNeeded()
        registerTask(guideId)
    end
end

registerTask liest die bootstrapScriptFn der Ressource (MissionInterface.BootstrapDesc), ruft sie einmal auf und merkt sie sich in state.bootTypes. Mission 01 registriert ihre Startfunktion in content/mission/mission/mission.res.lua:

function data()
return {
    type = "missiontask_boot",
    data = {
        bootstrapScriptFn = "urbangames_campaign_mission_01::/mission/mission.script@mission.spawnBootTasks",
        bootstrapScriptFnParams = nil,
    }

}
end

mission.script.lua stellt das Modul unter dem Namen mission bereit. Deshalb endet der Verweis auf @mission.spawnBootTasks:

local mission = require "mission.tl"

function data()
return {
    mission = mission
}
end

mission.tl baut dieses Modul:

local mission_boot_util = ug_require "::/mission/mission_boot_util.tl" as MissionBootUtil
local taskDataFactory = ug_require "mission_story.tl" as function(taskKey : string) : MissionTaskUtil.StaticTaskInfo

return mission_boot_util.makeMission({
    scriptKey = "urbangames_campaign_mission_01::/mission/mission.script",
    taskDataFactory = taskDataFactory,
    bootTasks = {
        { taskKey = "check_entities" },
        { taskKey = "everything" },
        { taskKey = "intro" },
        { taskKey = "disableMostUiStuff" },
        { taskKey = "intro_cutscene" },
        { taskKey = "non_bulldozable" },
        { taskKey = "transfer_ownership" },
        { taskKey = "town_development" },
        { taskKey = "task_1_spawn_alligators" },
        { taskKey = "industry_activity_1" },
        { taskKey = "protect_important_entities" },
    },
})

MissionBootUtil.makeMission gibt ein Modul mit spawnBootTasks und getVTable zurück. In base/content/mission/mission/mission_boot_util.tl fügt spawnBootTasks die bootTasks über missionApi.addTask hinzu, jede mit einer Aufgabenkonfiguration. Deren getVTableScriptFn ist scriptKey .. "@mission.getVTable", und ihr uniqueKey kommt aus mission_task_util.makeTaskKey(key, scriptKey). Später ruft das Framework getVTable(taskKey) auf, um Skript und Parameter einer Aufgabe aus taskDataFactory zu holen.

Der Spielführer funktioniert genauso, mit makeGuide, dem Ressourcentyp guide_system_boot und dem Modus GuideSystem.

Aufgaben definieren

mission_story.tl gibt eine Funktion zurück, die einem Aufgabenschlüssel eine MissionTaskUtil.StaticTaskInfo zuordnet. Jeder Zweig wählt ein Aufgabenskript aus der Bibliothek des Basisspiels und konfiguriert es. Ein typisches Beispiel ist die Aufgabe, in der der Spieler ein Straßendepot bauen soll (gekürzt):

if taskKey == "buildStreetDepot" then
    return {
        taskScript = build_construction as MissionInterface.TaskScript<any, any>,
        taskParams : MissionTaskBuildConstruction.Params = {
            zones = {{
                key = "buildStreetDepot",
                polygon = polygon_util.transform(api.type.Mat4f.rotZTransl(params.streetDepotAngle, api.type.Vec3f.new(params.streetDepotZone.pos[1], params.streetDepotZone.pos[2], 0)), mission_outline.streetDepotOutline()),
                draw = true,
                playAnimation = true;
            }},
            initialAngle = params.streetDepotAngle,
            proposalHandler = build_construction_util.makeProposalHandlerFilterApply({
                    checkConstruction = build_construction_util.makeCheckConstruction("::/depots/road/road_depot/road_depot.con", nil, { error = _("MISSION_PROPOSAL_FEEDBACK_NOT_A_ROAD_DEPOT") }),
                    checkLocation = build_construction_util.makeCheckLocation(params.streetDepotZone.pos[1], params.streetDepotZone.pos[2], 20, { error = _("MISSION_PROPOSAL_FEEDBACK_NOT_IN_OUTLINED_AREA") }),
                    checkSnapping = build_construction_util.makeCheckSnapping({}, { error = _("MISSION_PROPOSAL_FEEDBACK_NOT_CONNECTED_TO_STREET") }),
                }, true),
            },
        showInHistory = true,
        taskInfo = {
            name = _("MISSION_01_TASK_BUILD_STREET_FIRST_DEPOT_HOTEL_NAME"),
            paragraphs = {
                { text = _("MISSION_01_TASK_BUILD_STREET_FIRST_DEPOT_HOTEL_PARAGRAPH") },
            },
            instructionData = {
                subtitle = _("MISSION_01_TASK_BUILD_STREET_FIRST_DEPOT_HOTEL_INSTRUCTION"),
                portrait = "urbangames_campaign_mission_01::/mission/dialogue/andrew_neutral.tga",
                voiceOver = "urbangames_campaign_mission_01::/audio/voice_over/MISSION_01_TASK_BUILD_STREET_FIRST_DEPOT_HOTEL_INSTRUCTION.wav",
                duration = 7,
            },
        },
        enabled = { "menu.construction.road", },
        highlightRules = { {"menu.construction.road", "Circle"}, },
        followup = {
            { taskKey = "buyTruck" },
            { taskKey = "highlightDepotBuy" },
            { taskKey = "buy_vehicle_guide" },
        },
        menuFilter = {
            enabledItems = {
                "::/depots/road/road_depot/road_depot.con@0",
                "::/infrastructure/street/country/country_old_small.street_template"
            },
            disabledItems = {},
        },
    }
end

Die Felder lassen sich in vier Gruppen teilen:

Gruppe Felder in dieser Aufgabe Bedeutung
Die Aufgabe taskScript, taskParams welches Aufgabenskript läuft und mit welchen eigenen Parametern (hier MissionTaskBuildConstruction.Params)
Darstellung taskInfo (name, paragraphs, instructionData), showInHistory Text im Aufgabenfenster, Sprachausgabe und Porträt; MissionInterface.TaskInfo
Einschränkungen, solange die Aufgabe aktiv ist enabled, highlightRules, menuFilter, vehicleFilter, notificationFilter, disableFeatures welche Bedienelemente und Einträge im Baumenü verfügbar oder hervorgehoben sind; notificationFilter nennt Typen von Benachrichtigungen (über den Skriptpfad, etwa "::/landmarks/landmarks_notification.script" in Mission 05), die das Missionsskript dem Benachrichtigungsskript als ignorierte Typen meldet
Ablauf followup Aufgaben, die hinzukommen, wenn diese abgeschlossen ist; jeder Eintrag kann eine condition haben

Wenn eine Aufgabe abgeschlossen ist, ruft das Framework onComplete der Vtable auf, und das startet in mission_boot_util.tl die followup-Aufgaben. Die Mission ist ein Graph aus Aufgabenschlüsseln: intro führt zu findAlligators, buildStreetDepot zu buyTruck und so weiter, bis missionEnd die Aufgabe finish verwendet. mission_story.tl endet mit einer Warnung für unbekannte Schlüssel:

    log.warning("task not found! taskKey = " .. taskKey)
    return nil
end

Verzweigen per Schaltfläche

TaskInfo.options fügt dem Missionsfenster Schaltflächen hinzu, und followup-Einträge können eine condition tragen. Zusammen verzweigen sie den Graphen. Das Beispiel im Wiki verwendet die Aufgabe page, die keine eigene Logik hat (gekürzt):

if taskKey == "branching_point" then
    return {
        taskScript = page as MissionInterface.TaskScript<any, any>,
        taskInfo = {
            name = _("Branching point"),
            paragraphs = { { text = _("Make your decision") } },
            options = {
                { text = _("Choose A"), key = "finish-a" },
                { text = _("Choose B"), key = "finish-b" },
            },
        },
        followup = {
            { taskKey = "a", condition = function(ctx : MissionInterface.TaskContext<any, any>) : boolean
                return ctx.genericState.optionDecision == "finish-a" end },
            { taskKey = "b", condition = function(ctx : MissionInterface.TaskContext<any, any>) : boolean
                return ctx.genericState.optionDecision == "finish-b" end },
        },
    }
end

Ein Klick auf eine Schaltfläche speichert ihren key in genericState.optionDecision. page.tl definiert kein isComplete. mission_sim.script.tl schließt jede Aufgabe ab, deren optionDecision "finish" ist oder mit "finish" beginnt, und deshalb fangen beide Schlüssel oben damit an. Bei paragraphs ist laut Wiki der erste Eintrag die Aufgabenbeschreibung, die weiteren sind Hinweise.

Die Mission beenden

Eine Mission endet mit der Aufgabe finish. Ohne Parameter ist sie ein Erfolg, mit taskParams = { success = false } ein Fehlschlag:

if taskKey == "failure" then
    return { taskScript = finish as MissionInterface.TaskScript<any, any>, taskParams = { success = false } }
end
if taskKey == "success" then
    return { taskScript = finish as MissionInterface.TaskScript<any, any> }
end

finish.tl setzt success in onSpawn standardmäßig auf true und wechselt in den Zustand Complete, wenn das Endfenster completeMission sendet.

Weitere Aufgabenfelder

StaticTaskInfo hat mehr Optionen, als diese Aufgabe nutzt: stars und medals (welche Sterne eine Aufgabe vergibt), isBonusTask, financialReward, requiredTaskToComplete, allowSimultaneousUiTasks, allowBuyVehicles, lockedVehicles, spawnGuide, soundtrack und weitere. In mission_sim.script.tl bucht ein financialReward Geld mit makeJournalBookAssetCmd und schickt eine Benachrichtigung.

Aufgabenskripte

Ein Aufgabenskript implementiert MissionInterface.TaskScript<State, Params>. Das Framework ruft nur die Callbacks auf, die ein Skript definiert (mission_sim.script.tl prüft zum Beispiel if scriptOnComplete then). Auf der Engine-Seite sind das onSpawn, onStart, isComplete, onComplete, onUpdate, handleEvent und handleLegacy, auf der GUI-Seite guiUpdate, guiHandleEvent, guiHandleProposal und onGuiKickOff. getInfo, getMarkers, getZones, getNonBulldozableEntities, getProtectedEntities und die Getter für Regeln beschreiben die Aufgabe für die Oberfläche. Das Wiki sagt, in welchem Zustand jede Seite läuft: onUpdate und handleEvent laufen im Engine-Zustand (das Wiki nennt ihn Engine-Thread), wo Engine-Funktionen arbeiten und api.gui nicht verfügbar ist. guiUpdate und guiHandleEvent laufen im GUI-Zustand, wo es umgekehrt ist (siehe Scripting). onUpdate läuft in jedem Simulationsschritt, guiUpdate in jedem Frame. Die Callbacks bekommen einen TaskContext mit dem state der Aufgabe, ihren params, dem genericState und guiTimeSeconds.

Die Aufgabe wait ist ein kurzes, vollständiges Beispiel. Aus base/content/mission/mission/tasks/wait/wait.tl (ohne handleLegacy):

local type S = MissionTaskWait.State
local type P = MissionTaskWait.Params

local ret : MissionTaskWait = {

    taskScriptName = "wait",

    onSpawn = function(params : P, guiTimeSeconds : number) : S
        local result : S = {}
        if params.simTimeSeconds then
            local gt : Engine.Component.GameTime = api.engine.getComponent(api.engine.util.getWorld(), api.type.ComponentType.GAME_TIME)
            local currentGameTimeMs : number = gt.gameTime
            result.endSimTimeMs = currentGameTimeMs + 1000 * params.simTimeSeconds
        end
        if params.guiTimeSeconds then
            result.endGuiTimeSeconds = guiTimeSeconds + params.guiTimeSeconds
        end
        return result
    end,

    isComplete = function(ctx : MissionInterface.TaskContext<S, P>) : boolean
        if ctx.params.guiTimeSeconds then
            return ctx.guiTimeSeconds >= ctx.state.endGuiTimeSeconds
        end
        if ctx.state.endSimTimeMs then
            local gt : Engine.Component.GameTime = api.engine.getComponent(api.engine.util.getWorld(), api.type.ComponentType.GAME_TIME)
            local currentGameTimeMs : number = gt.gameTime
            return currentGameTimeMs >= ctx.state.endSimTimeMs
        end
        return false
    end,
}

return ret

Ihr Typ ist in base/tealdef/mission/tasks/wait/mission_task_wait.d.tl deklariert, mit Kommentaren, die die beiden Zeitbasen unterscheiden:

global record MissionTaskWait is MissionInterface.TaskScript<MissionTaskWait.State, MissionTaskWait.Params>
    record Params
        simTimeSeconds : number -- in simulation time (affected by game speed)
        guiTimeSeconds : number -- in approximated wall clock time (not affected by game speed, except that it is delayed when the game lags)
    end
    record State
        endSimTimeMs : number
        endGuiTimeSeconds : number
    end
end

Über onSpawn heißt es in der Deklaration: „prepare[s] the initial state when attempting to spawn (return nil to prevent spawning)“. Die Funktion legt also den Anfangszustand an und kann den Start verhindern, indem sie nil zurückgibt. Nach jedem Update prüft mission_sim.script.tl isComplete(ctx). Eine Aufgabe lässt sich auch über eine Option mit dem Schlüssel "finish" beenden (Mission 01 fügt auf diesem Weg „Debug: Skip“-Optionen hinzu, wenn DEBUGMODE gesetzt ist).

Die Aufgabenbibliothek des Basisspiels

Das Wiki beschreibt Aufgaben als „triggered sequentially“, also nacheinander ausgelöst. Die offiziellen Missionen starten mehrere gleichzeitig: Mission 01 hat elf bootTasks, und buildStreetDepot oben hat drei followup-Einträge, die alle zusammen hinzukommen, wenn die Aufgabe abgeschlossen ist.

Die Aufgabenskripte unter base/content/mission/mission/tasks/ (Definitionen in base/tealdef/mission/tasks/, Referenz unter base/mission/tasks):

Ordner Aufgaben
build_construction, build_path, connect_nodes, bulldoze, auto_builder bauen, verbinden und abreißen, automatischer Gleisbau und Elektrifizierung
buy_vehicle (Params: carrier, cargoTypeRes, minCount, allowBuyVehicles), assign_vehicle, create_line, vehicle_util Fahrzeuge kaufen und klonen, Linien zuweisen, Linien anlegen, Fahrzeuge verkaufen, ersetzen, starten und ändern
transport_cargo, town_size, town_rating, subsidies Ziele: Güter oder Passagiere liefern, eine Stadt wachsen lassen, eine Bewertung erreichen, Subventionen erfüllen, die die Aufgabe selbst anlegt
dialogue, cutscene, page, guide, waypoints, observe_entity, click_entity, click_and_collect_custom_entity Darstellung und Interaktion
wait, join, finish, page Ablauf: auf Zeit oder ein Ereignis warten, auf mehrere Aufgaben warten, die Mission beenden, auf eine Schaltfläche warten
utility send_event, send_command, highlight, protected_entities, town_development, industry_activity, check_entities, store_any, store_entity, play_sound und weitere
non_bulldozable, transfer_ownership, spawn_sims Einrichtung des Spielstands

Eine eigene Aufgabe der Mission

Reicht die Bibliothek nicht, bringt eine Mission ihr eigenes Aufgabenskript mit. Die Aufgabe everything aus Mission 01 läuft während der ganzen Mission. In onStart benennt sie Entitäten um, setzt Datum und Kalendergeschwindigkeit, ersetzt das Geld des Spielers und leert den Vorrat eines Waldes. Aus content/mission/mission/tasks/everything/everything.tl (gekürzt):

local ret : MissionInterface.TaskScript<table, string> = {
    taskScriptName = "everything",

    isComplete = function(ctx : MissionInterface.TaskContext<table, string>) : boolean
        local waitForTask = mission_framework_util.findTask(ctx.params)
        return waitForTask and waitForTask.genericState and waitForTask.genericState.completedTime ~= nil
    end,

    onStart = function(_ctx : MissionInterface.TaskContext<table, string>)
        calendar.syncDate(params.missionStartYear)
        api.cmd.sendCommand(api.cmd.makeGameSetCalendarSpeedCmd(8000))

        local cmd = api.cmd.makeJournalClearAllCmd()
        api.cmd.sendCommand(cmd)

        local entry = api.type.JournalEntry.new()
        entry.amount = 10000000
        entry.time = -1
        entry.category.type = api.type.JournalEntry.Type.OTHER
        api.cmd.sendCommand(api.cmd.makeJournalBookAssetCmd(api.engine.util.getPlayer(), entry, api.type.Vec3f.new(0, 0, 0)))
        -- ...
    end,

    onUpdate = function()
        local gameTimeComponent : Engine.Component.GameTime
                = api.engine.getComponent(api.engine.util.getWorld(), api.type.ComponentType.GAME_TIME)
        local date = api.engine.util.getCalendarDate(gameTimeComponent.gameTime)
        if date.year == params.missionEndYear - 1 and date.month == 12 and date.day == 31 then
            api.cmd.sendCommand(api.cmd.makeGameSetCalendarSpeedCmd(0))
        end
    end,
    -- handleEvent, guiUpdate ...
}

Ihr Parameter ist der eindeutige Schlüssel der Aufgabe missionEnd (taskParams = mission_task_util.makeTaskKey("missionEnd", ...)), und so ist everything genau dann abgeschlossen, wenn die Mission es ist. Die Missionen 02, 03 und 05 deklarieren eigene Aufgabentypen in tealdef/mission/tasks/, zum Beispiel MissionTaskLineRate in Mission 02 mit Params { targetRate, lineEntity } und State { rate }.

Missionsparameter

Die Aufgaben sprechen den Inhalt des Spielstands über Entitäts-IDs an. mission_params_01.tl sammelt sie in einer typisierten Tabelle:

local missionStartYear = 1900
local missionEndYear = 1910

local t : MissionParams01 = {
    crocodileSound = "urbangames_campaign_mission_01::/audio/sound/mission01_alligator.wav",

    missionStartYear = missionStartYear,
    missionEndYear = missionEndYear,

    entityList_nonPlayerOwnedLines = {
        51165, -- tram-
        33105, -- tram-
        45546, -- ship-
        44294, -- train-
        1368, -- new orleans bus line-
    },
    -- ...
    entity_relevantShipyard = 2397,--
    entity_relevantHarbor = 25264,--

Die IDs gelten nur für den Spielstand dieser Mission. Deshalb bekommt die erste Boot-Aufgabe, check_entities, die ganze Tabelle (taskParams = { params = params }). Der Typ MissionParams01 steht in tealdef/mission/mission_params_01.d.tl und verwendet Framework-Typen wie MissionInterface.PosRadius für Zonen.

Missionsweite Einstellungen außerhalb der Aufgaben

Zwei weitere Mechanismen in Mission 01 wirken auf das ganze Spiel:

  • content/mod.script.tl setzt in preRunFn die Auswirkungen der Wartung auf null. In postRunFn schaltet es Subventionen ab, macht das Pferdefuhrwerk schneller, blendet den normalen Spielführer und die Ressourcen des Tutorials aus und versteckt alle Brückentypen außer der Bockbrücke (trestle).
  • content/init/init/init.gs.lua ist ein kleines Game-Script, dessen handleEvent auf initMission reagiert und mission_savegame_util.updateMaintenanceCost() und updateIndustriesPlayerOwned() aufruft.

Offizielles Wiki: Missions, Mod definition (visible, cosmetic, strings.json), Localizations.