Skip to content

Commit

Permalink
Add the "Insurance Claims" skill.
Browse files Browse the repository at this point in the history
GitOrigin-RevId: f7af780a0b1ee575dff2575a45909bb43731f50d
  • Loading branch information
cpojer committed Mar 7, 2025
1 parent 68dfb35 commit 06e83ce
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 48 deletions.
53 changes: 34 additions & 19 deletions apollo/HiddenAction.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Skill } from '@deities/athena/info/Skill.tsx';
import { Jeep } from '@deities/athena/info/Unit.tsx';
import maybeRecoverUnitCost from '@deities/athena/lib/maybeRecoverUnitCost.tsx';
import updatePlayer from '@deities/athena/lib/updatePlayer.tsx';
import Building from '@deities/athena/map/Building.tsx';
import { PlayerID } from '@deities/athena/map/Player.tsx';
Expand Down Expand Up @@ -144,7 +145,7 @@ function applyHiddenSourceAttackUnitAction(
)
: map.units.delete(to);

const actualPlayerB = map.getPlayer(originalUnitB);
let actualPlayerB = map.getPlayer(originalUnitB);
let lostUnits = unitB && newPlayerB == null ? 0 : originalUnitB.count();

if (
Expand All @@ -157,6 +158,8 @@ function applyHiddenSourceAttackUnitAction(
lostUnits = Math.max(0, lostUnits - 1);
}

actualPlayerB = maybeRecoverUnitCost(!unitB, actualPlayerB, originalUnitB);

return map.copy({
teams:
originalUnitB.player > 0
Expand Down Expand Up @@ -211,12 +214,16 @@ function applyHiddenSourceAttackBuildingAction(
existingUnit && existingUnit.player > 0
? updatePlayer(
map.teams,
map
.getPlayer(existingUnit)
.modifyStatistics({
lostUnits: existingUnit.count(),
})
.maybeSetCharge(chargeC),
maybeRecoverUnitCost(
!unitC,
map
.getPlayer(existingUnit)
.modifyStatistics({
lostUnits: existingUnit.count(),
})
.maybeSetCharge(chargeC),
existingUnit,
),
)
: map.teams,
units: map.units.delete(to),
Expand Down Expand Up @@ -252,12 +259,16 @@ function applyHiddenTargetAttackUnitAction(
? map.teams
: updatePlayer(
map.teams,
map
.getPlayer(unit)
.modifyStatistics({
lostUnits: unit.count(),
})
.maybeSetCharge(chargeA),
maybeRecoverUnitCost(
!unitA,
map
.getPlayer(unit)
.modifyStatistics({
lostUnits: unit.count(),
})
.maybeSetCharge(chargeA),
unit,
),
),
units: unitA
? map.units.set(
Expand Down Expand Up @@ -313,12 +324,16 @@ function applyHiddenTargetAttackBuildingAction(
? map.teams
: updatePlayer(
map.teams,
map
.getPlayer(unit)
.modifyStatistics({
lostUnits: unit.count(),
})
.maybeSetCharge(chargeA),
maybeRecoverUnitCost(
!unitA,
map
.getPlayer(unit)
.modifyStatistics({
lostUnits: unit.count(),
})
.maybeSetCharge(chargeA),
unit,
),
),
units,
});
Expand Down
38 changes: 37 additions & 1 deletion apollo/__tests__/Skill.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Bar, ResearchLab } from '@deities/athena/info/Building.tsx';
import { Bar, House, ResearchLab } from '@deities/athena/info/Building.tsx';
import { Skill } from '@deities/athena/info/Skill.tsx';
import { Forest, Forest2, RailTrack } from '@deities/athena/info/Tile.tsx';
import {
Expand All @@ -25,6 +25,7 @@ import MapData from '@deities/athena/MapData.tsx';
import { expect, test } from 'vitest';
import {
ActivatePowerAction,
AttackBuildingAction,
AttackUnitAction,
CaptureAction,
HealAction,
Expand Down Expand Up @@ -717,3 +718,38 @@ test(`some skills require a target to be provided`, () => {
}
expect(actionResponse.from).toBe(from);
});

test('some skills can recover unit costs', async () => {
const skills = new Set([Skill.CostRecovery]);
const mapA = map.copy({
teams: updatePlayer(map.teams, map.getPlayer(1).copy({ skills })),
units: map.units
.set(fromA, SmallTank.create(1).setHealth(5))
.set(toA, SmallTank.create(2)),
});
const [, state1] = execute(mapA, vision, AttackUnitAction(fromA, toA))!;

expect(state1.getPlayer(1).funds).toEqual(mapA.getPlayer(1).funds);

const mapB = mapA.copy({
teams: updatePlayer(
map.teams,
map.getPlayer(1).copy({ activeSkills: skills }),
),
});

const [, state2] = execute(mapB, vision, AttackUnitAction(fromA, toA))!;
expect(state2.getPlayer(1).funds).toBeGreaterThan(mapA.getPlayer(1).funds);

const mapC = mapB.copy({
currentPlayer: 2,
});
const [, state3] = execute(mapC, vision, AttackUnitAction(toA, fromA))!;
expect(state3.getPlayer(1).funds).toBeGreaterThan(mapA.getPlayer(1).funds);

const mapD = mapC.copy({
buildings: mapB.buildings.set(fromA, House.create(1).setHealth(5)),
});
const [, state4] = execute(mapD, vision, AttackBuildingAction(toA, fromA))!;
expect(state4.getPlayer(1).funds).toBeGreaterThan(mapA.getPlayer(1).funds);
});
87 changes: 59 additions & 28 deletions apollo/actions/applyActionResponse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import getActivePlayers from '@deities/athena/lib/getActivePlayers.tsx';
import getHealCost from '@deities/athena/lib/getHealCost.tsx';
import getUnitsToRefill from '@deities/athena/lib/getUnitsToRefill.tsx';
import maybeConvertPlayer from '@deities/athena/lib/maybeConvertPlayer.tsx';
import maybeRecoverUnitCost from '@deities/athena/lib/maybeRecoverUnitCost.tsx';
import mergeTeams from '@deities/athena/lib/mergeTeams.tsx';
import refillUnits from '@deities/athena/lib/refillUnits.tsx';
import spawnBuildings from '@deities/athena/lib/spawnBuildings.tsx';
Expand Down Expand Up @@ -124,8 +125,8 @@ export default function applyActionResponse(
)
: units.delete(to);

const actualPlayerA = map.getPlayer(playerA);
const actualPlayerB = map.getPlayer(playerB);
let actualPlayerA = map.getPlayer(playerA);
let actualPlayerB = map.getPlayer(playerB);

const lostUnits =
unitA &&
Expand Down Expand Up @@ -155,6 +156,17 @@ export default function applyActionResponse(
destroyedUnits = Math.max(0, destroyedUnits - 1);
}

actualPlayerA = maybeRecoverUnitCost(
!unitA,
actualPlayerA,
originalUnitA,
);
actualPlayerB = maybeRecoverUnitCost(
!unitB,
actualPlayerB,
originalUnitB,
);

return map.copy({
teams: updatePlayers(map.teams, [
actualPlayerA
Expand Down Expand Up @@ -244,10 +256,17 @@ export default function applyActionResponse(
const oneShotA =
originalUnitA && originalUnitA?.health >= MaxHealth && !unitA ? 1 : 0;
// Update `playerA` and `playerB` first, then update `playerC` which might equal `playerB`.
let actualPlayerA = map.getPlayer(playerA);

actualPlayerA = maybeRecoverUnitCost(
!unitA,
actualPlayerA,
originalUnitA,
);

const teams = originalBuilding
? updatePlayers(map.teams, [
map
.getPlayer(playerA)
actualPlayerA
.modifyStatistics({
damage: Math.max(
0,
Expand All @@ -267,31 +286,39 @@ export default function applyActionResponse(
])
: map.teams;

let actualPlayerC =
originalUnitC && originalUnitC.player > 0 && playerC
? map.copy({ teams }).getPlayer(playerC)
: null;

if (actualPlayerC) {
actualPlayerC = maybeRecoverUnitCost(
!unitC,
actualPlayerC,
originalUnitC,
);
}

return map.copy({
buildings: building
? map.buildings.set(to, building)
: map.buildings.delete(to),
teams:
originalUnitC && originalUnitC.player > 0 && playerC
? updatePlayer(
teams,
map
.getPlayer(playerC)
.modifyStatistics({
damage:
hasCounterAttack && originalUnitA
? Math.max(
0,
originalUnitA.health - (unitA?.health || 0),
)
: 0,
destroyedUnits: lostUnits,
lostUnits: unitC ? 0 : originalUnitC?.count() || 1,
oneShots: oneShotA,
})
.maybeSetCharge(chargeC),
)
: teams,
teams: actualPlayerC
? updatePlayer(
teams,
actualPlayerC
.modifyStatistics({
damage:
hasCounterAttack && originalUnitA
? Math.max(0, originalUnitA.health - (unitA?.health || 0))
: 0,
destroyedUnits: lostUnits,
lostUnits: unitC ? 0 : originalUnitC?.count() || 1,
oneShots: oneShotA,
})
.maybeSetCharge(chargeC),
)
: teams,
units,
});
}
Expand Down Expand Up @@ -530,9 +557,13 @@ export default function applyActionResponse(
oneShots: unit && unit.health >= MaxHealth ? 1 : 0,
}),
unit
? map
.getPlayer(unit.player)
.modifyStatistic('lostUnits', unit.count())
? maybeRecoverUnitCost(
!!unit,
map
.getPlayer(unit.player)
.modifyStatistic('lostUnits', unit.count()),
unit,
)
: null,
]),
units: unit ? map.units.delete(to) : map.units,
Expand Down
11 changes: 11 additions & 0 deletions athena/info/Skill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export enum Skill {
Jeep = 42,
ShipIncreaseAttackAndRange = 43,
XFighterAttackIncrase = 44,
CostRecovery = 45,
}

export const Skills = new Set<Skill>([
Expand Down Expand Up @@ -95,6 +96,7 @@ export const Skills = new Set<Skill>([
Skill.ShipIncreaseAttackAndRange,
Skill.XFighterAttackIncrase,
Skill.DragonSaboteur,
Skill.CostRecovery,
Skill.Jeep,
Skill.RecoverAirUnits,
Skill.Shield,
Expand Down Expand Up @@ -317,6 +319,11 @@ const skillConfig: Record<
cost: 1200,
group: SkillGroup.Special,
},
[Skill.CostRecovery]: {
charges: 3,
cost: 800,
group: SkillGroup.Special,
},
};

export const CampaignOnlySkills = new Set(
Expand Down Expand Up @@ -539,6 +546,7 @@ const unitCostEffects: SkillMap = new Map([
[Skill.AttackIncreaseMajorDefenseDecreaseMajor, 0.15],
[Skill.HealVehiclesAttackDecrease, -0.2],
[Skill.DecreaseUnitCostAttackAndDefenseDecreaseMinor, -0.1],
[Skill.CostRecovery, 0.1],
]);

const unitCostPowerEffects: SkillMap = new Map([
Expand Down Expand Up @@ -1395,6 +1403,8 @@ export const VampireSoldierMovementTypes = new Set([
MovementTypes.AirInfantry,
]);

export const CostRecoverySkillModifier = 0.5;

const octopusPowerDamage = 20;
const dragonPowerDamage = 80;
const vampirePowerDamage = 25;
Expand Down Expand Up @@ -1452,6 +1462,7 @@ export function shouldUpgradeUnit(unit: Unit, skill: Skill) {
case Skill.BuyUnitSuperTank:
case Skill.BuyUnitZombieDefenseDecreaseMajor:
case Skill.Charge:
case Skill.CostRecovery:
case Skill.CounterAttackPower:
case Skill.DecreaseUnitCostAttackAndDefenseDecreaseMinor:
case Skill.DefenseIncreaseMinor:
Expand Down
18 changes: 18 additions & 0 deletions athena/lib/maybeRecoverUnitCost.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { CostRecoverySkillModifier, Skill } from '../info/Skill.tsx';
import Player from '../map/Player.tsx';
import Unit from '../map/Unit.tsx';

export default function maybeRecoverUnitCost(
shouldRecover: boolean,
player: Player,
unit: Unit | undefined,
) {
if (shouldRecover && unit && player.activeSkills.has(Skill.CostRecovery)) {
const cost = unit.info.getCostFor(player);
if (cost < Number.POSITIVE_INFINITY) {
return player.modifyFunds(cost * CostRecoverySkillModifier);
}
}

return player;
}
1 change: 1 addition & 0 deletions dionysus/lib/shouldActivatePower.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const shouldConsiderUnitRatio = (skill: Skill) => {
case Skill.BuyUnitBear:
case Skill.BuyUnitDinosaur:
case Skill.BuyUnitOctopus:
case Skill.CostRecovery:
case Skill.DecreaseUnitCostAttackAndDefenseDecreaseMinor:
case Skill.HighTide:
case Skill.Shield:
Expand Down
1 change: 1 addition & 0 deletions hera/lib/getSkillBasedPortrait.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export default function getSkillBasedPortrait(skill: Skill): UnitInfo | null {
case Skill.AttackIncreaseMajorDefenseDecreaseMajor:
case Skill.AttackIncreaseMinor:
case Skill.Charge:
case Skill.CostRecovery:
case Skill.CounterAttackPower:
case Skill.DecreaseUnitCostAttackAndDefenseDecreaseMinor:
case Skill.DefenseIncreaseMinor:
Expand Down
8 changes: 8 additions & 0 deletions hera/lib/getSkillConfigForDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,14 @@ export default function getSkillConfigForDisplay(skill: Skill): SkillConfig {
icon: Ungroup,
name: fbt("Amira's Ace", 'Skill name'),
};
case Skill.CostRecovery:
return {
alpha: 0.5,
borderStyle: 'up',
colors: 'orange',
icon: Coin,
name: fbt('Insurance Claims', 'Skill name'),
};
default: {
skill satisfies never;
throw new UnknownTypeError('getSkillConfig', skill);
Expand Down
Loading

0 comments on commit 06e83ce

Please sign in to comment.