Index: binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- binaries/data/mods/public/globalscripts/Templates.js +++ binaries/data/mods/public/globalscripts/Templates.js @@ -464,6 +464,8 @@ "maxTowerOverlap": +template.WallSet.MaxTowerOverlap, "minTowerOverlap": +template.WallSet.MinTowerOverlap }; + if (template.WallSet.Templates.TowerSlot) + ret.wallSet.templates.towerSlot = template.WallSet.Templates.TowerSlot; if (template.WallSet.Templates.WallEnd) ret.wallSet.templates.end = template.WallSet.Templates.WallEnd; if (template.WallSet.Templates.WallCurves) 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 @@ -190,12 +190,16 @@ true, // require exact template match true // include foundations ); + placementSupport.wallSlots = null; + if (placementSupport.wallSet.templates.towerSlot) + placementSupport.wallSlots = Engine.GuiInterfaceCall("GetSlots", undefined); return Engine.GuiInterfaceCall("SetWallPlacementPreview", { "wallSet": placementSupport.wallSet, "start": placementSupport.position, "end": placementSupport.wallEndPosition, "snapEntities": placementSupport.wallSnapEntities, // snapping entities (towers) for starting a wall segment + "snapSlots": placementSupport.wallSlots }); } } @@ -356,6 +360,7 @@ "pieces": wallPlacementInfo.pieces, "startSnappedEntity": wallPlacementInfo.startSnappedEnt, "endSnappedEntity": wallPlacementInfo.endSnappedEnt, + "rebuildStartTower": wallPlacementInfo.rebuildStartTower }; // make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end @@ -371,7 +376,7 @@ } } - if (hasWallSegment) + if (hasWallSegment || cmd.rebuildStartTower) { Engine.PostNetworkCommand(cmd); Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] }); @@ -692,6 +697,7 @@ { placementSupport.Reset(); inputState = INPUT_NORMAL; + Engine.GuiInterfaceCall("SetSlotVisibility", { "hidden": true }); } } else @@ -702,6 +708,7 @@ } else if (ev.button == SDL_BUTTON_RIGHT) { + Engine.GuiInterfaceCall("SetSlotVisibility", { "hidden": true }); // reset to normal input mode placementSupport.Reset(); updateBuildingPlacementPreview(); @@ -1111,6 +1118,7 @@ else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building + Engine.GuiInterfaceCall("SetSlotVisibility", { "hidden": true }); placementSupport.Reset(); inputState = INPUT_NORMAL; return true; @@ -1305,6 +1313,7 @@ placementSupport.mode = "wall"; placementSupport.wallSet = templateData.wallSet; inputState = INPUT_BUILDING_PLACEMENT; + Engine.GuiInterfaceCall("SetSlotVisibility", { "hidden": false }); } else { Index: binaries/data/mods/public/simulation/components/Foundation.js =================================================================== --- binaries/data/mods/public/simulation/components/Foundation.js +++ binaries/data/mods/public/simulation/components/Foundation.js @@ -39,6 +39,13 @@ this.maxProgress = 0; this.initialised = true; + + let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); + if (!cmpObstruction) + return; + + for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction()) + Engine.DestroyEntity(ent); }; /** 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 @@ -1168,6 +1168,7 @@ "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID + "snappedSlot": false }; let end = { @@ -1175,6 +1176,7 @@ "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID + "snappedSlot": false }; // -------------------------------------------------------------------------------- @@ -1240,6 +1242,33 @@ if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z)) end.pos = undefined; + // Try to snap to existing slots. + if (cmd.snapSlots) + { + // Determined through trial and error. + const snapMultiplier = 0.5; + let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * snapMultiplier; + let startSnapData = this.GetFoundationSnapData(player, { + "x": start.pos.x, + "z": start.pos.z, + "template": wallSet.templates.tower, + "snapEntities": cmd.snapSlots, + "snapRadius": snapRadius + }); + + if (startSnapData) + { + start.pos.x = startSnapData.x; + start.pos.z = startSnapData.z; + start.angle = startSnapData.angle; + start.snapped = true; + start.snappedSlot = true; + + if (startSnapData.ent) + start.snappedEnt = startSnapData.ent; + } + } + // See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData). @@ -1323,7 +1352,18 @@ // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed // by the foundation it snaps to. - if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) + if (start.snappedSlot) + { + let slotEntityObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction); + previewEntities.unshift({ + "template": wallSet.templates.tower, + "pos": start.pos, + "angle": start.angle, + "controlGroups": [slotEntityObstruction.GetControlGroup()], + "slot": true + }); + } + else if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) { let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction); if (previewEntities.length > 0 && startEntObstruction) @@ -1430,7 +1470,10 @@ let allPiecesValid = true; let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity - for (let i = 0; i < previewEntities.length; ++i) + // If the first element is a tower slot, only build that preview. + const length = previewEntities.length && previewEntities[0].slot ? 1 : previewEntities.length; + + for (let i = 0; i < length; ++i) { let entInfo = previewEntities[i]; @@ -1545,6 +1588,9 @@ // check for collisions with entities that share both control groups. if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1) validPlacement = cmpObstruction.CheckDuplicateFoundation(); + + if (entInfo.slot) + validPlacement = true; } allPiecesValid = allPiecesValid && validPlacement; @@ -1603,6 +1649,7 @@ if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) result.startSnappedEnt = start.snappedEnt; + result.rebuildStartTower = start.snappedSlot; // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed, // i.e. are included in result.pieces (see docs for the result object). if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid) @@ -1892,6 +1939,34 @@ Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled); }; +GuiInterface.prototype.GetSlots = function(player, data) +{ + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + let entities = cmpRangeManager.GetEntitiesByPlayer(player); + return entities.filter((ent) => { + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); + return cmpIdentity && cmpIdentity.HasClass("Slot"); + }); +}; + +GuiInterface.prototype.SetSlotVisibility = function(player, data) +{ + let slots = this.GetSlots(player, undefined); + if (!slots || !slots.length) + return; + + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + for (let ent of slots) + { + let cmpVisibility = Engine.QueryInterface(ent, IID_Visibility); + if (!cmpVisibility || cmpVisibility.IsHidden() == data.hidden) + continue; + + cmpVisibility.SetHidden(data.hidden); + cmpRangeManager.RequestVisibilityUpdate(ent); + } +}; + GuiInterface.prototype.GetTraderNumber = function(player) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); @@ -2005,7 +2080,9 @@ "GetTraderNumber": 1, "GetTradingGoods": 1, "IsTemplateModified": 1, - "ResetTemplateModified": 1 + "ResetTemplateModified": 1, + "GetSlots": 1, + "SetSlotVisibility": 1 }; GuiInterface.prototype.ScriptCall = function(player, name, args) Index: binaries/data/mods/public/simulation/components/Health.js =================================================================== --- binaries/data/mods/public/simulation/components/Health.js +++ binaries/data/mods/public/simulation/components/Health.js @@ -47,6 +47,11 @@ "" + "" + "" + + "" + + "" + + "" + + "" + + "" + "" + "" + ""; @@ -254,7 +259,10 @@ PlaySound("death", this.entity); if (this.template.SpawnEntityOnDeath) - this.CreateDeathSpawnedEntity(); + this.CreateDeathSpawnedEntity(this.template.SpawnEntityOnDeath); + + if (this.template.SpawnSlotOnDeath) + this.CreateDeathSpawnedSlot(this.template.SpawnSlotOnDeath); switch (this.template.DeathType) { @@ -312,34 +320,13 @@ Health.prototype.CreateCorpse = function(leaveResources) { - // If the unit died while not in the world, don't create any corpse for it - // since there's nowhere for the corpse to be placed - let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); - if (!cmpPosition.IsInWorld()) - return INVALID_ENTITY; - // Either creates a static local version of the current entity, or a // persistent corpse retaining the ResourceSupply element of the parent. let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let templateName = cmpTemplateManager.GetCurrentTemplateName(this.entity); - let corpse; - if (leaveResources) - corpse = Engine.AddEntity("resource|" + templateName); - else - corpse = Engine.AddLocalEntity("corpse|" + templateName); - - // Copy various parameters so it looks just like us - - let cmpCorpsePosition = Engine.QueryInterface(corpse, IID_Position); - let pos = cmpPosition.GetPosition(); - cmpCorpsePosition.JumpTo(pos.x, pos.z); - let rot = cmpPosition.GetRotation(); - cmpCorpsePosition.SetYRotation(rot.y); - cmpCorpsePosition.SetXZRotation(rot.x, rot.z); - - let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - let cmpCorpseOwnership = Engine.QueryInterface(corpse, IID_Ownership); - cmpCorpseOwnership.SetOwner(cmpOwnership.GetOwner()); + let corpse = this.CreateDeathSpawnedEntity((leaveResources ? "resource|" : "corpse|") + templateName, !leaveResources); + if (corpse == INVALID_ENTITY) + return INVALID_ENTITY; let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); let cmpCorpseVisual = Engine.QueryInterface(corpse, IID_Visual); @@ -351,7 +338,32 @@ return corpse; }; -Health.prototype.CreateDeathSpawnedEntity = function() +Health.prototype.CreateDeathSpawnedSlot = function(entityTemplate) +{ + let spawnedEntity = this.CreateDeathSpawnedEntity(entityTemplate, false); + if (spawnedEntity == INVALID_ENTITY) + return INVALID_ENTITY; + + let cmpVisual = Engine.QueryInterface(spawnedEntity, IID_Visual); + if (cmpVisual) + cmpVisual.SetShadingColor(0, 1, 0, 1); + + let cmpVisibility = Engine.QueryInterface(spawnedEntity, IID_Visibility); + if (cmpVisibility) + { + cmpVisibility.SetActivated(true); + cmpVisibility.SetHidden(true); + } + + let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); + let cmpSpawnedObstruction = Engine.QueryInterface(spawnedEntity, IID_Obstruction); + if (cmpObstruction && cmpSpawnedObstruction) + cmpSpawnedObstruction.SetControlGroup(cmpObstruction.GetControlGroup()); + + return spawnedEntity; +}; + +Health.prototype.CreateDeathSpawnedEntity = function(entityTemplate, isLocal = true) { // If the unit died while not in the world, don't spawn a death entity for it // since there's nowhere for it to be placed @@ -360,15 +372,20 @@ return INVALID_ENTITY; // Create SpawnEntityOnDeath entity - let spawnedEntity = Engine.AddLocalEntity(this.template.SpawnEntityOnDeath); + let spawnedEntity = isLocal ? Engine.AddLocalEntity(entityTemplate) : Engine.AddEntity(entityTemplate); // Move to same position let cmpSpawnedPosition = Engine.QueryInterface(spawnedEntity, IID_Position); - let pos = cmpPosition.GetPosition(); - cmpSpawnedPosition.JumpTo(pos.x, pos.z); - let rot = cmpPosition.GetRotation(); - cmpSpawnedPosition.SetYRotation(rot.y); - cmpSpawnedPosition.SetXZRotation(rot.x, rot.z); + if (cmpSpawnedPosition) + { + let pos = cmpPosition.GetPosition(); + let rot = cmpPosition.GetRotation(); + cmpSpawnedPosition.JumpTo(pos.x, pos.z); + cmpSpawnedPosition.SetYRotation(rot.y); + cmpSpawnedPosition.SetXZRotation(rot.x, rot.z); + } + else + error("entity " + this.template.SpawnSlotOnDeath + " has not position"); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); let cmpSpawnedOwnership = Engine.QueryInterface(spawnedEntity, IID_Ownership); Index: binaries/data/mods/public/simulation/components/Visibility.js =================================================================== --- binaries/data/mods/public/simulation/components/Visibility.js +++ binaries/data/mods/public/simulation/components/Visibility.js @@ -26,6 +26,7 @@ this.preview = this.template.Preview == "true"; this.activated = false; + this.hidden = false; if (this.preview || this.corpse) this.SetActivated(true); @@ -60,6 +61,9 @@ */ Visibility.prototype.GetVisibility = function(player, isVisible, isExplored) { + if (this.hidden) + return VIS_HIDDEN; + if (this.preview) { // For the owner only, mock the "RetainInFog" behavior @@ -94,4 +98,14 @@ return this.alwaysVisible; }; +Visibility.prototype.SetHidden = function(hidden) +{ + this.hidden = hidden; +}; + +Visibility.prototype.IsHidden = function() +{ + return this.hidden; +}; + Engine.RegisterComponentType(IID_Visibility, "Visibility", Visibility); Index: binaries/data/mods/public/simulation/components/WallSet.js =================================================================== --- binaries/data/mods/public/simulation/components/WallSet.js +++ binaries/data/mods/public/simulation/components/WallSet.js @@ -36,6 +36,11 @@ "" + "" + "" + + "" + + "" + + "" + + "" + + "" + "" + "" + "" + 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 @@ -1193,13 +1193,13 @@ if (cmd.pieces.length <= 0) return; - if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower) + if (!cmd.rebuildStartTower && cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side"); return; } - if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower) + if (!cmd.rebuildEndTower && cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side"); return; @@ -1250,7 +1250,9 @@ } lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup(); - //warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup); + // Set different control groups to delete upon construction. + if (cmd.rebuildStartTower) + cmpSnappedStartObstruction.SetControlGroup(0); } var i = 0; @@ -1314,15 +1316,15 @@ if (i > 0) { - //warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup); - var cmpPreviousObstruction = Engine.QueryInterface(pieces[i-1].ent, IID_Obstruction); - // TODO: ensure that cmpPreviousObstruction exists - // TODO: ensure that the previous obstruction does not yet have a secondary control group set - cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup); + let cmpPreviousObstruction = Engine.QueryInterface(pieces[i - 1].ent, IID_Obstruction); + let previousGroup2 = cmpPreviousObstruction.GetControlGroup2(); + if(previousGroup2 == INVALID_ENTITY && cmpPreviousObstruction) + cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup); } - // TODO: ensure that cmpTowerObstruction exists - cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group + // If we are not rebuilding it, give the tower its unique control group. + if (!cmd.rebuildStartTower && cmpTowerObstruction) + cmpTowerObstruction.SetControlGroup(newTowerControlGroup); lastTowerIndex = i; lastTowerControlGroup = newTowerControlGroup; Index: binaries/data/mods/public/simulation/templates/structures/palisades_tower.xml =================================================================== --- binaries/data/mods/public/simulation/templates/structures/palisades_tower.xml +++ binaries/data/mods/public/simulation/templates/structures/palisades_tower.xml @@ -12,6 +12,7 @@ 0.75 + slots/slot_palisade_tower structures/wallset_palisade Index: binaries/data/mods/public/simulation/templates/structures/wallset_palisade.xml =================================================================== --- binaries/data/mods/public/simulation/templates/structures/wallset_palisade.xml +++ binaries/data/mods/public/simulation/templates/structures/wallset_palisade.xml @@ -17,6 +17,7 @@ structures/palisades_short structures/palisades_curve structures/palisades_end + slots/slot_palisade_tower Index: binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml +++ binaries/data/mods/public/simulation/templates/template_structure_defensive_palisade.xml @@ -15,6 +15,7 @@ 1000 decay|rubble/rubble_stone_2x2 + true Palisade Index: binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_tower.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_tower.xml +++ binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_tower.xml @@ -49,6 +49,8 @@ 4000 decay|rubble/rubble_stone_wall_tower + slots/slot_wall_tower + true Wall Turret Index: binaries/data/mods/public/simulation/templates/template_wallset.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_wallset.xml +++ binaries/data/mods/public/simulation/templates/template_wallset.xml @@ -20,6 +20,9 @@ false + + slots/slot_wall_tower + 0.85 0.05 Index: source/simulation2/components/CCmpRangeManager.cpp =================================================================== --- source/simulation2/components/CCmpRangeManager.cpp +++ source/simulation2/components/CCmpRangeManager.cpp @@ -1600,15 +1600,6 @@ int i = (pos.X / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); int j = (pos.Y / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); - // Reveal flag makes all positioned entities visible and all mirages useless - if (GetLosRevealAll(player)) - { - if (LosIsOffWorld(i, j) || cmpMirage) - return VIS_HIDDEN; - else - return VIS_VISIBLE; - } - // Get visible regions CLosQuerier los(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); @@ -1627,7 +1618,10 @@ return cmpVisibility->GetVisibility(player, los.IsVisible(i, j), los.IsExplored(i, j)); } - // Else, default behavior + // Else, default behavior. + // Reveal flag makes all positioned entities visible and all mirages useless. + if (GetLosRevealAll(player)) + return LosIsOffWorld(i, j) || cmpMirage ? VIS_HIDDEN : VIS_VISIBLE; if (los.IsVisible(i, j)) {