Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
92c4324
Better ship handling for AFK player
VariableVince Oct 15, 2025
870a35a
New src for retreating transport
VariableVince Oct 15, 2025
0f867eb
Small fix
VariableVince Oct 15, 2025
ac128e0
Merge branch 'v26' into warship-afk-handling
VariableVince Oct 15, 2025
d479c3e
Warships never attack when friendly is disconnected
VariableVince Oct 17, 2025
1b44273
Fix leftover troop removal on attack and add tests
VariableVince Oct 17, 2025
3c0baa5
Fix condition
VariableVince Oct 17, 2025
f583e5b
Merge branch 'v26' into warship-afk-handling
VariableVince Oct 17, 2025
fac44c8
Merge branch 'v26' into warship-afk-handling
VariableVince Oct 18, 2025
c84e532
Originalowner = attacker
VariableVince Oct 24, 2025
705fce6
Move src check to retreating block
VariableVince Oct 24, 2025
a85b6da
add param to isFriendly to prevent afk warship attack
VariableVince Oct 24, 2025
9e7d9ef
Prettier
VariableVince Oct 24, 2025
b844bea
origowner outside constr
VariableVince Oct 24, 2025
d26886b
Merge branch 'v26' into warship-afk-handling
VariableVince Oct 24, 2025
8b4f35b
Fix origowner
VariableVince Oct 24, 2025
dd13a89
Use bestTransportShipSpawn instead of canBuild
VariableVince Oct 25, 2025
3c0d967
Fix no troop loss
VariableVince Oct 28, 2025
32bbdf2
Wording
VariableVince Oct 28, 2025
d3382a7
Merge branch 'v26' into warship-afk-handling
VariableVince Oct 28, 2025
04d9c76
No troop remove at init but keep bugfix and test
VariableVince Oct 30, 2025
cbcd3db
Prettier
VariableVince Oct 30, 2025
8abebe6
Merge branch 'v26' into warship-afk-handling
VariableVince Oct 30, 2025
a91c671
Pascal Case and more ticks
VariableVince Oct 30, 2025
fe854c0
Merge branch 'warship-afk-handling' of https://github.com/openfrontio…
VariableVince Oct 30, 2025
a7974d9
Merge branch 'v26' into warship-afk-handling
evanpelle Nov 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/core/configuration/DefaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ export class DefaultConfig implements Config {

if (attacker.isPlayer() && defender.isPlayer()) {
if (defender.isDisconnected() && attacker.isOnSameTeam(defender)) {
// No troop loss if defender is disconnected.
// No troop loss if defender is disconnected and on same team
mag = 0;
}
if (
Expand Down
7 changes: 6 additions & 1 deletion src/core/execution/AttackExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,12 @@ export class AttackExecution implements Execution {
this.startTroops ??= this.mg
.config()
.attackAmount(this._owner, this.target);
if (this.removeTroops) {
const isDisconnectedTeammate =
this.target.isPlayer() &&
(this.target as Player).isDisconnected() &&
this._owner.isOnSameTeam(this.target as Player);
if (this.removeTroops && !isDisconnectedTeammate) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have no troop loss when attacking afk teammate, in DefaultConfig.attackLogic. This looks like it could allow players to gain additional troops, since they will get their troops back after the attack completes.

Copy link
Contributor Author

@VariableVince VariableVince Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The attacker has less troops left after the attack currently, than expected without troop loss. This is what i discovered when making test "Player can attack disconnected team mate without troop loss" in Disconnected.test.ts, which is part of this PR. It tests this by calculating the normal troop growth, then attacking, and afterwards checking if troops have increased by the normal troop growth. The player should have the number of troops afterwards that he would have had when the attack had not taken place. But each test run the player had less troops than that after the attack. If the start troops that are removed in the AttackExecution init were added back after the attack, that should not have happened.

The test only started passing after applying these changes in the AttackExecition init, ie. remove no troops at the start of the attack on the disconnected teammate.

So like you I was not expecting troop loss either based on AttackLoss in DefaultConfig. But AttackExecution removes the start troops (attack ratio %) in its init. Even for attacks on disconnected team mates. And those start troops apparently are not returned after the attack ends. Otherwise the test "Player can attack disconnected team mate without troop loss" would have already passed before the fix.

But it could be i am missing something of course. Could you maybe point me to the code where troops are added back? Or is the test flawed?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... looking at the code is seems the refund happens only after the attack completes, and in that test it only executes 2 ticks, so idk if the attack completes?

Copy link
Contributor Author

@VariableVince VariableVince Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes i missed that, thanks for looking into it again! Since i still find it better if there's no visual troop loss either, i kept with it but fixed how it works.

It now sets the existing removeTroops variable to True, still resulting in startTroops not being removed from owner troops using existing code.

Next is basically a bug fix: when the attack ends it checks if removeTroops was set to True in the retreat() function, and if so it subtracts the startTroops from the attack troops. So the startTroops aren't added back to the owner troops when they were never removed from owner troops in the first place. Variable removeTroops variable could have been set to False for another reason someday so this fix was actually needed anyway.

Added comments to explain, maybe a bit verbose.

Changed test "Player can attack disconnected team mate without troop loss" so it fully conquers the disconnected player before checking if there was no troop loss.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After talking about it in the Dev server I decided to remove the 'do not remove troops from owner at attackexecution init' part from the PR. Left in the bug fix in retreat(), which was needed already anyway in the case anyone would set existing variable removeTroops to false. Also left in the test "Player can attack disconnected team mate without troop loss" in Disconnected.test.ts.

(You don't have to read this, but before deciding on removing the 'do not remove troops from owner at attackexecution init' part from this PR, I did consider and try:

  • Setting startTroops to 0 so attack.troops() would be 0. But this leads to devide-by-zero calculations in AttackLoss in DefaultConfig. It would be too much for this PR to change this. I propose we do this in another PR. The attack should, imo, feel more like a swift annexation than like a normal attack (but it should still be an attack just an easy one, not some automatic annexation, an attack adds tension to the game because you need to be quick about it before the enemy attacks the AFK teammate). AttackLoss could get a specific part for annexing an AFK teammate, that can handle attack.troops 0 and make it feel annexation-like. Once we can set startTroops/attack.troops to 0, so there is no initial removal of owner troops and no troops in the attack, we can have renderTroops in Utils.ts return an empty string for "0" so EventDisplay just displays "AFK Teammate" and not "0 AFK Teammate" so there's no confusion about an attack with 0 troops.

  • Add a 'hideTroopsInUI' flag to attack.troops in AttackUpdate. So even when attack.troops would be >0, it would just send attack.troops 0 to the UI like EventsDisplay. We could then have renderTroops in Utils.ts return an empty string for "0" so EventDisplay just displays "AFK Teammate" and not "0 AFK Teammate" so there would be no confusion about an attack with 0 troops. However, just hiding it in the UI would cause confusion in itself because the attack would still feel slow like now, you get no feedback as to why it is slow. Only if the attack becomes swift annexation-like (see above) would it make sense to not display troops in the UI.)

// No troop loss if defender is disconnected and on same team
this.startTroops = Math.min(this._owner.troops(), this.startTroops);
this._owner.removeTroops(this.startTroops);
}
Expand Down
44 changes: 40 additions & 4 deletions src/core/execution/TransportShipExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,17 @@ export class TransportShipExecution implements Execution {

private pathFinder: PathFinder;

private originalOwner: Player;

constructor(
private attacker: Player,
private targetID: PlayerID | null,
private ref: TileRef,
private startTroops: number,
private src: TileRef | null,
) {}
) {
this.originalOwner = this.attacker;
}

activeDuringSpawnPhase(): boolean {
return false;
Expand Down Expand Up @@ -173,11 +177,43 @@ export class TransportShipExecution implements Execution {
}
this.lastMove = ticks;

// Team mate can conquer disconnected player and get their ships
// captureUnit has changed the owner of the unit, now update attacker
if (
this.originalOwner.isDisconnected() &&
this.boat.owner() !== this.originalOwner &&
this.boat.owner().isOnSameTeam(this.originalOwner)
) {
this.attacker = this.boat.owner();
this.originalOwner = this.boat.owner(); // for when this owner disconnects too
}

if (this.boat.retreating()) {
this.dst = this.src!; // src is guaranteed to be set at this point
// Ensure retreat source is valid for the new owner
if (this.mg.owner(this.src!) !== this.attacker) {
// Use bestTransportShipSpawn, not canBuild because of its max boats check etc
const newSrc = this.attacker.bestTransportShipSpawn(this.dst);
if (newSrc === false) {
this.src = null;
} else {
this.src = newSrc;
}
}

if (this.boat.targetTile() !== this.dst) {
this.boat.setTargetTile(this.dst);
if (this.src === null) {
console.warn(
`TransportShipExecution: retreating but no src found for new attacker`,
);
this.attacker.addTroops(this.boat.troops());
this.boat.delete(false);
this.active = false;
return;
} else {
this.dst = this.src;

if (this.boat.targetTile() !== this.dst) {
this.boat.setTargetTile(this.dst);
}
}
}

Expand Down
6 changes: 1 addition & 5 deletions src/core/execution/WarshipExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,6 @@ export class WarshipExecution implements Execution {
this.warship.delete();
return;
}
if (this.warship.owner().isDisconnected()) {
this.warship.delete();
return;
}

const hasPort = this.warship.owner().unitCount(UnitType.Port) > 0;
if (hasPort) {
Expand Down Expand Up @@ -93,7 +89,7 @@ export class WarshipExecution implements Execution {
if (
unit.owner() === this.warship.owner() ||
unit === this.warship ||
unit.owner().isFriendly(this.warship.owner()) ||
unit.owner().isFriendly(this.warship.owner(), true) ||
this.alreadySentShell.has(unit)
) {
continue;
Expand Down
2 changes: 1 addition & 1 deletion src/core/game/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ export interface Player {
decayRelations(): void;
isOnSameTeam(other: Player): boolean;
// Either allied or on same team.
isFriendly(other: Player): boolean;
isFriendly(other: Player, treatAFKFriendly?: boolean): boolean;
team(): Team | null;
clan(): string | null;
incomingAllianceRequests(): AllianceRequest[];
Expand Down
14 changes: 14 additions & 0 deletions src/core/game/GameImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,20 @@ export class GameImpl implements Game {
return this._railNetwork;
}
conquerPlayer(conqueror: Player, conquered: Player) {
if (conquered.isDisconnected() && conqueror.isOnSameTeam(conquered)) {
const ships = conquered
.units()
.filter(
(u) =>
u.type() === UnitType.Warship ||
u.type() === UnitType.TransportShip,
);

for (const ship of ships) {
conqueror.captureUnit(ship);
}
}

const gold = conquered.gold();
this.displayMessage(
`Conquered ${conquered.displayName()} received ${renderNumber(
Expand Down
4 changes: 2 additions & 2 deletions src/core/game/PlayerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,8 +761,8 @@ export class PlayerImpl implements Player {
return this._team === other.team();
}

isFriendly(other: Player): boolean {
if (other.isDisconnected()) {
isFriendly(other: Player, treatAFKFriendly: boolean = false): boolean {
if (other.isDisconnected() && !treatAFKFriendly) {
return false;
}
return this.isOnSameTeam(other) || this.isAlliedWith(other);
Expand Down
2 changes: 2 additions & 0 deletions src/core/game/TransportShipUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ export function bestShoreDeploymentSource(
if (t === null) return false;

const candidates = candidateShoreTiles(gm, player, t);
if (candidates.length === 0) return false;

const aStar = new MiniAStar(gm, gm.miniMap(), candidates, t, 1_000_000, 1);
const result = aStar.compute();
if (result !== PathFindResultType.Completed) {
Expand Down
Loading
Loading