Add non-random building AI

-lets building arrows target the closest unit by default.
-adds player control of building arrows.
-adds a tooltip to explain how to control arrows.
-Uses "F" for focus fire and concurrently use "F" for force-attack, since "C" is capture.
This commit is contained in:
real_tabasco_sauce 2024-12-21 13:10:00 -08:00 committed by Nicolas Auvray
parent bae50fe4f7
commit 7c95b6700b
12 changed files with 183 additions and 27 deletions

View file

@ -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

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5095290f717f89b2f00a45dac4e4bbafc2c12879e3b50897e57e384e7efec3ee
size 1690

View file

@ -0,0 +1 @@
1 1

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:88f38275bfb85bb5e724d974e639d71f94c5625f2e21ec0a9fbbe1636399fbe2
size 522853

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
<SoundGroup>
<Gain>10</Gain>
<Priority>1</Priority>
<Looping>0</Looping>
<RandOrder>1</RandOrder>
<RandPitch>1</RandPitch>
<PitchUpper>1.2</PitchUpper>
<PitchLower>0.8</PitchLower>
<Threshold>500</Threshold>
<Path>audio/attack/weapon/</Path>
<Sound>sling_210.ogg</Sound>
</SoundGroup>

View file

@ -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."
}
}
}

View file

@ -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)

View file

@ -41,6 +41,12 @@
"briton_war_dog.png"
]
},
{
"textFile": "building_control.txt",
"imageFiles": [
"building_control.png"
]
},
{
"textFile": "carth_sacred_band.txt",
"imageFiles": [

View file

@ -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)

View file

@ -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;
};

View file

@ -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)

View file

@ -139,6 +139,7 @@
<attacked_gaia>interface/alarm/alarm_attacked_gaia.xml</attacked_gaia>
<attacked_capture>interface/alarm/alarm_attackplayer.xml</attacked_capture>
<attacked_capture_gaia>interface/alarm/alarm_attacked_gaia.xml</attacked_capture_gaia>
<focus_fire>attack/weapon/bow_launch_building.xml</focus_fire>
</SoundGroups>
</Sound>
<StatusBars>