diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg index 20d705e603..f7539414a6 100644 --- a/binaries/data/config/default.cfg +++ b/binaries/data/config/default.cfg @@ -237,7 +237,7 @@ quickload = "Shift+F8" [hotkey.camera] reset = "R" ; Reset camera rotation to default. -follow = "F" ; Follow the first unit in the selection +follow = "" ; Follow the first unit in the selection rallypointfocus = "" ; Focus the camera on the rally point of the selected building lastattackfocus = "Space" ; Focus the camera on the last notified attack zoom.in = Plus, NumPlus ; Zoom camera in (continuous control) @@ -351,7 +351,7 @@ unloadturrets = "U" ; Unload turreted units. leaveturret = "U" ; Leave turret point. move = "" ; Modifier to move to a point instead of another action (e.g. gather) capture = "C" ; Modifier to capture instead of another action (e.g. attack) -attack = "" ; Modifier to attack instead of another action (e.g. capture) +attack = "F" ; Modifier to attack instead of another action (e.g. capture) attackmove = Ctrl ; Modifier to attackmove when clicking on a point attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point garrison = Ctrl ; Modifier to garrison when clicking on building @@ -377,6 +377,7 @@ toggledefaultformation = "" ; Switch between null default formation and the las flare = K ; Modifier to send a flare to your allies flareactivate = "" ; Modifier to activate the mode to send a flare to your allies calltoarms = "" ; Modifier to call the selected units to the arms. +focusfire = "F" ; Modifier to control exclusively a building's arrows if it can attack ; Overlays showstatusbars = Tab ; Toggle display of status bars devcommands.toggle = "Alt+D" ; Toggle developer commands panel diff --git a/binaries/data/mods/public/art/textures/cursors/action-target.png b/binaries/data/mods/public/art/textures/cursors/action-target.png new file mode 100644 index 0000000000..5dea77b50e --- /dev/null +++ b/binaries/data/mods/public/art/textures/cursors/action-target.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5095290f717f89b2f00a45dac4e4bbafc2c12879e3b50897e57e384e7efec3ee +size 1690 diff --git a/binaries/data/mods/public/art/textures/cursors/action-target.txt b/binaries/data/mods/public/art/textures/cursors/action-target.txt new file mode 100644 index 0000000000..2fb73a07ec --- /dev/null +++ b/binaries/data/mods/public/art/textures/cursors/action-target.txt @@ -0,0 +1 @@ +1 1 diff --git a/binaries/data/mods/public/art/textures/ui/tips/building_control.png b/binaries/data/mods/public/art/textures/ui/tips/building_control.png new file mode 100644 index 0000000000..da840a5ac6 --- /dev/null +++ b/binaries/data/mods/public/art/textures/ui/tips/building_control.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88f38275bfb85bb5e724d974e639d71f94c5625f2e21ec0a9fbbe1636399fbe2 +size 522853 diff --git a/binaries/data/mods/public/audio/attack/weapon/bow_launch_building.xml b/binaries/data/mods/public/audio/attack/weapon/bow_launch_building.xml new file mode 100644 index 0000000000..34138c024c --- /dev/null +++ b/binaries/data/mods/public/audio/attack/weapon/bow_launch_building.xml @@ -0,0 +1,13 @@ + + + 10 + 1 + 0 + 1 + 1 + 1.2 + 0.8 + 500 + audio/attack/weapon/ + sling_210.ogg + diff --git a/binaries/data/mods/public/gui/hotkeys/spec/ingame.json b/binaries/data/mods/public/gui/hotkeys/spec/ingame.json index 933ed51546..faf01cc182 100644 --- a/binaries/data/mods/public/gui/hotkeys/spec/ingame.json +++ b/binaries/data/mods/public/gui/hotkeys/spec/ingame.json @@ -182,6 +182,10 @@ "session.calltoarms": { "name": "Call to arms", "desc": "Send the selected units on attack move to the specified location after dropping resources." + }, + "session.focusfire": { + "name": "Focus Fire", + "desc": "Exclusively control a building's arrows without setting a rallypoint." } } } diff --git a/binaries/data/mods/public/gui/reference/tips/texts/building_control.txt b/binaries/data/mods/public/gui/reference/tips/texts/building_control.txt new file mode 100644 index 0000000000..7f243de3f5 --- /dev/null +++ b/binaries/data/mods/public/gui/reference/tips/texts/building_control.txt @@ -0,0 +1,4 @@ +CONTROLLING BUILDINGS +You can control a building's arrows independently from its rally point: +Right-click with the hotkey "Focus Fire" ("F" by default) to direct all arrow fire to a target unit or building. (Left) +Right-click alone to set the rally point on a unit. (Right) diff --git a/binaries/data/mods/public/gui/reference/tips/tipfiles.json b/binaries/data/mods/public/gui/reference/tips/tipfiles.json index 3436b123b0..6b4acfa1de 100644 --- a/binaries/data/mods/public/gui/reference/tips/tipfiles.json +++ b/binaries/data/mods/public/gui/reference/tips/tipfiles.json @@ -41,6 +41,12 @@ "briton_war_dog.png" ] }, + { + "textFile": "building_control.txt", + "imageFiles": [ + "building_control.png" + ] + }, { "textFile": "carth_sacred_band.txt", "imageFiles": [ diff --git a/binaries/data/mods/public/gui/session/unit_actions.js b/binaries/data/mods/public/gui/session/unit_actions.js index ca4880f9b5..d3f59cb31c 100644 --- a/binaries/data/mods/public/gui/session/unit_actions.js +++ b/binaries/data/mods/public/gui/session/unit_actions.js @@ -1027,6 +1027,86 @@ var g_UnitActions = "specificness": 41, }, + "focus-fire": + { + "execute": function(position, action, selection, queued, pushFront) + { + Engine.PostNetworkCommand({ + "type": "focus-fire", + "entities": selection, + "x": position.x, + "z": position.z, + "target": action.target, + "data": action.data, + "queued": queued, + "pushFront": pushFront + }); + + if (action.data.sound) + Engine.GuiInterfaceCall("PlaySound", { + "name": "focus_fire", + "entity": action.firstAbleEntity + }); + + return true; + }, + "getActionInfo": function(entState, targetState) + { + if (!entState.rallyPoint) + return false; + + let tooltip; + let data = { "command": "walk" }; + let cursor = ""; + data.sound = false; + + if (entState.attack && Engine.HotkeyIsPressed("session.focusfire")) + { + cursor = "action-target"; + data.command = "attack-only"; + if (targetState && playerCheck(entState, targetState, ["Enemy"]) && !targetState.resourceSupply) + { + data.target = targetState.id; + data.sound = true; + } + return { + "possible": true, + "data": data, + "position": targetState && targetState.position, + "cursor": cursor, + "tooltip": tooltip + }; + } + }, + "hotkeyActionCheck": function(target, selection) + { + // Hotkeys are checked in the actionInfo. + return this.actionCheck(target, selection); + }, + "actionCheck": function(target, selection) + { + // We want commands to units take precedence. + if (selection.some(ent => { + let entState = GetEntityState(ent); + return entState && !!entState.unitAI; + })) + return false; + + let actionInfo = getActionInfo("focus-fire", target, selection); + + return actionInfo.possible && { + "type": "focus-fire", + "cursor": actionInfo.cursor, + "data": actionInfo.data, + "target": target, + "tooltip": actionInfo.tooltip, + "position": actionInfo.position, + "firstAbleEntity": actionInfo.entity + }; + }, + "specificness": 6, + }, + "set-rallypoint": { "execute": function(position, action, selection, queued, pushFront) diff --git a/binaries/data/mods/public/simulation/components/BuildingAI.js b/binaries/data/mods/public/simulation/components/BuildingAI.js index 634fc9df9b..377a7fadc8 100644 --- a/binaries/data/mods/public/simulation/components/BuildingAI.js +++ b/binaries/data/mods/public/simulation/components/BuildingAI.js @@ -1,5 +1,5 @@ // Number of rounds of firing per 2 seconds. -const roundCount = 10; +const roundCount = 20; const attackType = "Ranged"; function BuildingAI() {} @@ -28,6 +28,7 @@ BuildingAI.prototype.Init = function() this.archersGarrisoned = 0; this.arrowsLeft = 0; this.targetUnits = []; + this.focusTargets = []; }; BuildingAI.prototype.OnGarrisonedUnitsChanged = function(msg) @@ -50,6 +51,7 @@ BuildingAI.prototype.OnGarrisonedUnitsChanged = function(msg) BuildingAI.prototype.OnOwnershipChanged = function(msg) { this.targetUnits = []; + this.focusTargets = []; this.SetupRangeQuery(); this.SetupGaiaRangeQuery(); }; @@ -266,6 +268,22 @@ BuildingAI.prototype.SetUnitAITarget = function(ent) this.StartTimer(); }; +/** + * Adds index to keep track of the user-targeted units supporting a queue + * @param {ent} - Target of focus-fire from unit-actions if the selection is an enemy. + */ +BuildingAI.prototype.AddFocusTarget = function(ent, queued, push) +{ + if (!ent || this.targetUnits.indexOf(ent) === -1) + return; + if (queued) + this.focusTargets.push({"entityId": ent}); + else if (push) + this.focusTargets.unshift({"entityId": ent}); + else + this.focusTargets = [{"entityId": ent}]; +}; + /** * Fire arrows with random temporal distribution on prefered targets. * Called 'roundCount' times every 'RepeatTime' seconds when there are units in the range. @@ -298,8 +316,9 @@ BuildingAI.prototype.FireArrows = function() arrowsToFire = this.arrowsLeft; else arrowsToFire = Math.min( - randIntInclusive(0, 2 * this.GetArrowCount() / roundCount), - this.arrowsLeft + // shooting arrows in the first quarter of rounds results in a burst. + this.GetArrowCount() / (roundCount / 4), + this.arrowsLeft ); if (arrowsToFire <= 0) @@ -308,25 +327,38 @@ BuildingAI.prototype.FireArrows = function() return; } - // Add targets to a weighted list, to allow preferences. - let targets = new WeightedList(); - let maxPreference = this.MAX_PREFERENCE_BONUS; - let addTarget = function(target) + // Add targets to a list. + let targets = []; + let addTarget = function(target) { - let preference = cmpAttack.GetPreference(target); - let weight = 1; - - if (preference !== null && preference !== undefined) - weight += maxPreference / (1 + preference); - - targets.push(target, weight); + const pref = (cmpAttack.GetPreference(target) ?? 49); + targets.push({"entityId": target, "preference": pref}); }; // Add the UnitAI target separately, as the UnitMotion and RangeManager implementations differ. if (this.unitAITarget && this.targetUnits.indexOf(this.unitAITarget) == -1) addTarget(this.unitAITarget); - for (let target of this.targetUnits) - addTarget(target); + + else if (this.unitAITarget && this.targetUnits.indexOf(this.unitAITarget) != -1) + this.focusTargets = [{"entityId": this.unitAITarget}]; + + if (!this.focusTargets.length) + { + for (let target of this.targetUnits) + addTarget(target); + // Sort targets by preference and then by proximity. + targets.sort( (a,b) => { + if (a.preference > b.preference) + return 1; + else if (a.preference < b.preference) + return -1; + else if (PositionHelper.DistanceBetweenEntities(this.entity, a.entityId) > PositionHelper.DistanceBetweenEntities(this.entity, b.entityId)) + return 1; + return -1; + }); + } + else + targets = this.focusTargets; // The obstruction manager performs approximate range checks. // so we need to verify them here. @@ -335,10 +367,12 @@ BuildingAI.prototype.FireArrows = function() const range = cmpAttack.GetRange(attackType); const yOrigin = cmpAttack.GetAttackYOrigin(attackType); - let firedArrows = 0; - while (firedArrows < arrowsToFire && targets.length()) - { - const selectedTarget = targets.randomItem(); + let firedArrows = 0; + let targetIndex = 0; + while (firedArrows < arrowsToFire && targetIndex < targets.length) + { + + let selectedTarget = targets[targetIndex].entityId; if (this.CheckTargetVisible(selectedTarget) && cmpObstructionManager.IsInTargetParabolicRange( this.entity, selectedTarget, @@ -350,13 +384,11 @@ BuildingAI.prototype.FireArrows = function() cmpAttack.PerformAttack(attackType, selectedTarget); PlaySound("attack_" + attackType.toLowerCase(), this.entity); ++firedArrows; - continue; } - - // Could not attack target, try a different target. - targets.remove(selectedTarget); + else + ++targetIndex;// Could not attack target, try a different target. } - + targets.splice(0, targetIndex); this.arrowsLeft -= firedArrows; ++this.currentRound; }; diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js index 8bd77366fd..d8d5cd0620 100644 --- a/binaries/data/mods/public/simulation/helpers/Commands.js +++ b/binaries/data/mods/public/simulation/helpers/Commands.js @@ -431,6 +431,14 @@ var g_Commands = { } }, + "focus-fire": function (player, cmd, data) + { + for (let ent of data.entities) + { + Engine.QueryInterface(ent, IID_BuildingAI)?.AddFocusTarget(cmd.target, cmd.queued, cmd.pushFront); + } + }, + "set-rallypoint": function(player, cmd, data) { for (let ent of data.entities) diff --git a/binaries/data/mods/public/simulation/templates/template_structure.xml b/binaries/data/mods/public/simulation/templates/template_structure.xml index 3eea9cf8b3..478cfc8f61 100644 --- a/binaries/data/mods/public/simulation/templates/template_structure.xml +++ b/binaries/data/mods/public/simulation/templates/template_structure.xml @@ -139,6 +139,7 @@ interface/alarm/alarm_attacked_gaia.xml interface/alarm/alarm_attackplayer.xml interface/alarm/alarm_attacked_gaia.xml + attack/weapon/bow_launch_building.xml