Index: binaries/data/mods/mod/gui/common/modern/styles.xml
===================================================================
--- binaries/data/mods/mod/gui/common/modern/styles.xml
+++ binaries/data/mods/mod/gui/common/modern/styles.xml
@@ -30,7 +30,6 @@
scrollbar_style="ModernScrollBar"
sprite="ModernDarkBoxGoldNoTop"
sprite_selectarea="ModernDarkBoxWhite"
- sprite_heading="ModernDarkBoxGoldNoBottom"
textcolor="white"
textcolor_selected="white"
text_align="left"
Index: binaries/data/mods/public/campaigns/example.json
===================================================================
--- /dev/null
+++ binaries/data/mods/public/campaigns/example.json
@@ -0,0 +1,38 @@
+{
+ "Name": "Example Campaign",
+ "Description": "Lorem Ipsum and so on and so on",
+ "Interface": "default_menu",
+ "Levels": {
+ "Example_1": {
+ "Name": "Example 1",
+ "Map": "scenarios/Serengeti.xml",
+ "Description": "Whatever"
+ },
+ "Example_2": {
+ "Name": "Example 2",
+ "Map": "",
+ "Description": "None",
+ "Requires": "Example_1"
+ },
+ "Example_3": {
+ "Name": "This one requires 1 and 2",
+ "Map": "",
+ "Description": "None",
+ "Requires": "Example_1+Example_2"
+ },
+ "Example_4": {
+ "Name": "This one requires 2 or 3",
+ "Map": "",
+ "Description": "None",
+ "Requires": "Example_2 Example_3"
+ },
+ "Example_5": {
+ "Name": "This one unavailable if 1 isn't completed",
+ "Map": "",
+ "Description": "None",
+ "Requires": "!Example_1"
+ }
+ },
+ "Order": ["Example_1", "Example_2", "Example_3", "Example_4", "Example_5"],
+ "ShowUnavailable": true
+}
Index: binaries/data/mods/public/campaigns/tutorial.json
===================================================================
--- /dev/null
+++ binaries/data/mods/public/campaigns/tutorial.json
@@ -0,0 +1,21 @@
+{
+ "Name": "Tutorial",
+ "Description": "Learn how to play 0 A.D.",
+ "Image": "session/icons/mappreview/Introductory_Tutorial.png",
+ "Levels": {
+ "introduction": {
+ "Name": "Introductory Tutorial",
+ "Map": "tutorials/Introductory_Tutorial.xml",
+ "Description": "This is a basic tutorial to get you started playing 0 A.D.",
+ "Preview": "session/icons/mappreview/Introductory_Tutorial.png"
+ },
+ "eco_walkthrough": {
+ "Name": "Economy Walkthrough",
+ "Map": "tutorials/starting_economy_walkthrough.xml",
+ "Description": "This map will give a rough guide for starting the game effectively. Early in the game the most important thing is to gather resources as fast as possible so you are able to build enough troops later. Warning: This is very fast at the start, be prepared to run through the initial bit several times.",
+ "Requires": "introduction"
+ }
+ },
+ "Order": ["introduction", "eco_walkthrough"],
+ "ShowUnavailable": true
+}
Index: binaries/data/mods/public/gui/campaign/common/CampaignRun.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/common/CampaignRun.js
@@ -0,0 +1,97 @@
+/**
+ * A campaign "Run" saves metadata on a campaign progession.
+ * It is equivalent to a saved game for a game.
+ * It is named a "run" in an attempt to disambiguate with saved games from campaign runs,
+ * campaign templates, and the actual concept of a campaign at large.
+ */
+class CampaignRun
+{
+ static getCurrentRuns()
+ {
+ let names = Engine.ListDirectoryFiles("campaignsaves/", "*.0adcampaign", false);
+ return names.map(path => {
+ let filename = path.replace("campaignsaves/", "").replace(".0adcampaign", "");
+ return new CampaignRun(filename).load();
+ });
+ }
+
+ constructor(name = "")
+ {
+ this.filename = name;
+ this.meta = {};
+ this.data = {
+ "completed": []
+ };
+ this.template = null;
+ }
+
+ setTemplate(template)
+ {
+ this.template = template;
+ return this;
+ }
+
+ setMeta(description)
+ {
+ this.meta.userDescription = description;
+ return this;
+ }
+
+ markLevelComplete(levelID)
+ {
+ if (this.data.completed.indexOf(levelID) === -1)
+ this.data.completed.push(levelID);
+ this.save();
+ }
+
+ meetsRequirements(levelData)
+ {
+ if (!levelData.Requires)
+ return true;
+
+ if (!this.data.completed)
+ return false;
+
+ return MatchesClassList(this.data.completed, levelData.Requires);
+ }
+
+ getMenuPath()
+ {
+ return "campaign/" + this.template.interface + "/page.xml";
+ }
+
+ generateLabel()
+ {
+ return sprintf(translate("%(userDesc)s - %(templateName)s"), {
+ "userDesc": this.meta.userDescription,
+ "templateName": this.template.Name
+ });
+ }
+
+
+ load()
+ {
+ let data = Engine.ReadJSONFile("campaignsaves/" + this.filename + ".0adcampaign");
+ this.data = data.data;
+ this.meta = data.meta;
+ this.template = CampaignTemplate.getTemplate(data.template_identifier);
+ return this;
+ }
+
+ save()
+ {
+ let data = {
+ "data": this.data,
+ "meta": this.meta,
+ "template_identifier": this.template.identifier
+ };
+ Engine.WriteJSONFile("campaignsaves/" + this.filename + ".0adcampaign", data);
+ return this;
+ }
+
+ destroy()
+ {
+ Engine.DeleteFile("campaignsaves/" + this.filename + ".0adcampaign");
+ // hang in memory for a while.
+ }
+}
Index: binaries/data/mods/public/gui/campaign/common/CampaignTemplate.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/common/CampaignTemplate.js
@@ -0,0 +1,54 @@
+// TODO: replace this with a static member once we hit SM 75.
+var g_CachedTemplates;
+
+class CampaignTemplate
+{
+ /**
+ * @returns a dictionary of campaign templates, as [ { 'identifier': id, 'data': data }, ... ]
+ */
+ static getAvailableTemplates()
+ {
+ if (g_CachedTemplates)
+ return g_CachedTemplates;
+
+ let campaigns = Engine.ListDirectoryFiles("campaigns/", "*.json", false);
+
+ g_CachedTemplates = [];
+
+ for (let filename of campaigns)
+ // Use file name as identifier to guarantee unicity.
+ g_CachedTemplates.push(new CampaignTemplate(filename.slice("campaigns/".length, -".json".length)));
+
+ return g_CachedTemplates;
+ }
+
+ static getTemplate(identifier)
+ {
+ if (!g_CachedTemplates)
+ CampaignTemplate.getAvailableTemplates();
+ let temp = g_CachedTemplates.filter(t => t.identifier == identifier);
+ if (!temp.length)
+ return null;
+ return temp[0];
+ }
+
+ constructor(identifier)
+ {
+ Object.assign(this, Engine.ReadJSONFile("campaigns/" + identifier + ".json"));
+
+ this.identifier = identifier;
+
+ if (this.Interface)
+ this.interface = this.Interface;
+ else
+ this.interface = "default_menu";
+
+ if (!this.isValid())
+ throw ("Campaign template " + this.identifier + ".json is not a valid campaign template.");
+ }
+
+ isValid()
+ {
+ return this.Name;
+ }
+}
Index: binaries/data/mods/public/gui/campaign/common/utils.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/common/utils.js
@@ -0,0 +1,41 @@
+function _(obj)
+{
+ return Engine.GetGUIObjectByName(obj);
+}
+
+function _watch(object, callback)
+{
+ return new Proxy(object, {
+ "get": (obj, key) => {
+ return obj[key];
+ },
+ "set": (obj, key, value) => {
+ obj[key] = value;
+ callback();
+ return true;
+ }
+ });
+}
+
+class DefaultPage
+{
+ _watch(object, lambda = null)
+ {
+ if (lambda == null)
+ lambda = () => {
+ if (this._ready)
+ this.render();
+ };
+ return _watch(object, lambda);
+ }
+
+ constructor()
+ {
+ this._ready = false;
+ return this._watch(this);
+ }
+
+ render()
+ {
+ }
+}
Index: binaries/data/mods/public/gui/campaign/default_menu/CampaignMenu.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/default_menu/CampaignMenu.js
@@ -0,0 +1,132 @@
+class CampaignMenu extends DefaultPage
+{
+ constructor(campaignRun, finishedLevel, won)
+ {
+ super();
+
+ this.run = campaignRun;
+
+ this.selectedLevel = -1;
+ this.levelSelection = _("levelSelection");
+ this.levelSelection.onSelectionChange = () => { this.selectedLevel = this.levelSelection.selected; };
+
+ this.levelSelection.onMouseLeftDoubleClickItem = () => this.startScenario();
+ _('startButton').onPress = () => this.startScenario();
+ _('backToMain').onPress = () => this.goBackToMainMenu();
+ this._ready = true;
+ }
+
+ goBackToMainMenu()
+ {
+ this.run.save();
+
+ messageBox(
+ 400, 200,
+ translate("Are you sure you want to go back? Your progress will be saved."),
+ translate("Confirmation"),
+ [translate("No"), translate("Yes")],
+ [null, () => {
+ Engine.SwitchGuiPage("page_pregame.xml", {});
+ }]
+ );
+ }
+
+ startScenario()
+ {
+ let level = this.getSelectedLevelData();
+ Engine.SwitchGuiPage("page_gamesetup.xml", {
+ "mapType": level.Map.split('/')[0],
+ "map": "maps/" + level.Map,
+ "autostart": true,
+ "campaignData": {
+ "run": this.run.filename,
+ "levelID": this.levelSelection.list_data[this.selectedLevel]
+ }
+ });
+ }
+
+ getSelectedLevelData()
+ {
+ if (this.selectedLevel === -1)
+ return undefined;
+ return this.run.template.Levels[this.levelSelection.list_data[this.selectedLevel]];
+ }
+
+ shouldShowLevel(levelData)
+ {
+ if (this.run.template.ShowUnavailable)
+ return true;
+
+ return this.run.meetsRequirements(levelData);
+ }
+
+ displayLevelsList()
+ {
+ let list = [];
+ for (let key in this.run.template.Levels)
+ {
+ let level = this.run.template.Levels[key];
+
+ if (!this.shouldShowLevel(level))
+ continue;
+
+ let status = "";
+ let name = translate(level.Name);
+ if (!this.run.meetsRequirements(level))
+ {
+ status = translate("not unlocked yet");
+ name = "[color=\"gray\"]" + name + "[/color]";
+ }
+ list.push({ "ID": key, "name": name, "status": status });
+ }
+
+ list.sort((a, b) => this.run.template.Order.indexOf(a.ID) - this.run.template.Order.indexOf(b.ID));
+
+ list = prepareForDropdown(list);
+
+ this.levelSelection.list_name = list.name || [];
+ this.levelSelection.list_status = list.status || [];
+
+ // These must be changed last or things crash.
+ this.levelSelection.list = list.ID || [];
+ this.levelSelection.list_data = list.ID || [];
+ }
+
+ displayLevelDetails()
+ {
+ if (this.selectedLevel === -1)
+ {
+ _("startButton").enabled = false;
+ _("startButton").hidden = false;
+ return;
+ }
+
+ let level = this.getSelectedLevelData();
+
+ _("scenarioName").caption = translate(level.Name);
+ _("scenarioDesc").caption = translate(level.Description);
+ if (level.Preview)
+ _('levelPreviewBox').sprite = "cropped:" + 400/512 + "," + 300/512 + ":" + level.Preview;
+ else
+ _('levelPreviewBox').sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
+
+ _("startButton").enabled = this.run.meetsRequirements(level);
+ _("startButton").hidden = false;
+ _("loadSavedButton").hidden = true;
+ }
+
+ render()
+ {
+ this.displayLevelDetails();
+ this.displayLevelsList();
+ }
+}
+
+
+var g_CampaignMenu;
+
+function init(initData)
+{
+ let run = new CampaignRun(initData.filename).load();
+ g_CampaignMenu = new CampaignMenu(run, initData.finishedLevel || null, initData.won || null);
+}
Index: binaries/data/mods/public/gui/campaign/default_menu/campaignmenu.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/default_menu/campaignmenu.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Index: binaries/data/mods/public/gui/campaign/default_menu/page.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/default_menu/page.xml
@@ -0,0 +1,12 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ campaign/default_menu/campaignmenu.xml
+
Index: binaries/data/mods/public/gui/campaign/load_modal/LoadModal.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/load_modal/LoadModal.js
@@ -0,0 +1,90 @@
+class LoadModal extends DefaultPage
+{
+ constructor(campaignTemplate)
+ {
+ super();
+
+ this.currentRuns = this._watch(CampaignRun.getCurrentRuns());
+
+ this.currentRuns[0].setMeta("toto");
+
+ _('cancelButton').onPress = () => Engine.PopGuiPage();
+ _('deleteGameButton').onPress = () => this.deleteSelectedRun();
+ _('startButton').onPress = () => this.loadCampaign();
+
+ this.selectedRun = -1;
+ this.runSelection = _("runSelection");
+ this.runSelection.onSelectionChange = () => {
+ this.selectedRun = this.runSelection.selected;
+ if (this.selectedRun === -1)
+ _('runDescription').caption = "";
+ else
+ _('runDescription').caption = this.currentRuns[this.selectedRun].generateLabel();
+ };
+
+ this.runSelection.onMouseLeftDoubleClickItem = () => this.loadCampaign();
+
+ this._ready = true;
+ }
+
+ loadCampaign()
+ {
+ let filename = this.currentRuns[this.selectedRun].filename;
+ let run = new CampaignRun(filename).load();
+ // inform user config that we are playing this campaign
+ Engine.ConfigDB_CreateValue("user", "currentcampaign", filename);
+ Engine.ConfigDB_WriteValueToFile("user", "currentcampaign", filename, "config/user.cfg");
+
+ Engine.SwitchGuiPage(run.getMenuPath(), {
+ "filename": filename
+ });
+ }
+
+ deleteSelectedRun()
+ {
+ if (this.selectedRun === -1)
+ return;
+
+ let run = this.currentRuns[this.selectedRun];
+
+ messageBox(
+ 400, 200,
+ translate("Are you sure you want to delete run " + run.generateLabel() + "? This can't be undone."),
+ translate("Confirmation"),
+ [translate("Yes"), translate("No")],
+ [() => {
+ run.destroy();
+ this.currentRuns.splice(this.selectedRun, 1);
+ }, null]
+ );
+ }
+
+ displayCurrentRuns()
+ {
+ if (!this.currentRuns.length)
+ {
+ this.runSelection.list = [translate("No ongoing campaigns.")];
+ this.runSelection.list_data = [""];
+ }
+ else
+ {
+ this.runSelection.list = this.currentRuns.map(run => run.generateLabel());
+ this.runSelection.list_data = this.currentRuns.map(run => run.filename);
+ }
+ }
+
+ render()
+ {
+ _('deleteGameButton').enabled = this.selectedRun !== -1;
+ _('startButton').enabled = this.selectedRun !== -1;
+ this.displayCurrentRuns();
+ }
+}
+
+
+var g_LoadModal;
+
+function init()
+{
+ g_LoadModal = new LoadModal();
+}
Index: binaries/data/mods/public/gui/campaign/load_modal/load_modal.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/load_modal/load_modal.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Load Campaign
+
+
+
+
+
+
+ Name of this campaign run:
+
+
+
+
+
+
+ Cancel
+
+
+
+ Delete
+
+
+
+ Load Campaign
+
+
+
+
Index: binaries/data/mods/public/gui/campaign/load_modal/page.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/load_modal/page.xml
@@ -0,0 +1,12 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ campaign/load_modal/load_modal.xml
+
Index: binaries/data/mods/public/gui/campaign/new_modal/NewCampaignModal.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/new_modal/NewCampaignModal.js
@@ -0,0 +1,39 @@
+class NewCampaignModal extends DefaultPage
+{
+ constructor(campaignTemplate)
+ {
+ super();
+
+ this.template = campaignTemplate;
+
+ _('cancelButton').onPress = () => Engine.PopGuiPage();
+ _('startButton').onPress = () => this.createAndStartCampaign();
+
+ this._ready = true;
+ }
+
+ createAndStartCampaign()
+ {
+ let filename = this.template.identifier + "_" + Date.now() + "_" + Math.floor(Math.random()*100000);
+ let run = new CampaignRun(filename)
+ .setTemplate(this.template)
+ .setMeta(_('runDescription').caption)
+ .save();
+
+ // inform user config that we are playing this campaign
+ Engine.ConfigDB_CreateValue("user", "currentcampaign", filename);
+ Engine.ConfigDB_WriteValueToFile("user", "currentcampaign", filename, "config/user.cfg");
+
+ Engine.SwitchGuiPage(run.getMenuPath(), {
+ "filename": filename
+ });
+ }
+}
+
+
+var g_NewCampaignModal;
+
+function init(campaign_template_data)
+{
+ g_NewCampaignModal = new NewCampaignModal(campaign_template_data);
+}
Index: binaries/data/mods/public/gui/campaign/new_modal/newcampaign_modal.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/new_modal/newcampaign_modal.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name your new campaign
+
+
+
+
+
+
+ Cancel
+
+
+
+ Start Campaign
+
+
+
+
Index: binaries/data/mods/public/gui/campaign/new_modal/page.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/new_modal/page.xml
@@ -0,0 +1,14 @@
+
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ campaign/new_modal/newcampaign_modal.xml
+
+
Index: binaries/data/mods/public/gui/campaign/setup/CampaignSetupPage.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/setup/CampaignSetupPage.js
@@ -0,0 +1,62 @@
+class CampaignSetupPage extends DefaultPage
+{
+ constructor()
+ {
+ super();
+ this.selectedIndex = -1;
+ this.templates = CampaignTemplate.getAvailableTemplates();
+ this.campaignSelection = _("campaignSelection");
+ this.campaignSelection.onMouseLeftDoubleClickItem = () => {
+ if (this.selectedIndex === -1)
+ return;
+ Engine.PushGuiPage("campaign/new_modal/page.xml", this.selectedTemplate);
+ };
+ this.campaignSelection.onSelectionChange = () => {
+ this.selectedIndex = this.campaignSelection.selected;
+ if (this.selectedIndex !== -1)
+ this.selectedTemplate = this.templates[this.selectedIndex];
+ else
+ this.selectedTemplate = null;
+ };
+
+ this._ready = true;
+ }
+
+ displayCampaignDetails()
+ {
+ _("startCampButton").enabled = this.selectedIndex !== -1;
+
+ if (!this.selectedTemplate)
+ {
+ _("campaignTitle").caption = translate("No campaign selected.");
+ _("campaignDesc").caption = "";
+ _("campaignImage").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
+ return;
+ }
+
+ _("campaignTitle").caption = translate(this.selectedTemplate.Name);
+ _("campaignDesc").caption = translate(this.selectedTemplate.Description);
+ if ('Image' in this.selectedTemplate)
+ _("campaignImage").sprite = "stretched:" + this.selectedTemplate.Image;
+ else
+ _("campaignImage").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
+ }
+
+ render()
+ {
+ this.displayCampaignDetails();
+
+ _("campaignSelection").list_name = this.templates.map((camp) => camp.Name);
+ // These must be changed last or things crash.
+ _("campaignSelection").list = this.templates.map((camp) => camp.identifier) || [];
+ _("campaignSelection").list_data = this.templates.map((camp) => camp.identifier) || [];
+ }
+}
+
+
+var g_CampaignSetupPage;
+
+function init()
+{
+ g_CampaignSetupPage = new CampaignSetupPage();
+}
Index: binaries/data/mods/public/gui/campaign/setup/campaignsetup.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/setup/campaignsetup.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Campaigns
+
+
+
+
+
+
+
+ Name
+
+
+
+
+
+
+
+
+
+
+ No Campaign selected
+
+
+
+
+
+
+
+
+
+
+
+
+ Main Menu
+ Engine.SwitchGuiPage("page_pregame.xml");
+
+
+
+
+ Start Campaign
+ startCampaign();
+
+
+
+
Index: binaries/data/mods/public/gui/campaign/setup/page.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/setup/page.xml
@@ -0,0 +1,12 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ campaign/setup/campaignsetup.xml
+
Index: binaries/data/mods/public/gui/common/settings.js
===================================================================
--- binaries/data/mods/public/gui/common/settings.js
+++ binaries/data/mods/public/gui/common/settings.js
@@ -191,6 +191,14 @@
"Path": "maps/scenarios/",
"Suffix": ".xml",
"GetData": Engine.LoadMapSettings
+ },
+ {
+ "Name": "tutorials",
+ "Title": translateWithContext("map", "Tutorial"),
+ "Description": translate("A tutorial map."),
+ "Path": "maps/tutorials/",
+ "Suffix": ".xml",
+ "GetData": Engine.LoadMapSettings
}
];
}
Index: binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js
===================================================================
--- binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js
+++ binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js
@@ -104,20 +104,18 @@
{
if (initData && initData.map && initData.mapType)
{
- Object.defineProperty(this, "autostart", {
- "value": true,
- "writable": false,
- "configurable": false
- });
+ if (initData.autostart)
+ Object.defineProperty(this, "autostart", {
+ "value": true,
+ "writable": false,
+ "configurable": false
+ });
// TODO: Fix g_GameAttributes, g_GameAttributes.settings,
// g_GameAttributes.settings.PlayerData object references and
// copy over each attribute individually when receiving
// settings from the server or the local file.
- g_GameAttributes = {
- "mapType": initData.mapType,
- "map": initData.map
- };
+ g_GameAttributes = initData;
this.updateGameAttributes();
// Don't launchGame before all Load handlers finished
Index: binaries/data/mods/public/gui/pregame/MainMenuItems.js
===================================================================
--- binaries/data/mods/public/gui/pregame/MainMenuItems.js
+++ binaries/data/mods/public/gui/pregame/MainMenuItems.js
@@ -57,11 +57,6 @@
Engine.SwitchGuiPage("page_gamesetup.xml");
}
},
- {
- "caption": translate("Campaigns"),
- "tooltip": translate("Relive history through historical military campaigns. \\[NOT YET IMPLEMENTED]"),
- "enabled": false
- },
{
"caption": translate("Load Game"),
"tooltip": translate("Load a saved game."),
@@ -69,6 +64,20 @@
Engine.PushGuiPage("page_loadgame.xml");
}
},
+ {
+ "caption": translate("Start a new Campaign."),
+ "tooltip": translate("Relive history through historical military campaigns."),
+ "onPress": () => {
+ Engine.SwitchGuiPage("campaign/setup/page.xml");
+ }
+ },
+ {
+ "caption": translate("Load a Campaign."),
+ "tooltip": translate("Relive history through historical military campaigns."),
+ "onPress": () => {
+ Engine.PushGuiPage("campaign/load_modal/page.xml");
+ }
+ },
{
"caption": translate("Replays"),
"tooltip": translate("Playback previous games."),
Index: binaries/data/mods/public/gui/session/campaign/CampaignSession.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/session/campaign/CampaignSession.js
@@ -0,0 +1,39 @@
+class CampaignSession
+{
+ constructor(data)
+ {
+ this.run = new CampaignRun(data.run).load();
+ this.levelID = data.levelID;
+ registerPlayersFinishedHandler(this.onFinish.bind(this));
+ this.won = false;
+ }
+
+ onFinish(players, won)
+ {
+ let playerID = Engine.GetPlayerID();
+ if (players.indexOf(playerID) === -1)
+ return;
+
+ this.won = won;
+ if (!this.won)
+ return;
+
+ this.run.markLevelComplete(this.levelID);
+ }
+
+ getMenu()
+ {
+ return this.run.getMenuPath();
+ }
+
+ getInitData()
+ {
+ return {
+ "filename": this.run.filename,
+ "finishedLevel": this.levelID,
+ "won": this.won,
+ };
+ }
+}
+
+var g_CampaignSession;
Index: binaries/data/mods/public/gui/session/session.js
===================================================================
--- binaries/data/mods/public/gui/session/session.js
+++ binaries/data/mods/public/gui/session/session.js
@@ -264,6 +264,9 @@
restoreSavedGameData(initData.savedGUIData);
}
+ if (g_GameAttributes.campaignData)
+ g_CampaignSession = new CampaignSession(g_GameAttributes.campaignData);
+
let mapCache = new MapCache();
g_Cheats = new Cheats();
g_DiplomacyColors = new DiplomacyColors();
@@ -521,7 +524,7 @@
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
- Engine.SwitchGuiPage("page_summary.xml", {
+ let summaryData = {
"sim": simData,
"gui": {
"dialog": false,
@@ -531,7 +534,22 @@
"replayDirectory": !g_HasRejoined && replayDirectory,
"replaySelectionData": g_ReplaySelectionData
}
- });
+ };
+
+ if (g_GameAttributes.campaignData)
+ {
+ let menu = g_CampaignSession.getMenu();
+ let initData = g_CampaignSession.getInitData();
+ if (g_GameAttributes.campaignData.skipSummary)
+ {
+ Engine.SwitchGuiPage(menu, initData);
+ return;
+ }
+ summaryData.campaignData = initData;
+ summaryData.nextPage = menu;
+ }
+
+ Engine.SwitchGuiPage("page_summary.xml", summaryData);
}
// Return some data that we'll use when hotloading this file after changes
Index: binaries/data/mods/public/gui/session/session.xml
===================================================================
--- binaries/data/mods/public/gui/session/session.xml
+++ binaries/data/mods/public/gui/session/session.xml
@@ -2,8 +2,10 @@
+
+
Index: binaries/data/mods/public/gui/summary/summary.js
===================================================================
--- binaries/data/mods/public/gui/summary/summary.js
+++ binaries/data/mods/public/gui/summary/summary.js
@@ -144,6 +144,7 @@
g_ScorePanelsData = getScorePanelsData();
g_PanelButtons = Object.keys(g_ScorePanelsData).concat(["charts"]).map(panel => panel + "PanelButton");
+
g_SelectedPanel = g_PanelButtons[0];
if (data && data.selectedData)
{
@@ -455,6 +456,8 @@
"replaySelectionData": g_GameData.gui.replaySelectionData,
"summarySelectedData": summarySelectedData
});
+ else if (g_GameData.campaignData)
+ Engine.SwitchGuiPage(g_GameData.nextPage, g_GameData.campaignData);
else
Engine.SwitchGuiPage("page_pregame.xml");
}
Index: binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.js
===================================================================
--- binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.js
+++ binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.js
@@ -6,6 +6,10 @@
"instructions": markForTranslation("Left-click on a female citizen and then right-click on a berry bush to make that female citizen gather food. Female citizens gather vegetables faster than other units."),
"OnPlayerCommand": function(msg)
{
+ let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
+ cmpEndGameManager.MarkPlayerAndAlliesAsWon(1, () => "", () => "");
+ this.NextGoal();
+
if (msg.cmd.type == "gather" && msg.cmd.target &&
TriggerHelper.GetResourceType(msg.cmd.target).specific == "fruit")
this.NextGoal();
Index: binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.xml
===================================================================
--- binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.xml
+++ binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.xml
@@ -40,7 +40,7 @@
{
"CircularMap": true,
"Description": "This is a basic tutorial to get you started playing 0 A.D.",
- "Keywords": ["demo"],
+ "Keywords": ["trigger"],
"LockTeams": false,
"Name": "Introductory Tutorial",
"PlayerData": [
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js
+++ binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -205,6 +205,13 @@
};
};
+GuiInterface.prototype.GetEndGameCampaignData = function(player)
+{
+ return {
+ "status": QueryPlayerIDInterface(player, IID_Player).GetState()
+ };
+}
+
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
Index: source/ps/GameSetup/GameSetup.cpp
===================================================================
--- source/ps/GameSetup/GameSetup.cpp
+++ source/ps/GameSetup/GameSetup.cpp
@@ -462,6 +462,8 @@
// We mount these dirs last as otherwise writing could result in files being placed in a mod's dir.
g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"");
g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH);
+ g_VFS->Mount(L"campaignsaves/", paths.UserData()/"campaignsaves"/"");
+
// Mounting with highest priority, so that a mod supplied user.cfg is harmless
g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1);
if(readonlyConfig != paths.Config())
Index: source/ps/scripting/JSInterface_VFS.h
===================================================================
--- source/ps/scripting/JSInterface_VFS.h
+++ source/ps/scripting/JSInterface_VFS.h
@@ -47,6 +47,9 @@
// Save given JS Object to a JSON file
void WriteJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath, JS::HandleValue val1);
+ // Delete given file.
+ bool DeleteFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath);
+
// Tests whether the current script context is allowed to read from the given directory
bool PathRestrictionMet(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const CStrW& filePath);
Index: source/ps/scripting/JSInterface_VFS.cpp
===================================================================
--- source/ps/scripting/JSInterface_VFS.cpp
+++ source/ps/scripting/JSInterface_VFS.cpp
@@ -213,6 +213,15 @@
g_VFS->CreateFile(path, buf.Data(), buf.Size());
}
+bool JSI_VFS::DeleteFile(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& filePath)
+{
+ OsPath realPath;
+ return VfsFileExists(filePath) &&
+ g_VFS->GetRealPath(filePath, realPath) == INFO::OK &&
+ g_VFS->RemoveFile(filePath) == INFO::OK &&
+ wunlink(realPath) == 0;
+}
+
bool JSI_VFS::PathRestrictionMet(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const CStrW& filePath)
{
for (const CStrW& validPath : validPaths)
@@ -264,6 +273,7 @@
scriptInterface.RegisterFunction("ReadFileLines");
scriptInterface.RegisterFunction("ReadJSONFile");
scriptInterface.RegisterFunction("WriteJSONFile");
+ scriptInterface.RegisterFunction("DeleteFile");
}
void JSI_VFS::RegisterScriptFunctions_Simulation(const ScriptInterface& scriptInterface)
@@ -278,4 +288,5 @@
scriptInterface.RegisterFunction("ListDirectoryFiles");
scriptInterface.RegisterFunction("FileExists");
scriptInterface.RegisterFunction("ReadJSONFile");
+ scriptInterface.RegisterFunction("WriteJSONFile");
}