Index: binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- binaries/data/mods/public/globalscripts/Templates.js +++ binaries/data/mods/public/globalscripts/Templates.js @@ -273,6 +273,8 @@ if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = getEntityValue("BuildRestrictions/Distance/MaxDistance"); } + if (template.BuildRestrictions.Sockets) + ret.buildRestrictions.sockets = template.BuildRestrictions.Sockets._string } if (template.TrainingRestrictions) Index: binaries/data/mods/public/gui/session/input.js =================================================================== --- binaries/data/mods/public/gui/session/input.js +++ binaries/data/mods/public/gui/session/input.js @@ -131,10 +131,19 @@ { if (placementSupport.template && placementSupport.position) { - var result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { + // Fetch an updated list of snapping candidate entities + placementSupport.socketSnapEntities = Engine.PickSimilarEntities( + "template_socket", + true, // Include offscreen. + true, // ToDo: Do not require an exact template match. + true // Include foundations. + ); + + let result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, + "snapEntities": placementSupport.socketSnapEntities, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed }); @@ -176,7 +185,7 @@ placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" + sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange }); } - return true; + return result; } } else if (placementSupport.mode === "wall") @@ -296,7 +305,8 @@ return false; } - if (!updateBuildingPlacementPreview()) + let buildingPlacementInfo = updateBuildingPlacementPreview(); + if (!buildingPlacementInfo) { // invalid location - don't build it // TODO: play a sound? @@ -308,14 +318,15 @@ Engine.PostNetworkCommand({ "type": "construct", "template": placementSupport.template, - "x": placementSupport.position.x, - "z": placementSupport.position.z, + "x": buildingPlacementInfo.x, + "z": buildingPlacementInfo.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed, "entities": selection, "autorepair": true, "autocontinue": true, - "queued": queued + "queued": queued, + "snapEntities": buildingPlacementInfo.snapEntities }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] }); @@ -1310,6 +1321,7 @@ { placementSupport.mode = "building"; placementSupport.template = buildTemplate; + placementSupport.socketSnapClasses = templateData.sockets; inputState = INPUT_BUILDING_PLACEMENT; } Index: binaries/data/mods/public/gui/session/placement.js =================================================================== --- binaries/data/mods/public/gui/session/placement.js +++ binaries/data/mods/public/gui/session/placement.js @@ -15,6 +15,8 @@ this.template = null; this.tooltipMessage = ""; // tooltip text to show while the user is placing a structure this.tooltipError = false; + this.socketClasses = null; + this.socketSnapEntities = null; this.wallSet = null; // maps types of wall pieces ("tower", "long", "short", ...) to template names this.wallSnapEntities = null; // list of candidate entities to snap the starting and (!) ending positions to when building walls this.wallEndPosition = null; Index: binaries/data/mods/public/simulation/components/BuildRestrictions.js =================================================================== --- binaries/data/mods/public/simulation/components/BuildRestrictions.js +++ binaries/data/mods/public/simulation/components/BuildRestrictions.js @@ -18,6 +18,7 @@ "land" + "shore" + "land-shore"+ + "socket"+ "" + "" + "" + @@ -45,11 +46,23 @@ "" + "" + "" + + "" + + "" + + "" + + "" + + "tokens" + + "" + + "" + + "" + ""; BuildRestrictions.prototype.Init = function() { this.territories = this.template.Territory.split(/\s+/); + if (this.template.Sockets) + this.sockets = this.template.Sockets._string.split(/\s+/); + if (this.template.PlacementType == "socket" && !this.sockets) + warn("Placement type 'Socket' without a socket specified, this building (" + this.entity + ") can never be built."); }; /** @@ -144,7 +157,7 @@ var ret = cmpObstruction.CheckFoundation(passClassName, false); } - if (ret != "success") + if (ret != "success" && this.template.PlacementType != "socket") { switch (ret) { @@ -229,6 +242,14 @@ return result; // Fail } } + if (this.template.PlacementType == "socket") + { + if (!this.CheckSocketPlacement(pos)) + { + result.message = markForTranslation("%(name)s must be built on a valid socket."); + return result; // Fail + } + } let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); @@ -298,6 +319,21 @@ return result; }; +BuildRestrictions.prototype.CheckSocketPlacement = function(pos) +{ + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + let classes = this.sockets; + + let filter = function(id) + { + let cmpIdentity = Engine.QueryInterface(id, IID_Identity); + return cmpIdentity && MatchesClassList(classes, cmpIdentity.GetClassesList()); + }; + let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + + return cmpRangeManager.ExecuteQuery(this.entity, 0, 0, cmpPlayerManager.GetAllPlayers(), IID_BuildSlot).some(filter); +}; + BuildRestrictions.prototype.GetCategory = function() { return this.template.Category; Index: binaries/data/mods/public/simulation/components/BuildSlot.js =================================================================== --- binaries/data/mods/public/simulation/components/BuildSlot.js +++ binaries/data/mods/public/simulation/components/BuildSlot.js @@ -1,10 +1,3 @@ -function Settlement() {} - -Settlement.prototype.Schema = - ""; - -Engine.RegisterComponentType(IID_Settlement, "Settlement", Settlement); - /* * TODO: the vague plan is that this should keep track of who currently owns the settlement, * and some other code can detect this (or get notified of changes) when it needs to. @@ -13,3 +6,77 @@ * tell us that its player owns us, and move us back into our original position when the building * is destroyed. Don't know if that's a sensible plan, though. */ +class BuildSlot +{ + get Schema() + { + return "Specifies this is a building slot, an entity where a structure can be placed upon." + + "" + + "" + + "true" + + "" + + "" + + "" + + "" + + ""; + } + + Init() + { + this.owner = INVALID_PLAYER; + }; + + /** + * Initialises construction, thus rendering this socket useless. + * + * @param {number} player - The player requesting the initialisation. + * + * @return {boolean} Whether the initialisation was successful. + */ + InitConstruction(player) + { + this.owner = player; + + if (this.template.HideUponUse != "true") + return true; + + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition) + return false; + + this.previousPosition = cmpPosition.GetPosition(); + cmpPosition.MoveOutOfWorld(); + + return true; + }; + + /** + * Resets this socket by setting the owner to -1 and moving back to its former position. + */ + Reset() + { + this.owner = INVALID_PLAYER; + + if (!this.previousPosition) + return; + + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition) + return; + + cmpPosition.JumpTo(this.previousPosition.x, this.previousPosition.z); + delete this.previousPosition; + }; + + /** + * Get the current owner. + * + * @return {number} The current owner of this build slot. + */ + GetOwner() + { + return this.owner; + } +} + +Engine.RegisterComponentType(IID_BuildSlot, "BuildSlot", BuildSlot); 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 @@ -1040,6 +1040,29 @@ "translateMessage": false, "translateParameters": [], }; + let snapData; + if (cmd.snapEntities) + { + let snapRadius = 10; // ToDo: Adapt. + snapData = this.GetFoundationSnapData(player, { + "x": cmd.x, + "z": cmd.z, + "template": "template_socket", + "snapEntities": cmd.snapEntities, + "snapRadius": snapRadius, + }); + + if (snapData) + { + cmd.x = snapData.x; + cmd.z = snapData.z; + cmd.angle = snapData.angle; + cmd.snapped = true; + + if (snapData.ent) + cmd.snappedEnt = snapData.ent; + } + } // See if we're changing template if (!this.placementEntity || this.placementEntity[0] != cmd.template) @@ -1077,6 +1100,14 @@ else result = cmpBuildRestrictions.CheckPlacement(); + if (snapData) + { + result.x = snapData.x; + result.z = snapData.z; + if (snapData.entity) + result.snappedEnt = snapData.snappedEntity + } + let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes); @@ -1678,7 +1709,8 @@ return position; } - if (template.BuildRestrictions.PlacementType == "shore") + if (template.BuildRestrictions && + template.BuildRestrictions.PlacementType == "shore") { let angle = GetDockAngle(template, data.x, data.z); if (angle !== undefined) Index: binaries/data/mods/public/simulation/components/Settlement.js =================================================================== --- binaries/data/mods/public/simulation/components/Settlement.js +++ binaries/data/mods/public/simulation/components/Settlement.js @@ -1,15 +0,0 @@ -function Settlement() {} - -Settlement.prototype.Schema = - ""; - -Engine.RegisterComponentType(IID_Settlement, "Settlement", Settlement); - -/* - * TODO: the vague plan is that this should keep track of who currently owns the settlement, - * and some other code can detect this (or get notified of changes) when it needs to. - * A civcenter's BuildRestrictions component will see that it's being built on this settlement, - * call MoveOutOfWorld on us (so we're invisible and only the building is visible/selectable), - * tell us that its player owns us, and move us back into our original position when the building - * is destroyed. Don't know if that's a sensible plan, though. - */ Index: binaries/data/mods/public/simulation/components/interfaces/BuildSlot.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/interfaces/BuildSlot.js @@ -0,0 +1 @@ +Engine.RegisterInterface("BuildSlot"); Index: binaries/data/mods/public/simulation/components/tests/test_BuildSlot.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/tests/test_BuildSlot.js @@ -0,0 +1,13 @@ +Engine.LoadComponentScript("interfaces/BuildSlot.js"); +Engine.LoadComponentScript("BuildSlot.js"); + +const buildSlotId = 1; +const playerId = 2; + +let cmpBuildSlot = ConstructComponent(buildSlotId, "BuildSlot", { + "HideUponUse": false +}); + +TS_ASSERT_UNEVAL_EQUALS(cmpBuildSlot.GetOwner(), INVALID_PLAYER); +TS_ASSERT_UNEVAL_EQUALS(cmpBuildSlot.InitConstruction(playerId), true); +TS_ASSERT_UNEVAL_EQUALS(cmpBuildSlot.GetOwner(), playerId); Index: binaries/data/mods/public/simulation/templates/template_socket.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/templates/template_socket.xml @@ -0,0 +1,93 @@ + + + + + land + own + special + + + false + + + 500 + 0.5 + 5.0 + + + 0 + 0 + 1 + + 0 + 0 + 0 + 0 + + + + 10 + + 0.85 + 0.65 + 0.35 + + corpse + 0 + 0 + true + + + gaia + Socket + socket_house + true + + + true + true + true + true + false + false + false + false + + + + + + 0 + upright + false + 0.0 + 6.0 + + + + + outline_border.png + outline_border_mask.png + 0.4 + + + + + 6.0 + 0.6 + 12.0 + + + true + false + false + false + + + + false + true + false + structures/fndn_3x3.xml + + Index: binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml +++ binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml @@ -1,7 +1,9 @@ + socket House + socket_house 300 Index: binaries/data/mods/public/simulation/templates/template_unit_infantry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry.xml @@ -24,6 +24,7 @@ 1.0 + template_socket structures/{civ}_house structures/{civ}_storehouse structures/{civ}_farmstead Index: source/simulation2/scripting/JSInterface_Simulation.h =================================================================== --- source/simulation2/scripting/JSInterface_Simulation.h +++ source/simulation2/scripting/JSInterface_Simulation.h @@ -34,6 +34,7 @@ std::vector GetEntitiesWithStaticObstructionOnScreen(ScriptInterface::CxPrivate* pCxPrivate); JS::Value GetEdgesOfStaticObstructionsOnScreenNearTo(ScriptInterface::CxPrivate* pCxPrivate, entity_pos_t x, entity_pos_t z); std::vector PickSimilarPlayerEntities(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName, bool includeOffScreen, bool matchRank, bool allowFoundations); + std::vector PickSimilarEntities(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName, bool includeOffScreen, bool matchRank, bool allowFoundations); JS::Value GetAIs(ScriptInterface::CxPrivate* pCxPrivate); void SetBoundingBoxDebugOverlay(ScriptInterface::CxPrivate* pCxPrivate, bool enabled); Index: source/simulation2/scripting/JSInterface_Simulation.cpp =================================================================== --- source/simulation2/scripting/JSInterface_Simulation.cpp +++ source/simulation2/scripting/JSInterface_Simulation.cpp @@ -195,6 +195,11 @@ return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetViewedPlayerID(), includeOffScreen, matchRank, false, allowFoundations); } +std::vector JSI_Simulation::PickSimilarEntities(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& templateName, bool includeOffScreen, bool matchRank, bool allowFoundations) +{ + return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, INVALID_PLAYER, includeOffScreen, matchRank, false, allowFoundations); +} + JS::Value JSI_Simulation::GetAIs(ScriptInterface::CxPrivate* pCxPrivate) { return ICmpAIManager::GetAIs(*(pCxPrivate->pScriptInterface)); @@ -218,5 +223,6 @@ scriptInterface.RegisterFunction, &GetEntitiesWithStaticObstructionOnScreen>("GetEntitiesWithStaticObstructionOnScreen"); scriptInterface.RegisterFunction("GetEdgesOfStaticObstructionsOnScreenNearTo"); scriptInterface.RegisterFunction, std::string, bool, bool, bool, &PickSimilarPlayerEntities>("PickSimilarPlayerEntities"); + scriptInterface.RegisterFunction, std::string, bool, bool, bool, &PickSimilarEntities>("PickSimilarEntities"); scriptInterface.RegisterFunction("SetBoundingBoxDebugOverlay"); }