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": "scenarios/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": "scenarios/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.\u000a\u000aWarning: 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,75 @@ +// ID of the current campaign. This is the name of the json file (not the "human readable" name) +var g_CampaignID = null; + +// Campaign template data from the JSON file. +var g_CampaignTemplate = null; + +// name of the file we're saving campaign data in +var g_CampaignSaveFilename = null; + +// Current campaign state, to be saved in/loaded from the above file +var g_CampaignState = null; + +function startLevel(level) +{ + let matchID = launchGame(g_CampaignTemplate.Levels[level], level); + + g_CampaignState.currentlyPlaying = matchID; + + saveCurrentCampaign(); +} + +// This function is called by session.js at the end of a game. It should save the campaign save state immediately. +function campaignGameEnded(data) +{ + g_CampaignID = data.ID; + g_CampaignTemplate = data.template; + g_CampaignSaveFilename = data.save; + g_CampaignState = data.data; + + // TODO: Deal with the endGameData + + // if we're not active, we either lost or won, so we're no longer playing the game. + if (data.endGameData.status !== "active") + { + if (g_CampaignState.currentlyPlaying) + g_CampaignState.currentlyPlaying = undefined; + } + 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(); +} + +// @returns true if the level "level" is available. +function hasRequirements(level) +{ + if (!level.Requires) + return true; + + if (!g_CampaignState.completed) + return false; + + // reuse class matching system, supporting "or", "+" and "!" + if (!MatchesClassList(g_CampaignState.completed, level.Requires)) + return false; + + return true; +} + +// @returns true if the player has completed the level with ID "level" +function hasCompleted(level) +{ + if (!g_CampaignState.completed) + return false; + + if (g_CampaignState.completed.indexOf(level.ID) === -1) + return false; + + return true; +} Index: binaries/data/mods/public/gui/campaign/campaign_io.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/campaign/campaign_io.js @@ -0,0 +1,137 @@ +// This file provides helper functions related to loading/saving campaign save states and campaign templates +// Most functions rely on loading campaign.js, but not all (such as canLoadCurrentCampaign) + +/* + * TODOs in this file: + * - Provide a way to check if the campaign state changed? + * Other things + */ + +function LoadAvailableCampaigns() +{ + let campaigns = Engine.BuildDirEntList("campaigns/", "*.json", false); + + let ret = {}; + + for (let filename of campaigns) + { + let data = Engine.ReadJSONFile(filename); + if (!data) + continue; + + // TODO: sanity checks, probably in their own function? + if (!data.Name) + continue; + + ret[filename.replace("campaigns/","").replace(".json","")] = data; + } + + return ret; +} + +function canLoadCurrentCampaign(verbose = false) +{ + let campaign = Engine.ConfigDB_GetValue("user", "currentcampaign"); + + if (!campaign) + { + if (verbose) + warn("No campaign chosen, currentcampaign is not defined in user.cfg. Quitting campaign mode.") + return false; + } + + if (!Engine.FileExists("campaignsaves/" + campaign + ".0adcampaign")) + { + if (verbose) + warn("Current campaign not found. Quitting campaign mode."); + return false; + } + + // TODO: load up the file and do some checks? + + return true; +} + +function loadCurrentCampaignSave() +{ + let campaign = Engine.ConfigDB_GetValue("user", "currentcampaign"); + + if (!canLoadCurrentCampaign(true)) + 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; + + // actually fetch the menu + Engine.SwitchGuiPage("campaign/" + g_CampaignTemplate.Interface + "/page_menu.xml", + { + "ID" : g_CampaignID, + "template" : g_CampaignTemplate, + "save": g_CampaignSaveFilename, + "data" : g_CampaignState + }); + + return true; +} + +function loadCampaignTemplate(name) +{ + let data = Engine.ReadJSONFile("campaigns/" + name + ".json"); + + if (!data) + { + warn("Could not parse campaign data."); + return false; + } + + g_CampaignTemplate = data; + + return true; +} + +function saveCurrentCampaign() +{ + if (!g_CampaignSaveFilename) + { + warn("Cannot save current campaign, no campaign is currently loaded."); + return false; + } + + return saveCampaign(g_CampaignSaveFilename, g_CampaignState); +} + +function saveCampaign(filename, data) +{ + Engine.WriteJSONFile("campaignsaves/" + filename + ".0adcampaign", data); + + return true; +} + +function loadCampaignSave(filename) +{ + let campaignData = Engine.ReadJSONFile("campaignsaves/" + filename + ".0adcampaign"); + if (!campaignData) + { + error("Campaign " + filename + " failed to load properly."); + return undefined; + } + + return campaignData; +} Index: binaries/data/mods/public/gui/campaign/gamesetup/gamesetup.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/campaign/gamesetup/gamesetup.js @@ -0,0 +1,83 @@ +// Here should go functions to set up Campaign games. +var g_GameAttributes = { "settings": {} }; + +var g_DefaultPlayerData = []; + +function sanitizePlayerData(playerData) +{ + // Remove gaia + if (playerData.length && !playerData[0]) + playerData.shift(); + + playerData.forEach((pData, index) => { + pData.Color = pData.Color; + pData.Civ = pData.Civ; + + // Use default AI if the map doesn't specify any explicitly + if (!("AI" in pData)) + pData.AI = g_DefaultPlayerData[index].AI; + + if (!("AIDiff" in pData)) + pData.AIDiff = g_DefaultPlayerData[index].AIDiff; + }); +} + + +// TODO: this is a minimalist patchwork from gamesetup.Js and only barely attempts to work. + +function launchGame(level, levelID) +{ + if (!level.Map) + { + warn("cannot start scenario: no maps specified."); + return; + } + + loadSettingsValues(); + + g_DefaultPlayerData = clone(g_Settings.PlayerDefaults); + + let mapData = Engine.LoadMapSettings("maps/" + level.Map); + if (!mapData) + { + warn("Could not load map"); + return; + } + + let mapSettings = mapData && mapData.settings ? clone(mapData.settings) : {}; + + if (mapSettings.PlayerData) + sanitizePlayerData(mapSettings.PlayerData); + + // Copy any new settings + g_GameAttributes.map = "maps/" + level.Map; + g_GameAttributes.script = mapSettings.Script; + + for (let prop in mapSettings) + g_GameAttributes.settings[prop] = mapSettings[prop]; + + // TODO: support default victory conditions? + g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.TriggerScripts || []; + + g_GameAttributes.settings.mapType = "scenario"; + g_GameAttributes.mapType = "scenario"; + + // Seed used for both map generation and simulation + g_GameAttributes.settings.Seed = Math.floor(Math.random() * Math.pow(2, 32)); + g_GameAttributes.settings.AISeed = Math.floor(Math.random() * Math.pow(2, 32)); + + g_GameAttributes.matchID = Engine.GetMatchID(); + + // TODO: player should be defined in the map or the campaign at the least. + let playerID = 1; + + g_GameAttributes.campaignData = {"ID" : g_CampaignID, "template" : g_CampaignTemplate, "save": g_CampaignSaveFilename, "data" : g_CampaignState, "level" : levelID}; + + Engine.StartGame(g_GameAttributes, playerID); + Engine.SwitchGuiPage("page_loading.xml", { + "attribs": g_GameAttributes, + "isNetworked" : false, + "playerAssignments": {} + }); + return g_GameAttributes.matchID; +} Index: binaries/data/mods/public/gui/campaign/load.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/campaign/load.js @@ -0,0 +1,112 @@ +var g_Campaigns = []; + +var g_CampaignTemplate = null; + +function init() +{ + let gameSelection = Engine.GetGUIObjectByName("gameSelection"); + + let campaigns = Engine.BuildDirEntList("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 campaignData.userDescription + " - " + 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("\"%(label)s\""), { + "label": 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.DeleteCampaignGame(name)) + error("Could not delete campaign game " + name); + + if (Engine.ConfigDB_GetValue("user", "currentcampaign") === name) + { + Engine.ConfigDB_RemoveValue("user", "currentcampaign"); + Engine.ConfigDB_WriteFile("user", "config/user.cfg"); + + // TODO: this doesn't seem to work. + if (Engine.GetGUIObjectByName("subMenuContinueCampaignButton")) + Engine.GetGUIObjectByName("subMenuContinueCampaignButton").enabled = false; + } + + // re-run init to refresh. + init(); +} Index: binaries/data/mods/public/gui/campaign/load.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/campaign/load.xml @@ -0,0 +1,45 @@ + + + + +