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,9 @@ if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = getEntityValue("BuildRestrictions/Distance/MaxDistance"); } + + if (template.BuildRestrictions.Sockets) + ret.buildRestrictions.sockets = template.BuildRestrictions.Sockets._string.split(/\s+/); } 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,11 +131,14 @@ { if (placementSupport.template && placementSupport.position) { - var result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { + let result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, + "snapClasses": placementSupport.socketSnapClasses, + // Ideally we'd only get entities with certain class. + "snapEntities": placementSupport.socketSnapClasses.length && Engine.GetEntitiesWithStaticObstructionOnScreen(), "actorSeed": placementSupport.actorSeed }); @@ -176,7 +179,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 +299,8 @@ return false; } - if (!updateBuildingPlacementPreview()) + let buildingPlacementInfo = updateBuildingPlacementPreview(); + if (!buildingPlacementInfo) { // invalid location - don't build it // TODO: play a sound? @@ -308,14 +312,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, + "snapEntity": buildingPlacementInfo.snappedEnt }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] }); @@ -1086,21 +1091,19 @@ { placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); - if (isSnapToEdgesEnabled()) + let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { + "template": placementSupport.template, + "x": placementSupport.position.x, + "z": placementSupport.position.z, + "snapEntities": placementSupport.socketSnapEntities, + "snapToEdges": isSnapToEdgesEnabled() && Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( + placementSupport.position.x, placementSupport.position.z) + }); + if (snapData) { - let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { - "template": placementSupport.template, - "x": placementSupport.position.x, - "z": placementSupport.position.z, - "snapToEdges": Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( - placementSupport.position.x, placementSupport.position.z) - }); - if (snapData) - { - placementSupport.angle = snapData.angle; - placementSupport.position.x = snapData.x; - placementSupport.position.z = snapData.z; - } + placementSupport.angle = snapData.angle; + placementSupport.position.x = snapData.x; + placementSupport.position.z = snapData.z; } g_DragStart = new Vector2D(ev.x, ev.y); @@ -1311,6 +1314,7 @@ placementSupport.mode = "building"; placementSupport.template = buildTemplate; inputState = INPUT_BUILDING_PLACEMENT; + placementSupport.socketSnapClasses = templateData.buildRestrictions.sockets || []; } if (templateData.attack && 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,8 @@ var ret = cmpObstruction.CheckFoundation(passClassName, false); } - if (ret != "success") + // When a socket is needed obstruction ought to be ignored. + if (ret != "success" && this.template.PlacementType != "socket") { switch (ret) { @@ -230,6 +244,12 @@ } } + if (this.template.PlacementType == "socket" && !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); let templateName = cmpTemplateManager.GetCurrentTemplateName(this.entity); @@ -298,6 +318,24 @@ return result; }; +/** + * Whether this entity is placed on the correct socket. + */ +BuildRestrictions.prototype.CheckSocketPlacement = function() +{ + 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,98 @@ * 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 +{ + Init() + { + this.occupant = INVALID_ENTITY; + } + + /** + * Initialises construction, thus rendering this socket useless. + * + * @param {number} player - The player requesting the initialisation. + * @param {number} entity - The entity being built on/in this slot. + * + * @return {boolean} Whether the initialisation was successful. + */ + InitConstruction(entity) + { + this.occupant = entity; + + 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.occupant = INVALID_ENTITY; + + 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. + */ + GetOccupant() + { + return this.occupant; + } +} + +BuildSlot.prototype.Schema = + "Specifies this is a building slot, an entity where a structure can be placed upon." + + "" + + "" + + "true" + + "" + + "" + + "" + + "" + + ""; + +BuildSlot.prototype.OnGlobalEntityRenamed = function(msg) +{ + if (msg.entity != this.occupant) + return; + + // Our occupant died, reset our state. + if (msg.newentity == INVALID_ENTITY) + this.Reset(); + else + this.occupant = msg.newentity; +}; + +BuildSlot.prototype.OnGlobalOwnershipChanged = function(msg) +{ + if (msg.entity != this.occupant) + return; + + // Our occupant died, reset our state. + if (msg.to == INVALID_PLAYER) + this.Reset(); +}; + +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 @@ -1041,6 +1041,30 @@ "translateParameters": [], }; + let snapData; + if (cmd.snapEntities) + { + let snapRadius = 10; // ToDo: Adapt. + snapData = this.GetFoundationSnapData(player, { + "x": cmd.x, + "z": cmd.z, + "template": cmd.template, + "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 +1101,16 @@ else result = cmpBuildRestrictions.CheckPlacement(); + result.x = cmd.x; + result.z = cmd.z; + if (snapData) + { + result.x = snapData.x; + result.z = snapData.z; + if (snapData.ent) + result.snappedEnt = snapData.ent + } + let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes); 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 buildingId = 2; + +let cmpBuildSlot = ConstructComponent(buildSlotId, "BuildSlot", { + "HideUponUse": "false" +}); + +TS_ASSERT_UNEVAL_EQUALS(cmpBuildSlot.GetOccupant(), INVALID_ENTITY); +TS_ASSERT_UNEVAL_EQUALS(cmpBuildSlot.InitConstruction(buildingId), true); +TS_ASSERT_UNEVAL_EQUALS(cmpBuildSlot.GetOccupant(), buildingId); Index: binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Commands.js +++ binaries/data/mods/public/simulation/helpers/Commands.js @@ -1136,6 +1136,13 @@ if (cmpVisual && cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); + if (cmd.snapEntity) + { + let cmpBuildSlot = Engine.QueryInterface(cmd.snapEntity, IID_BuildSlot); + if (cmpBuildSlot) + cmpBuildSlot.InitConstruction(ent); + } + // Initialise the foundation var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); cmpFoundation.InitialiseConstruction(player, cmd.template); 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 + + + true + + + 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}_civil_centre structures/{civ}_crannog structures/{civ}_military_colony