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": "simple_campaign", + "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,22 @@ +{ + "Name": "Tutorial", + "Description": "Learn how to play 0 A.D.", + "Interface": "simple_campaign", + "Image": "session/icons/mappreview/Introductory Tutorial.png", + "Levels": { + "introduction": { + "Name": "Introductory Tutorial", + "Map": "maps/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": "maps/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/campaign.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/campaign/campaign.js @@ -0,0 +1,58 @@ +/** + * ID of the current campaign. This is the name of the json file (not the "human readable" name) + */ +var g_CampaignID; + +/** + * Campaign template data from the JSON file. + */ +var g_CampaignTemplate; + +/** + * name of the file we're saving campaign data in + */ +var g_CampaignSaveFilename; + +/** + * Current campaign state, to be saved in/loaded from the above file + */ +var g_CampaignState; + +/** + * This function is called by session.js at the end of a game. It should save the campaign save state immediately. + */ +function onCampaignGameEnded(data) +{ + g_CampaignID = data.ID; + g_CampaignTemplate = data.template; + g_CampaignSaveFilename = data.save; + g_CampaignState = data.data; + + if (data.endGameData.status === "won") + { + if (!g_CampaignState.completed) + g_CampaignState.completed = []; + if (g_CampaignState.completed.indexOf(data.level) == -1) + g_CampaignState.completed.push(data.level); + } + + saveCurrentCampaign(); +} + +/** + * @param level the ID of the level. + * @returns true if the level is available. + */ +function meetsRequirements(level) +{ + return !level.Requires && g_CampaignState.completed && MatchesClassList(g_CampaignState.completed, level.Requires); +} + +/** + * @param level the ID of the level. + * @returns true if the player has completed the level + */ +function hasCompleted(level) +{ + return g_CampaignState.completed && g_CampaignState.completed.indexOf(level.ID) != -1; +} Index: binaries/data/mods/public/gui/campaign/campaign_io.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/campaign/campaign_io.js @@ -0,0 +1,125 @@ +/** + * This file provides helper functions related to loading/saving campaign save states and campaign templates + * Most functions rely on loading campaign.js + */ + +/** + * @returns a dictionary of campaign templates, as [ { 'identifier': id, 'data': data }, ... ] + */ +function loadAvailableCampaignTemplates() +{ + let campaigns = Engine.ListDirectoryFiles("campaigns/", "*.json", false); + + let ret = []; + + for (let filename of campaigns) + { + let data = Engine.ReadJSONFile(filename); + if (!isValidCampaignTemplate(data)) + continue; + + // Use file name rather than a particular identifier to guarantee unicity. + ret.push({ + "identifier": filename.slice("campaigns/".length, -".json".length), + "data": data + }); + } + + return ret; +} + +/** + * @returns true if data (a JSON object) is a valid campaign template. + */ +function isValidCampaignTemplate(data) +{ + return data && data.Name; +} + +function loadCampaignTemplate(name) +{ + let data = Engine.ReadJSONFile("campaigns/" + name + ".json"); + + if (!isValidCampaignTemplate(data)) + { + warn("Campaign template " + name + " is not a valid campaign template."); + return false; + } + + g_CampaignTemplate = data; + + return true; +} + +/** + * Check whether loading the current campaign can be loaded. + * @returns true if it can be loaded + */ +function canLoadCurrentCampaignSave() +{ + let campaign = Engine.ConfigDB_GetValue("user", "currentcampaign"); + + if (!campaign) + return false; + + if (!Engine.FileExists("campaignsaves/" + campaign + ".0adcampaign")) + return false; + + // TODO: load up the file and run other checks, such as mod compatibility perhaps. + return true; +} + +function loadCurrentCampaignSave() +{ + let campaign = Engine.ConfigDB_GetValue("user", "currentcampaign"); + + if (!canLoadCurrentCampaignSave()) + return false; + + if (g_CampaignSaveFilename) + { + warn("Campaign already loaded"); + return false; + } + g_CampaignSaveFilename = campaign; + + let campaignData = loadCampaignSave(g_CampaignSaveFilename); + if (!campaignData) + { + warn("Campaign failed to load properly. Quitting campaign mode.") + return false; + } + + g_CampaignState = campaignData; + g_CampaignID = g_CampaignState.campaign; + + if (!loadCampaignTemplate(g_CampaignID)) + return false; + + Engine.SwitchGuiPage("campaign/" + g_CampaignTemplate.Interface + "/page_menu.xml", { + "ID" : g_CampaignID, + "template" : g_CampaignTemplate, + "save": g_CampaignSaveFilename, + "data" : g_CampaignState + }); + + return true; +} + +function saveCurrentCampaign() +{ + if (!g_CampaignSaveFilename) + return false; + + return saveCampaign(g_CampaignSaveFilename, g_CampaignState); +} + +function saveCampaign(filename, data) +{ + Engine.WriteJSONFile("campaignsaves/" + filename + ".0adcampaign", data); +} + +function loadCampaignSave(filename) +{ + return Engine.ReadJSONFile("campaignsaves/" + filename + ".0adcampaign");; +} Index: binaries/data/mods/public/gui/campaign/load.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/campaign/load.js @@ -0,0 +1,108 @@ +function init() +{ + let gameSelection = Engine.GetGUIObjectByName("gameSelection"); + + let campaigns = Engine.ListDirectoryFiles("campaignsaves/", "*.0adcampaign", false); + + if (!campaigns.length) + { + gameSelection.list = [translate("No ongoing campaigns found")]; + gameSelection.selected = -1; + selectionChanged(); + Engine.GetGUIObjectByName("loadGameButton").enabled = false; + Engine.GetGUIObjectByName("deleteGameButton").enabled = false; + return; + } + + gameSelection.list = campaigns.map(path => generateLabel(pathToGame(path))); + gameSelection.list_data = campaigns.map(path => pathToGame(path)); + + if (gameSelection.selected == -1) + gameSelection.selected = 0; + else if (gameSelection.selected >= campaigns.length) // happens when deleting the last saved game + gameSelection.selected = campaigns.length - 1; + else + selectionChanged(); +} + +function pathToGame(path) +{ + return path.replace("campaignsaves/", "").replace(".0adcampaign", ""); +} + +function generateLabel(game) +{ + let campaignData = loadCampaignSave(game); + if (!campaignData) + return "Incompatible - " + game; + + if (!loadCampaignTemplate(campaignData.campaign)) + return "Incompatible - " + game; + + return sprintf(translate("%(userDesc)s - %(campaignName)s"), { + "userDesc": campaignData.userDescription, + "campaignName": g_CampaignTemplate.Name + }); +} + +function selectionChanged() +{ + let gameSelection = Engine.GetGUIObjectByName("gameSelection"); + let selectionEmpty = gameSelection.selected == -1; + + if (selectionEmpty) + return; +} + +function loadCampaign() +{ + let gameSelection = Engine.GetGUIObjectByName("gameSelection"); + + let campaign = gameSelection.list_data[gameSelection.selected]; + reallyLoadCampaign(campaign); + + // TODO: compatibility checks of saved games +} + +function reallyLoadCampaign(name) +{ + Engine.ConfigDB_CreateValue("user", "currentcampaign", name); + Engine.ConfigDB_WriteValueToFile("user", "currentcampaign", name, "config/user.cfg"); + + loadCurrentCampaignSave(); +} + +function deleteCampaign() +{ + let gameSelection = Engine.GetGUIObjectByName("gameSelection"); + let campaign = gameSelection.list_data[gameSelection.selected]; + + if (!campaign) + return; + + messageBox( + 500, 200, + sprintf(translate("\"%(filename)s\""), { + "filename": gameSelection.list[gameSelection.selected] + }) + "\n" + translate("Campaign will be permanently deleted, are you sure?"), + translate("DELETE"), + [translate("No"), translate("Yes")], + [null, function(){ reallyDeleteCampaign(campaign); }] + ); +} + +function reallyDeleteCampaign(name) +{ + if (!Engine.DeleteJSONFile("campaignsaves/" + name + ".0adcampaign")) + return; + + if (Engine.ConfigDB_GetValue("user", "currentcampaign") === name) + { + Engine.ConfigDB_RemoveValue("user", "currentcampaign"); + Engine.ConfigDB_WriteFile("user", "config/user.cfg"); + + Engine.GetGUIObjectByName("subMenuContinueCampaignButton").enabled = false; + } + + init(); +} Index: binaries/data/mods/public/gui/campaign/load.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/campaign/load.xml @@ -0,0 +1,43 @@ + + + + +