Index: ps/trunk/binaries/data/mods/public/globalscripts/utility.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/utility.js (revision 18543) +++ ps/trunk/binaries/data/mods/public/globalscripts/utility.js (revision 18544) @@ -1,18 +1,36 @@ /** * returns a clone of a simple object or array * Only valid JSON objects are accepted * So no recursion, and only plain objects or arrays */ function clone(o) { let r; if (o instanceof Array) r = []; else if (o instanceof Object) r = {}; else // native data type return o; for (let key in o) r[key] = clone(o[key]); return r; } + +/** + * "Inside-out" implementation of Fisher-Yates shuffle + */ +function shuffleArray(source) +{ + if (!source.length) + return []; + + let result = [source[0]]; + for (let i = 1; i < source.length; ++i) + { + let j = Math.floor(Math.random() * i); + result[i] = result[j]; + result[j] = source[i]; + } + return result; +} Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js (revision 18543) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js (revision 18544) @@ -1,558 +1,540 @@ const PI = Math.PI; const TWO_PI = 2 * Math.PI; const TERRAIN_SEPARATOR = "|"; const SEA_LEVEL = 20.0; const CELL_SIZE = 4; const HEIGHT_UNITS_PER_METRE = 92; const MIN_MAP_SIZE = 128; const MAX_MAP_SIZE = 512; const MAP_BORDER_WIDTH = 3; const FALLBACK_CIV = "athen"; /** * Constants needed for heightmap_manipulation.js */ -const MAX_HEIGHT_RANGE = 0xFFFF / HEIGHT_UNITS_PER_METRE // Engine limit, Roughly 700 meters +const MAX_HEIGHT_RANGE = 0xFFFF / HEIGHT_UNITS_PER_METRE; // Engine limit, Roughly 700 meters const MIN_HEIGHT = - SEA_LEVEL; const MAX_HEIGHT = MAX_HEIGHT_RANGE - SEA_LEVEL; // Default angle for buildings const BUILDING_ORIENTATION = - PI / 4; function fractionToTiles(f) { return g_Map.size * f; } function tilesToFraction(t) { return t / g_Map.size; } function fractionToSize(f) { return getMapArea() * f; } function sizeToFraction(s) { return s / getMapArea(); } function scaleByMapSize(min, max) { return min + (max - min) * (g_Map.size - MIN_MAP_SIZE) / (MAX_MAP_SIZE - MIN_MAP_SIZE); } function cos(x) { return Math.cos(x); } function sin(x) { return Math.sin(x); } function abs(x) { return Math.abs(x); } function round(x) { return Math.round(x); } function lerp(a, b, t) { return a + (b-a) * t; } function sqrt(x) { return Math.sqrt(x); } function ceil(x) { return Math.ceil(x); } function floor(x) { return Math.floor(x); } function max(a, b) { return a > b ? a : b; } function min(a, b) { return a < b ? a : b; } /** - * "Inside-out" implementation of Fisher-Yates shuffle - */ -function shuffleArray(source) -{ - if (!source.length) - return []; - - let result = [source[0]]; - for (let i = 1; i < source.length; ++i) - { - let j = randInt(0, i); - result[i] = result[j]; - result[j] = source[i]; - } - return result; -} - -/** * Retries the given function with those arguments as often as specified. */ function retryPlacing(placeFunc, placeArgs, retryFactor, amount, getResult) { let maxFail = amount * retryFactor; let results = []; let good = 0; let bad = 0; while (good < amount && bad <= maxFail) { let result = placeFunc(placeArgs); if (result !== undefined) { ++good; if (getResult) results.push(result); } else ++bad; } return getResult ? results : good; } /** * Helper function for randomly placing areas and groups on the map. */ function randomizePlacerCoordinates(placer, halfMapSize) { if (!!g_MapSettings.CircularMap) { // Polar coordinates let r = halfMapSize * Math.sqrt(randFloat()); // uniform distribution let theta = randFloat(0, 2 * PI); placer.x = Math.floor(r * Math.cos(theta)) + halfMapSize; placer.z = Math.floor(r * Math.sin(theta)) + halfMapSize; } else { // Rectangular coordinates placer.x = randInt(g_Map.size); placer.z = randInt(g_Map.size); } } /** * Helper function for randomly placing areas and groups in the given areas. */ function randomizePlacerCoordinatesFromAreas(placer, areas) { let i = randInt(areas.length); let pt = areas[i].points[randInt(areas[i].points.length)]; placer.x = pt.x; placer.z = pt.z; } /** * Attempts to place the given number of areas in random places of the map. * Returns actually placed areas. */ function createAreas(centeredPlacer, painter, constraint, amount, retryFactor = 10) { let placeFunc = function (args) { randomizePlacerCoordinates(args.placer, args.halfMapSize); return g_Map.createArea(args.placer, args.painter, args.constraint); }; let args = { "placer": centeredPlacer, "painter": painter, "constraint": constraint, "halfMapSize": g_Map.size / 2 }; return retryPlacing(placeFunc, args, retryFactor, amount, true); } /** * Attempts to place the given number of areas in random places of the given areas. * Returns actually placed areas. */ function createAreasInAreas(centeredPlacer, painter, constraint, amount, retryFactor, areas) { if (!areas.length) return []; let placeFunc = function (args) { randomizePlacerCoordinatesFromAreas(args.placer, args.areas); return g_Map.createArea(args.placer, args.painter, args.constraint); }; let args = { "placer": centeredPlacer, "painter": painter, "constraint": constraint, "areas": areas, "halfMapSize": g_Map.size / 2 }; return retryPlacing(placeFunc, args, retryFactor, amount, true); } /** * Attempts to place the given number of groups in random places of the map. * Returns the number of actually placed groups. */ function createObjectGroups(placer, player, constraint, amount, retryFactor = 10) { let placeFunc = function (args) { randomizePlacerCoordinates(args.placer, args.halfMapSize); return createObjectGroup(args.placer, args.player, args.constraint); }; let args = { "placer": placer, "player": player, "constraint": constraint, "halfMapSize": g_Map.size / 2 - 3 }; return retryPlacing(placeFunc, args, retryFactor, amount, false); } /** * Attempts to place the given number of groups in random places of the given areas. * Returns the number of actually placed groups. */ function createObjectGroupsByAreas(placer, player, constraint, amount, retryFactor, areas) { if (!areas.length) return 0; let placeFunc = function (args) { randomizePlacerCoordinatesFromAreas(args.placer, args.areas); return createObjectGroup(args.placer, args.player, args.constraint); }; let args = { "placer": placer, "player": player, "constraint": constraint, "areas": areas }; return retryPlacing(placeFunc, args, retryFactor, amount, false); } function createTerrain(terrain) { if (!(terrain instanceof Array)) return createSimpleTerrain(terrain); return new RandomTerrain(terrain.map(t => createTerrain(t))); } function createSimpleTerrain(terrain) { if (typeof(terrain) != "string") throw("createSimpleTerrain expects string as input, received "+terrain); // Split string by pipe | character, this allows specifying terrain + tree type in single string let params = terrain.split(TERRAIN_SEPARATOR, 2); if (params.length != 2) return new SimpleTerrain(terrain); return new SimpleTerrain(params[0], params[1]); } function placeObject(x, z, type, player, angle) { if (g_Map.validT(x, z, MAP_BORDER_WIDTH)) g_Map.addObject(new Entity(type, player, x, z, angle)); } function placeTerrain(x, z, terrain) { // convert terrain param into terrain object g_Map.placeTerrain(x, z, createTerrain(terrain)); } function isCircularMap() { return !!g_MapSettings.CircularMap; } function getMapBaseHeight() { return g_MapSettings.BaseHeight || 0; } function createTileClass() { return g_Map.createTileClass(); } function getTileClass(id) { if (!g_Map.validClass(id)) return undefined; return g_Map.tileClasses[id]; } function createArea(placer, painter, constraint) { return g_Map.createArea(placer, painter, constraint); } function createObjectGroup(placer, player, constraint) { return g_Map.createObjectGroup(placer, player, constraint); } function getMapSize() { return g_Map.size; } function getMapArea() { return g_Map.size * g_Map.size; } function getNumPlayers() { return g_MapSettings.PlayerData.length - 1; } function getCivCode(player) { if (g_MapSettings.PlayerData[player+1].Civ) return g_MapSettings.PlayerData[player+1].Civ; warn("undefined civ specified for player " + (player + 1) + ", falling back to '" + FALLBACK_CIV + "'"); return FALLBACK_CIV; } function areAllies(player1, player2) { if (g_MapSettings.PlayerData[player1+1].Team === undefined || g_MapSettings.PlayerData[player2+1].Team === undefined || g_MapSettings.PlayerData[player2+1].Team == -1 || g_MapSettings.PlayerData[player1+1].Team == -1) return false; return g_MapSettings.PlayerData[player1+1].Team === g_MapSettings.PlayerData[player2+1].Team; } function getPlayerTeam(player) { if (g_MapSettings.PlayerData[player+1].Team === undefined) return -1; return g_MapSettings.PlayerData[player+1].Team; } /** * Sorts an array of player IDs by team index. Players without teams come first. * Randomize order for players of the same team. */ function sortPlayers(playerIndices) { return shuffleArray(playerIndices).sort((p1, p2) => getPlayerTeam(p1 - 1) - getPlayerTeam(p2 - 1)); } function primeSortPlayers(playerIndices) { if (!playerIndices.length) return []; let prime = []; for (let i = 0; i < Math.ceil(playerIndices.length / 2); ++i) { prime.push(playerIndices[i]); prime.push(playerIndices[playerIndices.length - 1 - i]); } return prime; } function getStartingEntities(player) { let civ = getCivCode(player); if (!g_CivData[civ] || !g_CivData[civ].StartEntities || !g_CivData[civ].StartEntities.length) { warn("Invalid or unimplemented civ '"+civ+"' specified, falling back to '" + FALLBACK_CIV + "'"); civ = FALLBACK_CIV; } return g_CivData[civ].StartEntities; } function getHeight(x, z) { return g_Map.getHeight(x, z); } function setHeight(x, z, height) { g_Map.setHeight(x, z, height); } /** * Utility functions for classes */ /** * Add point to given class by id */ function addToClass(x, z, id) { let tileClass = getTileClass(id); if (tileClass !== null) tileClass.add(x, z); } /** * Remove point from the given class by id */ function removeFromClass(x, z, id) { let tileClass = getTileClass(id); if (tileClass !== null) tileClass.remove(x, z); } /** * Create a painter for the given class */ function paintClass(id) { return new TileClassPainter(getTileClass(id)); } /** * Create a painter for the given class */ function unPaintClass(id) { return new TileClassUnPainter(getTileClass(id)); } /** * Create an avoid constraint for the given classes by the given distances */ function avoidClasses(/*class1, dist1, class2, dist2, etc*/) { let ar = []; for (let i = 0; i < arguments.length/2; ++i) ar.push(new AvoidTileClassConstraint(arguments[2*i], arguments[2*i+1])); // Return single constraint if (ar.length == 1) return ar[0]; return new AndConstraint(ar); } /** * Create a stay constraint for the given classes by the given distances */ function stayClasses(/*class1, dist1, class2, dist2, etc*/) { let ar = []; for (let i = 0; i < arguments.length/2; ++i) ar.push(new StayInTileClassConstraint(arguments[2*i], arguments[2*i+1])); // Return single constraint if (ar.length == 1) return ar[0]; return new AndConstraint(ar); } /** * Create a border constraint for the given classes by the given distances */ function borderClasses(/*class1, idist1, odist1, class2, idist2, odist2, etc*/) { let ar = []; for (let i = 0; i < arguments.length/3; ++i) ar.push(new BorderTileClassConstraint(arguments[3*i], arguments[3*i+1], arguments[3*i+2])); // Return single constraint if (ar.length == 1) return ar[0]; return new AndConstraint(ar); } /** * Checks if the given tile is in class "id" */ function checkIfInClass(x, z, id) { let tileClass = getTileClass(id); if (tileClass === null) return 0; let members = tileClass.countMembersInRadius(x, z, 1); if (members === null) return 0; return members; } /** * Returns the distance between 2 points */ function getDistance(x1, z1, x2, z2) { return Math.pow(Math.pow(x1 - x2, 2) + Math.pow(z1 - z2, 2), 1/2); } /** * Returns the angle of the vector between point 1 and point 2. * The angle is counterclockwise from the positive x axis. */ function getAngle(x1, z1, x2, z2) { return Math.atan2(z2 - z1, x2 - x1); } /** * Returns the gradient of the line between point 1 and 2 in the form dz/dx */ function getGradient(x1, z1, x2, z2) { if (x1 == x2 && z1 == z2) return 0; return (z1-z2)/(x1-x2); } function getTerrainTexture(x, y) { return g_Map.getTexture(x, y); } Index: ps/trunk/binaries/data/mods/public/maps/scripts/Regicide.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/Regicide.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/maps/scripts/Regicide.js (revision 18544) @@ -0,0 +1,104 @@ +Trigger.prototype.CheckRegicideDefeat = function(data) +{ + if (data.entity == this.regicideHeroes[data.from]) + TriggerHelper.DefeatPlayer(data.from); +}; + +Trigger.prototype.InitRegicideGame = function(msg) +{ + let playersCivs = []; + for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID) + playersCivs[playerID] = QueryPlayerIDInterface(playerID).GetCiv(); + + // Get all hero templates of these civs + let heroTemplates = {}; + let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + for (let templateName of cmpTemplateManager.FindAllTemplates(false)) + { + if (templateName.substring(0,6) != "units/") + continue; + + let identity = cmpTemplateManager.GetTemplate(templateName).Identity; + let classes = GetIdentityClasses(identity); + + if (classes.indexOf("Hero") == -1 || + playersCivs.every(civ => civ != identity.Civ)) + continue; + + if (!heroTemplates[identity.Civ]) + heroTemplates[identity.Civ] = []; + + if (heroTemplates[identity.Civ].indexOf(templateName) == -1) + heroTemplates[identity.Civ].push({ + "templateName": templateName, + "classes": classes + }); + } + + // Sort available spawn points by preference + let spawnPreference = ["Ship", "Structure", "CivilCentre"]; + let getSpawnPreference = entity => { + let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); + let classes = cmpIdentity.GetClassesList(); + return spawnPreference.findIndex(className => classes.indexOf(className) != -1); + }; + + // Attempt to spawn one hero per player + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID) + { + let spawnPoints = cmpRangeManager.GetEntitiesByPlayer(playerID).sort((entity1, entity2) => + getSpawnPreference(entity2) - getSpawnPreference(entity1)); + + this.regicideHeroes[playerID] = this.SpawnRegicideHero(playerID, heroTemplates[playersCivs[playerID]], spawnPoints); + } +}; + +/** + * Spawn a random hero at one of the given locations (which are checked in order). + * Garrison it if the location is a ship. + * + * @param spawnPoints - entity IDs at which to spawn + */ +Trigger.prototype.SpawnRegicideHero = function(playerID, heroTemplates, spawnPoints) +{ + for (let heroTemplate of shuffleArray(heroTemplates)) + for (let spawnPoint of spawnPoints) + { + let cmpPosition = Engine.QueryInterface(spawnPoint, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld()) + continue; + + // Consider nomad maps where units start on a ship + let isShip = TriggerHelper.EntityHasClass(spawnPoint, "Ship"); + if (isShip) + { + let cmpGarrisonHolder = Engine.QueryInterface(spawnPoint, IID_GarrisonHolder); + if (cmpGarrisonHolder.IsFull() || + !MatchesClassList(heroTemplate.classes, cmpGarrisonHolder.GetAllowedClasses())) + continue; + } + + let hero = TriggerHelper.SpawnUnits(spawnPoint, heroTemplate.templateName, 1, playerID); + if (!hero.length) + continue; + + hero = hero[0]; + + if (isShip) + { + let cmpUnitAI = Engine.QueryInterface(hero, IID_UnitAI); + cmpUnitAI.Garrison(spawnPoint); + } + + return hero; + } + + error("Couldn't spawn hero for player " + playerID); + return undefined; +}; + +let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); +cmpTrigger.regicideHeroes = []; +cmpTrigger.DoAfterDelay(0, "InitRegicideGame", {}); +cmpTrigger.RegisterTrigger("OnOwnershipChanged", "CheckRegicideDefeat", { "enabled": true }); Property changes on: ps/trunk/binaries/data/mods/public/maps/scripts/Regicide.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js (revision 18543) +++ ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js (revision 18544) @@ -1,160 +1,160 @@ // Contains standardized functions suitable for using in trigger scripts. // Do not use them in any other simulation script. var TriggerHelper = {}; TriggerHelper.GetPlayerIDFromEntity = function(ent) { let cmpPlayer = Engine.QueryInterface(ent, IID_Player); if (cmpPlayer) return cmpPlayer.GetPlayerID(); return -1; }; TriggerHelper.GetOwner = function(ent) { let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) return cmpOwnership.GetOwner(); return -1; }; /** - * Can be used to "force" a building to spawn a group of entities. - * Only works for buildings that can already train units. + * Can be used to "force" a building/unit to spawn a group of entities. + * * @param source Entity id of the point where they will be spawned from * @param template Name of the template * @param count Number of units to spawn * @param owner Player id of the owner of the new units. By default, the owner * of the source entity. */ TriggerHelper.SpawnUnits = function(source, template, count, owner) { let entities = []; let cmpFootprint = Engine.QueryInterface(source, IID_Footprint); let cmpPosition = Engine.QueryInterface(source, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) { error("tried to create entity from a source without position"); return entities; } if (owner == null) owner = TriggerHelper.GetOwner(source); for (let i = 0; i < count; ++i) { let ent = Engine.AddEntity(template); let cmpEntPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpEntPosition) { error("tried to create entity without position"); continue; } let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpEntOwnership) cmpEntOwnership.SetOwner(owner); entities.push(ent); let pos; if (cmpFootprint) pos = cmpFootprint.PickSpawnPoint(ent); // TODO this can happen if the player build on the place // where our trigger point is // We should probably warn the trigger maker in some way, // but not interrupt the game for the player if (!pos || pos.y < 0) pos = cmpPosition.GetPosition(); cmpEntPosition.JumpTo(pos.x, pos.z); } return entities; }; /** * Spawn units from all trigger points with this reference * If player is defined, only spaw units from the trigger points * that belong to that player * @param ref Trigger point reference name to spawn units from * @param template Template name * @param count Number of spawned entities per Trigger point * @param owner Owner of the spawned units. Default: the owner of the origins * @return A list of new entities per origin like * {originId1: [entId1, entId2], originId2: [entId3, entId4], ...} */ TriggerHelper.SpawnUnitsFromTriggerPoints = function(ref, template, count, owner = null) { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); let triggerPoints = cmpTrigger.GetTriggerPoints(ref); let entities = {}; for (let point of triggerPoints) entities[point] = TriggerHelper.SpawnUnits(point, template, count, owner); return entities; }; /** * Returns the resource type that can be gathered from an entity */ TriggerHelper.GetResourceType = function(entity) { let cmpResourceSupply = Engine.QueryInterface(entity, IID_ResourceSupply); if (!cmpResourceSupply) return undefined; return cmpResourceSupply.GetType(); }; /** * The given player will win the game. * If it's not a last man standing game, then allies will win too. */ TriggerHelper.SetPlayerWon = function(playerID) { let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); cmpEndGameManager.MarkPlayerAsWon(playerID); }; /** * Defeats a player */ TriggerHelper.DefeatPlayer = function(playerID) { let cmpPlayer = QueryPlayerIDInterface(playerID); if (cmpPlayer) cmpPlayer.SetState("defeated"); }; /** * Returns the number of current players */ TriggerHelper.GetNumberOfPlayers = function() { let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); return cmpPlayerManager.GetNumPlayers(); }; /** * A function to determine if an entity has a specific class * @param entity ID of the entity that we want to check for classes * @param classname The name of the class we are checking if the entity has */ TriggerHelper.EntityHasClass = function(entity, classname) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); if (!cmpIdentity) return false; let classes = cmpIdentity.GetClassesList(); return classes && classes.indexOf(classname) != -1; }; Engine.RegisterGlobal("TriggerHelper", TriggerHelper); Index: ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/regicide.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/regicide.json (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/regicide.json (revision 18544) @@ -0,0 +1,15 @@ +{ + "TranslatedKeys": ["Title", "Description"], + "Data": + { + "Title": "Regicide", + "Description": "Defeat opponents by killing their hero", + "Scripts": + [ + "scripts/TriggerHelper.js", + "scripts/ConquestCommon.js", + "scripts/Conquest.js", + "scripts/Regicide.js" + ] + } +} Property changes on: ps/trunk/binaries/data/mods/public/simulation/data/settings/victory_conditions/regicide.json ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp =================================================================== --- ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp (revision 18543) +++ ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp (revision 18544) @@ -1,576 +1,577 @@ -/* Copyright (C) 2015 Wildfire Games. +/* Copyright (C) 2016 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "Map.h" #include "AtlasObject/AtlasObject.h" #include "GameInterface/Messages.h" #include "ScenarioEditor/ScenarioEditor.h" #include "ScenarioEditor/Tools/Common/Tools.h" #include "wx/busyinfo.h" #include "wx/filename.h" enum { ID_MapName, ID_MapDescription, ID_MapReveal, ID_MapType, ID_MapPreview, ID_MapTeams, ID_MapKW_Demo, ID_MapKW_Naval, ID_RandomScript, ID_RandomSize, ID_RandomSeed, ID_RandomReseed, ID_RandomGenerate, ID_SimPlay, ID_SimFast, ID_SimSlow, ID_SimPause, ID_SimReset, ID_OpenPlayerPanel }; enum { SimInactive, SimPlaying, SimPlayingFast, SimPlayingSlow, SimPaused }; bool IsPlaying(int s) { return (s == SimPlaying || s == SimPlayingFast || s == SimPlayingSlow); } // TODO: Some of these helper things should be moved out of this file // and into shared locations // Helper function for adding tooltips static wxWindow* Tooltipped(wxWindow* window, const wxString& tip) { window->SetToolTip(tip); return window; } // Helper class for storing AtObjs class AtObjClientData : public wxClientData { public: AtObjClientData(const AtObj& obj) : obj(obj) {} virtual ~AtObjClientData() {} AtObj GetValue() { return obj; } private: AtObj obj; }; class MapSettingsControl : public wxPanel { public: MapSettingsControl(wxWindow* parent, ScenarioEditor& scenarioEditor); void CreateWidgets(); void ReadFromEngine(); void SetMapSettings(const AtObj& obj); AtObj UpdateSettingsObject(); private: void SendToEngine(); void OnEdit(wxCommandEvent& WXUNUSED(evt)) { SendToEngine(); } std::set m_MapSettingsKeywords; std::vector m_PlayerCivChoices; Observable& m_MapSettings; DECLARE_EVENT_TABLE(); }; BEGIN_EVENT_TABLE(MapSettingsControl, wxPanel) EVT_TEXT(ID_MapName, MapSettingsControl::OnEdit) EVT_TEXT(ID_MapDescription, MapSettingsControl::OnEdit) EVT_TEXT(ID_MapPreview, MapSettingsControl::OnEdit) EVT_CHECKBOX(wxID_ANY, MapSettingsControl::OnEdit) EVT_CHOICE(wxID_ANY, MapSettingsControl::OnEdit) END_EVENT_TABLE(); MapSettingsControl::MapSettingsControl(wxWindow* parent, ScenarioEditor& scenarioEditor) : wxPanel(parent, wxID_ANY), m_MapSettings(scenarioEditor.GetMapSettings()) { wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Map settings")); SetSizer(sizer); } void MapSettingsControl::CreateWidgets() { wxSizer* sizer = GetSizer(); ///////////////////////////////////////////////////////////////////////// // Map settings wxBoxSizer* nameSizer = new wxBoxSizer(wxHORIZONTAL); nameSizer->Add(new wxStaticText(this, wxID_ANY, _("Name")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); nameSizer->Add(8, 0); nameSizer->Add(Tooltipped(new wxTextCtrl(this, ID_MapName), _("Displayed name of the map")), wxSizerFlags().Proportion(1)); sizer->Add(nameSizer, wxSizerFlags().Expand()); sizer->Add(0, 2); sizer->Add(new wxStaticText(this, wxID_ANY, _("Description"))); sizer->Add(Tooltipped(new wxTextCtrl(this, ID_MapDescription, wxEmptyString, wxDefaultPosition, wxSize(-1, 100), wxTE_MULTILINE), _("Short description used on the map selection screen")), wxSizerFlags().Expand()); sizer->AddSpacer(5); // TODO: replace by filenames in binaries/data/mods/public/simulation/data/settings/victory_conditions/ wxArrayString gameTypes; gameTypes.Add(_T("conquest")); gameTypes.Add(_T("conquest_structures")); gameTypes.Add(_T("conquest_units")); gameTypes.Add(_T("wonder")); gameTypes.Add(_T("endless")); + gameTypes.Add(_T("regicide")); wxFlexGridSizer* gridSizer = new wxFlexGridSizer(2, 5, 5); gridSizer->AddGrowableCol(1); // TODO: have preview selector tool? gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Preview")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(Tooltipped(new wxTextCtrl(this, ID_MapPreview, wxEmptyString), _("Texture used for map preview")), wxSizerFlags().Expand()); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Reveal map")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(Tooltipped(new wxCheckBox(this, ID_MapReveal, wxEmptyString), _("If checked, players won't need to explore"))); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Game type")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(Tooltipped(new wxChoice(this, ID_MapType, wxDefaultPosition, wxDefaultSize, gameTypes), _("Select the game type (or victory condition)")), wxSizerFlags().Expand()); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Lock teams")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(Tooltipped(new wxCheckBox(this, ID_MapTeams, wxEmptyString), _("If checked, teams will be locked"))); sizer->Add(gridSizer, wxSizerFlags().Expand()); sizer->AddSpacer(5); wxStaticBoxSizer* keywordsSizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Keywords")); wxFlexGridSizer* kwGridSizer = new wxFlexGridSizer(4, 5, 5); kwGridSizer->Add(new wxStaticText(this, wxID_ANY, _("Demo")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); kwGridSizer->Add(Tooltipped(new wxCheckBox(this, ID_MapKW_Demo, wxEmptyString), _("If checked, map will only be visible using filters in game setup"))); kwGridSizer->Add(new wxStaticText(this, wxID_ANY, _("Naval")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); kwGridSizer->Add(Tooltipped(new wxCheckBox(this, ID_MapKW_Naval, wxEmptyString), _("If checked, map will only be visible using filters in game setup"))); keywordsSizer->Add(kwGridSizer); sizer->Add(keywordsSizer, wxSizerFlags().Expand()); } void MapSettingsControl::ReadFromEngine() { AtlasMessage::qGetMapSettings qry; qry.Post(); if (!(*qry.settings).empty()) { // Prevent error if there's no map settings to parse m_MapSettings = AtlasObject::LoadFromJSON(*qry.settings); } // map name wxDynamicCast(FindWindow(ID_MapName), wxTextCtrl)->ChangeValue(wxString(m_MapSettings["Name"])); // map description wxDynamicCast(FindWindow(ID_MapDescription), wxTextCtrl)->ChangeValue(wxString(m_MapSettings["Description"])); // map preview wxDynamicCast(FindWindow(ID_MapPreview), wxTextCtrl)->ChangeValue(wxString(m_MapSettings["Preview"])); // reveal map wxDynamicCast(FindWindow(ID_MapReveal), wxCheckBox)->SetValue(wxString(m_MapSettings["RevealMap"]) == L"true"); // game type / victory conditions if (m_MapSettings["GameType"].defined()) wxDynamicCast(FindWindow(ID_MapType), wxChoice)->SetStringSelection(wxString(m_MapSettings["GameType"])); else wxDynamicCast(FindWindow(ID_MapType), wxChoice)->SetSelection(0); // lock teams wxDynamicCast(FindWindow(ID_MapTeams), wxCheckBox)->SetValue(wxString(m_MapSettings["LockTeams"]) == L"true"); // keywords { m_MapSettingsKeywords.clear(); for (AtIter keyword = m_MapSettings["Keywords"]["item"]; keyword.defined(); ++keyword) m_MapSettingsKeywords.insert(std::wstring(keyword)); wxDynamicCast(FindWindow(ID_MapKW_Demo), wxCheckBox)->SetValue(m_MapSettingsKeywords.count(L"demo") != 0); wxDynamicCast(FindWindow(ID_MapKW_Naval), wxCheckBox)->SetValue(m_MapSettingsKeywords.count(L"naval") != 0); } } void MapSettingsControl::SetMapSettings(const AtObj& obj) { m_MapSettings = obj; m_MapSettings.NotifyObservers(); SendToEngine(); } AtObj MapSettingsControl::UpdateSettingsObject() { // map name m_MapSettings.set("Name", wxDynamicCast(FindWindow(ID_MapName), wxTextCtrl)->GetValue()); // map description m_MapSettings.set("Description", wxDynamicCast(FindWindow(ID_MapDescription), wxTextCtrl)->GetValue()); // map preview m_MapSettings.set("Preview", wxDynamicCast(FindWindow(ID_MapPreview), wxTextCtrl)->GetValue()); // reveal map m_MapSettings.setBool("RevealMap", wxDynamicCast(FindWindow(ID_MapReveal), wxCheckBox)->GetValue()); // game type / victory conditions m_MapSettings.set("GameType", wxDynamicCast(FindWindow(ID_MapType), wxChoice)->GetStringSelection()); // keywords { if (wxDynamicCast(FindWindow(ID_MapKW_Demo), wxCheckBox)->GetValue()) m_MapSettingsKeywords.insert(L"demo"); else m_MapSettingsKeywords.erase(L"demo"); if (wxDynamicCast(FindWindow(ID_MapKW_Naval), wxCheckBox)->GetValue()) m_MapSettingsKeywords.insert(L"naval"); else m_MapSettingsKeywords.erase(L"naval"); AtObj keywords; keywords.set("@array", L""); for (std::set::iterator it = m_MapSettingsKeywords.begin(); it != m_MapSettingsKeywords.end(); ++it) keywords.add("item", it->c_str()); m_MapSettings.set("Keywords", keywords); } // teams locked m_MapSettings.setBool("LockTeams", wxDynamicCast(FindWindow(ID_MapTeams), wxCheckBox)->GetValue()); return m_MapSettings; } void MapSettingsControl::SendToEngine() { UpdateSettingsObject(); std::string json = AtlasObject::SaveToJSON(m_MapSettings); // TODO: would be nice if we supported undo for settings changes POST_COMMAND(SetMapSettings, (json)); } MapSidebar::MapSidebar(ScenarioEditor& scenarioEditor, wxWindow* sidebarContainer, wxWindow* bottomBarContainer) : Sidebar(scenarioEditor, sidebarContainer, bottomBarContainer), m_SimState(SimInactive) { m_MapSettingsCtrl = new MapSettingsControl(this, m_ScenarioEditor); m_MainSizer->Add(m_MapSettingsCtrl, wxSizerFlags().Expand()); { ///////////////////////////////////////////////////////////////////////// // Random map settings wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Random map")); sizer->Add(new wxChoice(this, ID_RandomScript), wxSizerFlags().Expand()); sizer->AddSpacer(5); sizer->Add(new wxButton(this, ID_OpenPlayerPanel, _T("Change players")), wxSizerFlags().Expand()); sizer->AddSpacer(5); wxFlexGridSizer* gridSizer = new wxFlexGridSizer(2, 5, 5); gridSizer->AddGrowableCol(1); wxChoice* sizeChoice = new wxChoice(this, ID_RandomSize); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Map size")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(sizeChoice, wxSizerFlags().Expand()); gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Random seed")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); wxBoxSizer* seedSizer = new wxBoxSizer(wxHORIZONTAL); seedSizer->Add(Tooltipped(new wxTextCtrl(this, ID_RandomSeed, _T("0"), wxDefaultPosition, wxDefaultSize, 0, wxTextValidator(wxFILTER_NUMERIC)), _("Seed value for random map")), wxSizerFlags(1).Expand()); seedSizer->Add(Tooltipped(new wxButton(this, ID_RandomReseed, _("R"), wxDefaultPosition, wxSize(24, -1)), _("New random seed"))); gridSizer->Add(seedSizer, wxSizerFlags().Expand()); sizer->Add(gridSizer, wxSizerFlags().Expand()); sizer->AddSpacer(5); sizer->Add(Tooltipped(new wxButton(this, ID_RandomGenerate, _("Generate map")), _("Run selected random map script")), wxSizerFlags().Expand()); m_MainSizer->Add(sizer, wxSizerFlags().Expand().Border(wxTOP, 10)); } { ///////////////////////////////////////////////////////////////////////// // Simulation buttons wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Simulation test")); wxGridSizer* gridSizer = new wxGridSizer(5); gridSizer->Add(Tooltipped(new wxButton(this, ID_SimPlay, _("Play")), _("Run the simulation at normal speed")), wxSizerFlags().Expand()); gridSizer->Add(Tooltipped(new wxButton(this, ID_SimFast, _("Fast")), _("Run the simulation at 8x speed")), wxSizerFlags().Expand()); gridSizer->Add(Tooltipped(new wxButton(this, ID_SimSlow, _("Slow")), _("Run the simulation at 1/8x speed")), wxSizerFlags().Expand()); gridSizer->Add(Tooltipped(new wxButton(this, ID_SimPause, _("Pause")), _("Pause the simulation")), wxSizerFlags().Expand()); gridSizer->Add(Tooltipped(new wxButton(this, ID_SimReset, _("Reset")), _("Reset the editor to initial state")), wxSizerFlags().Expand()); sizer->Add(gridSizer, wxSizerFlags().Expand()); UpdateSimButtons(); m_MainSizer->Add(sizer, wxSizerFlags().Expand().Border(wxTOP, 10)); } } void MapSidebar::OnCollapse(wxCollapsiblePaneEvent& WXUNUSED(evt)) { Freeze(); // Toggling the collapsing doesn't seem to update the sidebar layout // automatically, so do it explicitly here Layout(); Refresh(); // fixes repaint glitch on Windows Thaw(); } void MapSidebar::OnFirstDisplay() { // We do this here becase messages are used which requires simulation to be init'd m_MapSettingsCtrl->CreateWidgets(); m_MapSettingsCtrl->ReadFromEngine(); // Load the map sizes list AtlasMessage::qGetMapSizes qrySizes; qrySizes.Post(); AtObj sizes = AtlasObject::LoadFromJSON(*qrySizes.sizes); wxChoice* sizeChoice = wxDynamicCast(FindWindow(ID_RandomSize), wxChoice); for (AtIter s = sizes["Data"]["item"]; s.defined(); ++s) { long tiles = 0; wxString(s["Tiles"]).ToLong(&tiles); sizeChoice->Append(wxString(s["Name"]), (void*)(intptr_t)tiles); } sizeChoice->SetSelection(0); // Load the RMS script list AtlasMessage::qGetRMSData qry; qry.Post(); std::vector scripts = *qry.data; wxChoice* scriptChoice = wxDynamicCast(FindWindow(ID_RandomScript), wxChoice); scriptChoice->Clear(); for (size_t i = 0; i < scripts.size(); ++i) { AtObj data = AtlasObject::LoadFromJSON(scripts[i]); wxString name(data["settings"]["Name"]); scriptChoice->Append(name, new AtObjClientData(*data["settings"])); } scriptChoice->SetSelection(0); Layout(); } void MapSidebar::OnMapReload() { m_MapSettingsCtrl->ReadFromEngine(); // Reset sim test buttons POST_MESSAGE(SimPlay, (0.f, false)); POST_MESSAGE(SimStopMusic, ()); POST_MESSAGE(GuiSwitchPage, (L"page_atlas.xml")); m_SimState = SimInactive; UpdateSimButtons(); } void MapSidebar::UpdateSimButtons() { wxButton* button; button = wxDynamicCast(FindWindow(ID_SimPlay), wxButton); wxCHECK(button, ); button->Enable(m_SimState != SimPlaying); button = wxDynamicCast(FindWindow(ID_SimFast), wxButton); wxCHECK(button, ); button->Enable(m_SimState != SimPlayingFast); button = wxDynamicCast(FindWindow(ID_SimSlow), wxButton); wxCHECK(button, ); button->Enable(m_SimState != SimPlayingSlow); button = wxDynamicCast(FindWindow(ID_SimPause), wxButton); wxCHECK(button, ); button->Enable(IsPlaying(m_SimState)); button = wxDynamicCast(FindWindow(ID_SimReset), wxButton); wxCHECK(button, ); button->Enable(m_SimState != SimInactive); } void MapSidebar::OnSimPlay(wxCommandEvent& event) { float speed = 1.f; int newState = SimPlaying; if (event.GetId() == ID_SimFast) { speed = 8.f; newState = SimPlayingFast; } else if (event.GetId() == ID_SimSlow) { speed = 0.125f; newState = SimPlayingSlow; } if (m_SimState == SimInactive) { // Force update of player settings POST_MESSAGE(LoadPlayerSettings, (false)); POST_MESSAGE(SimStateSave, (L"default")); POST_MESSAGE(GuiSwitchPage, (L"page_session.xml")); POST_MESSAGE(SimPlay, (speed, true)); m_SimState = newState; } else // paused or already playing at a different speed { POST_MESSAGE(SimPlay, (speed, true)); m_SimState = newState; } UpdateSimButtons(); } void MapSidebar::OnSimPause(wxCommandEvent& WXUNUSED(event)) { if (IsPlaying(m_SimState)) { POST_MESSAGE(SimPlay, (0.f, true)); m_SimState = SimPaused; } UpdateSimButtons(); } void MapSidebar::OnSimReset(wxCommandEvent& WXUNUSED(event)) { if (IsPlaying(m_SimState)) { POST_MESSAGE(SimPlay, (0.f, true)); POST_MESSAGE(SimStateRestore, (L"default")); POST_MESSAGE(SimStopMusic, ()); POST_MESSAGE(SimPlay, (0.f, false)); POST_MESSAGE(GuiSwitchPage, (L"page_atlas.xml")); m_SimState = SimInactive; } else if (m_SimState == SimPaused) { POST_MESSAGE(SimPlay, (0.f, true)); POST_MESSAGE(SimStateRestore, (L"default")); POST_MESSAGE(SimStopMusic, ()); POST_MESSAGE(SimPlay, (0.f, false)); POST_MESSAGE(GuiSwitchPage, (L"page_atlas.xml")); m_SimState = SimInactive; } UpdateSimButtons(); } void MapSidebar::OnRandomReseed(wxCommandEvent& WXUNUSED(evt)) { // Pick a shortish randomish value wxString seed; seed << (int)floor((rand() / (float)RAND_MAX) * 10000.f); wxDynamicCast(FindWindow(ID_RandomSeed), wxTextCtrl)->SetValue(seed); } void MapSidebar::OnRandomGenerate(wxCommandEvent& WXUNUSED(evt)) { if (m_ScenarioEditor.DiscardChangesDialog()) return; wxChoice* scriptChoice = wxDynamicCast(FindWindow(ID_RandomScript), wxChoice); if (scriptChoice->GetSelection() < 0) return; // TODO: this settings thing seems a bit of a mess, // since it's mixing data from three different sources AtObj settings = m_MapSettingsCtrl->UpdateSettingsObject(); AtObj scriptSettings = dynamic_cast(scriptChoice->GetClientObject(scriptChoice->GetSelection()))->GetValue(); settings.addOverlay(scriptSettings); wxChoice* sizeChoice = wxDynamicCast(FindWindow(ID_RandomSize), wxChoice); wxString size; size << (intptr_t)sizeChoice->GetClientData(sizeChoice->GetSelection()); settings.setInt("Size", wxAtoi(size)); settings.setInt("Seed", wxAtoi(wxDynamicCast(FindWindow(ID_RandomSeed), wxTextCtrl)->GetValue())); std::string json = AtlasObject::SaveToJSON(settings); wxBusyInfo busy(_("Generating map")); wxBusyCursor busyc; wxString scriptName(settings["Script"]); // Copy the old map settings, so we don't lose them if the map generation fails AtObj oldSettings = settings; AtlasMessage::qGenerateMap qry((std::wstring)scriptName.wc_str(), json); qry.Post(); if (qry.status < 0) { // Display error message and revert to old map settings wxLogError(_("Random map script '%ls' failed"), scriptName.wc_str()); m_MapSettingsCtrl->SetMapSettings(oldSettings); } m_ScenarioEditor.NotifyOnMapReload(); } void MapSidebar::OnOpenPlayerPanel(wxCommandEvent& WXUNUSED(evt)) { m_ScenarioEditor.SelectPage(_T("PlayerSidebar")); } BEGIN_EVENT_TABLE(MapSidebar, Sidebar) EVT_COLLAPSIBLEPANE_CHANGED(wxID_ANY, MapSidebar::OnCollapse) EVT_BUTTON(ID_SimPlay, MapSidebar::OnSimPlay) EVT_BUTTON(ID_SimFast, MapSidebar::OnSimPlay) EVT_BUTTON(ID_SimSlow, MapSidebar::OnSimPlay) EVT_BUTTON(ID_SimPause, MapSidebar::OnSimPause) EVT_BUTTON(ID_SimReset, MapSidebar::OnSimReset) EVT_BUTTON(ID_RandomReseed, MapSidebar::OnRandomReseed) EVT_BUTTON(ID_RandomGenerate, MapSidebar::OnRandomGenerate) EVT_BUTTON(ID_OpenPlayerPanel, MapSidebar::OnOpenPlayerPanel) END_EVENT_TABLE();