Combat¶
Wave capacity, bonuses, defense and the wave solver that client.attack fills waves with.
combat.solver¶
The soldier half of the client's wave auto-fill.
Ported from AFillFlankStrategy.fillFlankWithSoldiers and
StrongestDefenceCounterRatioConsideredFlankStrategy.pickSoldierStack: pick
the unit whose stack best counters whichever of the defender's melee or ranged
defense is the weaker share, place it, repeat until the flank is full.
Tools are not placed yet, so a wave built here carries units only.
Classes:
| Name | Description |
|---|---|
FillOptions |
Which flanks to fill and which units may be used. |
Inventory |
A mutable pool of units to draw a wave from. |
FilledAttack |
A complete attack: its waves and its courtyard wave. |
Functions:
| Name | Description |
|---|---|
pick_soldier_stack |
Choose one stack for a flank slot and take it out of the inventory. |
fill_flank_with_soldiers |
Fill one flank's unit slots. |
fill_wave |
Build one wave, flank by flank. |
fill_yard_wave |
Fill the courtyard wave, which goes out in |
fill_waves |
Fill every wave the attack may carry, front to back. |
wave_limit_violations |
Which of an attack's containers hold more than they may. |
FillOptions
pydantic-model
¶
Bases: EmpireModel
Which flanks to fill and which units may be used.
Every filter defaults to allowed, matching the client's
AutoFillOptions.createNewUnitFilter. Turning one off excludes units that
carry that cost: allow_ruby_cost=False keeps units whose healing costs
rubies out of the wave, for instance.
Fields:
-
fill_left(bool) -
fill_middle(bool) -
fill_right(bool) -
allow_coin_cost(bool) -
allow_ruby_cost(bool) -
allow_mead(bool) -
allow_beef(bool) -
use_melee(bool) -
use_ranged(bool)
Inventory
¶
A mutable pool of units to draw a wave from.
The solver deducts what it places, so one inventory can fill several waves in sequence without double-spending a unit.
Methods:
| Name | Description |
|---|---|
available |
|
soldier_count |
How many actual units are left, ignoring tools and boost items. |
deduct |
Take up to |
Attributes:
| Name | Type | Description |
|---|---|---|
counts |
dict[int, int]
|
|
FilledAttack
pydantic-model
¶
Bases: EmpireModel
A complete attack: its waves and its courtyard wave.
waves goes in the A field of a cra and yard in RW.
Fields:
-
waves(list[AttackWave]) -
yard(WodAmountSlots) -
min_soldiers(int) -
unread(dict[TargetRead, CommandError])
yard
pydantic-field
¶
The courtyard wave's unit slots, for send_attack's yard_wave
unread
pydantic-field
¶
unread: dict[TargetRead, CommandError]
What fill_attack filled without, each with the server's refusal; its error names the GGEError
pick_soldier_stack
¶
pick_soldier_stack(free_items: int, inventory: Inventory, game_data: GameData, *, attacker: AttackerFlankEffects | None = None, defender: DefenderFlankEffects | None = None, options: FillOptions | None = None, unit_attack_bonuses: Mapping[int, float] | None = None) -> tuple[int, int] | None
Choose one stack for a flank slot and take it out of the inventory.
The choice weighs the best melee stack against the best ranged stack by the defender's opposite defense share, so the attack lands where the defense is thinner. With no defender information both shares are 1.0 and the stronger stack simply wins.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
free_items
|
int
|
Remaining troop capacity of the flank |
required |
inventory
|
Inventory
|
Pool to draw from; the chosen stack is deducted |
required |
game_data
|
GameData
|
Loaded unit stats |
required |
attacker
|
AttackerFlankEffects | None
|
Attacker multipliers (unbuffed when omitted) |
None
|
defender
|
DefenderFlankEffects | None
|
The defender's strength on this flank, if known |
None
|
options
|
FillOptions | None
|
Unit filters |
None
|
unit_attack_bonuses
|
Mapping[int, float] | None
|
Per-unit attack bonuses from active global
effects, from |
None
|
Returns:
| Type | Description |
|---|---|
tuple[int, int] | None
|
|
fill_flank_with_soldiers
¶
fill_flank_with_soldiers(capacity: int, slots: int, inventory: Inventory, game_data: GameData, *, attacker: AttackerFlankEffects | None = None, defender: DefenderFlankEffects | None = None, options: FillOptions | None = None, unit_attack_bonuses: Mapping[int, float] | None = None) -> list[tuple[int, int]]
Fill one flank's unit slots.
Stops early when a slot cannot be filled, as the client does, so a partly filled flank is a normal result rather than an error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
capacity
|
int
|
The flank's troop capacity |
required |
slots
|
int
|
How many unlocked unit slots the flank has |
required |
inventory
|
Inventory
|
Pool to draw from; placed stacks are deducted |
required |
game_data
|
GameData
|
Loaded unit stats |
required |
attacker
|
AttackerFlankEffects | None
|
Attacker multipliers |
None
|
defender
|
DefenderFlankEffects | None
|
The defender's strength on this flank, if known |
None
|
options
|
FillOptions | None
|
Unit filters |
None
|
unit_attack_bonuses
|
Mapping[int, float] | None
|
Per-unit attack buffs from active global effects |
None
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, int]]
|
|
fill_wave
¶
fill_wave(inventory: Inventory, game_data: GameData, capacity: WaveCapacity, *, attacker: AttackerFlankEffects | None = None, defense: Mapping[Flank, DefenderFlankEffects] | None = None, options: FillOptions | None = None, unit_attack_bonuses: Mapping[int, float] | None = None, strategies: Sequence[ToolStrategy] | None = None, area_type: int | None = None, space_id: int | None = None, target_is_player: bool = True, active_raid_boss_id: int | None = None) -> AttackWave
Build one wave, flank by flank.
Follows the client's order: each flank takes tools first, then soldiers,
then check_flank returns the tools if no unit ended up on that flank.
Tools come first because a placed tool changes the defense the soldiers are
then picked against.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inventory
|
Inventory
|
Pool to draw from; placed stacks are deducted |
required |
game_data
|
GameData
|
Loaded unit and tool stats |
required |
capacity
|
WaveCapacity
|
The wave's size, from |
required |
attacker
|
AttackerFlankEffects | None
|
Attacker multipliers |
None
|
defense
|
Mapping[Flank, DefenderFlankEffects] | None
|
Defender strength per flank, if known |
None
|
options
|
FillOptions | None
|
Which flanks to fill and which units to allow |
None
|
unit_attack_bonuses
|
Mapping[int, float] | None
|
Per-unit attack buffs from active global effects |
None
|
strategies
|
Sequence[ToolStrategy] | None
|
The tool strategy pool; the client's five when omitted. Pass an empty list for a unit-only wave. |
None
|
area_type
|
int | None
|
The target's area type, which scopes a tool's effects |
None
|
space_id
|
int | None
|
The kingdom the target sits in, which some tools are limited to |
None
|
target_is_player
|
bool
|
Whether the target belongs to another player |
True
|
active_raid_boss_id
|
int | None
|
The boss of the alliance raid-boss event running now, None when none is. A tool whose effects are all tied to other raid bosses is not carried |
None
|
Returns:
| Type | Description |
|---|---|
AttackWave
|
An |
AttackWave
|
one |
AttackWave
|
|
Client: CastleAttackWaveVO.getWaveInfoObject (bundle line 99930) and
CastleFightItemContainer.getSlotList (bundle line 20573), which lists
every slot of the container's CombatConst.ITEMS_* table (dll lines
18864-18876), unlocked ones first
fill_yard_wave
¶
fill_yard_wave(inventory: Inventory, game_data: GameData, capacity: int, *, slots: int = YARD_SLOTS, defender: DefenderFlankEffects | None = None, options: FillOptions | None = None, unit_attack_bonuses: Mapping[int, float] | None = None) -> tuple[WodAmount, ...]
Fill the courtyard wave, which goes out in cra's RW field.
AFillWaveStrategy.fillYardContainer fills it with soldiers only - no
tools - and passes no attacker effects, so units are scored on their raw
values against the yard's own defenders.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inventory
|
Inventory
|
Pool to draw from |
required |
game_data
|
GameData
|
Loaded unit stats |
required |
capacity
|
int
|
The yard's capacity, from |
required |
slots
|
int
|
How many unit slots the yard offers; all eight are open from level 0 |
YARD_SLOTS
|
defender
|
DefenderFlankEffects | None
|
The yard's defenders, if known |
None
|
options
|
FillOptions | None
|
Unit filters |
None
|
unit_attack_bonuses
|
Mapping[int, float] | None
|
Per-unit attack buffs from active global effects |
None
|
Returns:
| Type | Description |
|---|---|
WodAmount
|
One |
...
|
The client sends every slot, so an empty one goes out as |
fill_waves
¶
fill_waves(inventory: Inventory, game_data: GameData, *, level: int, attacker_level: int | None = None, conquer: bool = False, wave_bonus: int = 0, flank_bonus_percent: float = 0.0, front_bonus_percent: float = 0.0, tool_bonus: float = 0.0, attacker: AttackerFlankEffects | None = None, defense: Mapping[Flank, DefenderFlankEffects] | None = None, options: FillOptions | None = None, unit_attack_bonuses: Mapping[int, float] | None = None, area_type: int | None = None, space_id: int | None = None, target_is_player: bool = True, active_raid_boss_id: int | None = None) -> list[AttackWave]
Fill every wave the attack may carry, front to back.
Sizes itself the way the client does: each flank's capacity and slots follow
the level of whoever owns the target, while the number of waves follows the
attacker's own level. Waves that come out empty are dropped, so the result is ready
for send_attack.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inventory
|
Inventory
|
Pool to draw from; shared across every wave |
required |
game_data
|
GameData
|
Loaded unit stats |
required |
level
|
int
|
The target owner's level, which sizes each flank |
required |
attacker_level
|
int | None
|
The attacker's own level, which decides how many waves an attack carries; defaults to the target's level |
None
|
conquer
|
bool
|
A conquest attack carries extra waves |
False
|
wave_bonus
|
int
|
Extra waves from the ADDITIONAL_WAVE legend skill |
0
|
flank_bonus_percent
|
float
|
Percentage bonus to units on each side flank |
0.0
|
front_bonus_percent
|
float
|
Percentage bonus to units in the middle |
0.0
|
tool_bonus
|
float
|
Extra flank tool capacity |
0.0
|
attacker
|
AttackerFlankEffects | None
|
Attacker multipliers |
None
|
defense
|
Mapping[Flank, DefenderFlankEffects] | None
|
Defender strength per flank, if known |
None
|
options
|
FillOptions | None
|
Which flanks to fill and which units to allow |
None
|
unit_attack_bonuses
|
Mapping[int, float] | None
|
Per-unit attack buffs from active global effects |
None
|
area_type
|
int | None
|
The target's area type, which scopes a tool's effects |
None
|
space_id
|
int | None
|
The kingdom the target sits in, which some tools are limited to |
None
|
target_is_player
|
bool
|
Whether the target belongs to another player |
True
|
active_raid_boss_id
|
int | None
|
The boss of the alliance raid-boss event running now, None when none is. A tool whose effects are all tied to other raid bosses is not carried |
None
|
Returns:
| Type | Description |
|---|---|
list[AttackWave]
|
One |
wave_limit_violations
¶
wave_limit_violations(waves: Sequence[AttackWave], capacity: WaveCapacity, *, yard: Sequence[WodAmount] | None = None, yard_capacity: int | None = None) -> list[str]
Which of an attack's containers hold more than they may.
The client refuses to send at all when any wave's unit container or the
courtyard is over capacity - exceedsUnitLimit() and exceedsLimit(),
both of which are freeItems < 0. It shows a blocking dialog instead. A
hand-built army can trip this; the solver cannot.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
waves
|
Sequence[AttackWave]
|
The waves about to be sent |
required |
capacity
|
WaveCapacity
|
The capacities those waves were sized against |
required |
yard
|
Sequence[WodAmount] | None
|
The courtyard wave, if one is being sent |
None
|
yard_capacity
|
int | None
|
Its capacity, from |
None
|
Returns:
| Type | Description |
|---|---|
list[str]
|
One readable line per overfull container, empty when the attack is legal |
combat.bonuses¶
Resolving a commander's bonuses into the multipliers combat uses.
A bonus on an item names an effect; the effect names an effect type, which is what a formula reads, and a cap, which is what it stacks within. This module does that resolution and the client's two-stage capping, so the combat maths can ask for "the melee attack multiplier" and get a number.
See docs/design/combat_effects.md for the full catalogue.
Classes:
| Name | Description |
|---|---|
Bonus |
One granted bonus: an id, its strength, and which id space the id is in. |
EffectResolver |
Turns bonuses into combat multipliers using the game data tables. |
Functions:
| Name | Description |
|---|---|
parse_bonus_entries |
Parse the bonus encodings the server uses. |
effect_value_bonuses |
The bonuses of an items row's |
construction_item_bonuses |
Bonuses from the construction items placed on a castle's buildings. |
alliance_buff_bonuses |
Bonuses from the alliance's researched buffs, at their current levels. |
global_effect_bonuses |
Bonuses from the global event effects currently running. |
sceat_skill_bonuses |
Bonuses from unlocked sceat skills (the Hall of Legends trees). |
general_skill_bonuses |
Bonuses from the skills unlocked on the general leading the attack. |
general_passive_bonuses |
The general's passive effects, which join its commander's bonuses. |
legend_skill_value |
Total value of one legend skill effect type. |
global_unit_attack_bonuses |
Per-unit attack bonuses from the global effects currently running. |
commander_bonuses |
Every bonus a commander grants, resolved into the right id space. |
equipment_set_bonuses |
The bonuses of the equipment sets the worn items and their gems complete. |
gem_bonuses |
The bonuses of the gem slotted in an item. |
effect_bonuses |
The bonuses of a list of commander effects, such as a |
attack_dialog_bonuses |
The one bonus list the attack dialog reads its commander's effects from. |
tool_effect_strength |
The summed strength of a tool's own effects of one type, with no condition. |
attacker_flank_effects |
Build a flank's attacker multipliers from resolved bonuses. |
support_tool_waves |
Extra waves the support tools grant. |
KEYED_EFFECT_TYPES
module-attribute
¶
KEYED_EFFECT_TYPES = frozenset({CombatEffectType.DEFENSE_SUPPORT_UNITS, CombatEffectType.ATTACK_SUPPORT_UNITS, CombatEffectType.RECRUITMENT_COST_DECREASE, CombatEffectType.RECRUITMENT_SPEED_BOOST, CombatEffectType.TOOL_PRODUCTION_SPEED_BOOST, CombatEffectType.UNIT_SPEED_BONUS, CombatEffectType.ATTACK_BONUS_UNIT, CombatEffectType.SPEED_BOOST_UNIT, CombatEffectType.LOOT_VALUE_BOOST_UNIT, CombatEffectType.FAME_BOOST_UNIT, CombatEffectType.CURRENCY_LOOT_BOOST, CombatEffectType.CRAFTING_QUEUE_PRODUCTION_BOOST, CombatEffectType.RESERVE_UNIT_KILL, CombatEffectType.SPAWN_RESERVE_UNIT, CombatEffectType.MUTATE_RESERVE_UNIT, CombatEffectType.ABILITY_PLUNDER})
UNCAPPED_EQUIPMENT_CAP_ID
module-attribute
¶
EquipmentBonusVO.capID of a bonus that overrides its cap (bundle line 20962).
Bonus
pydantic-model
¶
Bases: EmpireModel
One granted bonus: an id, its strength, and which id space the id is in.
A relic item's bonus ids index the relic effect table, and any other item's the equipment effect table, rather than the plain effect table. The tables overlap and disagree - id 4 is an economy effect in one and a gate reduction in another - so the space has to travel with the bonus.
Fields:
via_equipment
pydantic-field
¶
via_equipment: bool = False
The id is an equipment effect id, as on an item that is not a relic.
raw_values
pydantic-field
¶
The value array as sent, for the effect types that carry a keyed map.
strength
¶
What this bonus is worth for an effect of this type.
A keyed effect's array is [wod_id, value, ...], so its first number
is an id. EffectValueMap.strength returns the first value and
EffectValueWodID.strength the value of the first key - the same
number - so both read index 1.
EffectResolver
¶
EffectResolver(game_data: GameData)
Turns bonuses into combat multipliers using the game data tables.
Capping follows the client: bonuses are grouped by their effect's cap, each
group is summed up to that cap's ceiling, and the capped group totals are
then added together with no further ceiling. So a cap limits what stacks
within it, never the effect type as a whole. An equipment bonus whose
equipment effect row sets ignoreCap goes into a group of its own with no
ceiling.
Client: CastleEffectsHelper.getTotalEffectValue (bundle line 4139),
EquipmentBonusVO.capID and maxValueStrength (bundle line 20959).
Methods:
| Name | Description |
|---|---|
accumulate |
Total strength of one effect type across the given bonuses. |
effect_for |
The effect a bonus grants, resolved in the bonus's own id space. |
overrides_cap |
Whether an equipment bonus escapes its effect's cap. |
attack_multiplier |
The attacker's melee or ranged multiplier for a flank. |
flank_unit_bonus |
Percentage bonus to units on a side flank. |
front_unit_bonus |
Percentage bonus to units in the middle. |
yard_capacity_bonus |
Absolute unit bonus to the courtyard wave. |
yard_capacity_boost |
Percentage boost to the courtyard wave, applied as a multiplier. |
fortification_reductions |
Wall, gate and moat reductions as fractions. |
Attributes:
| Name | Type | Description |
|---|---|---|
game_data |
|
accumulate
¶
accumulate(bonuses: Iterable[Bonus], effect_type: CombatEffectType | int, *, area_type: int | None = None, player_target: bool | None = None, space_id: int | None = None, relation: str | None = None, raid_boss_id: int | None = None, include_economy: bool = False, ignore_cap: bool = False) -> float
Total strength of one effect type across the given bonuses.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bonuses
|
Iterable[Bonus]
|
The bonuses a commander (or other source) grants |
required |
effect_type
|
CombatEffectType | int
|
Which effect type to total, see |
required |
area_type
|
int | None
|
The target's area type; effects scoped to other areas are dropped, and None keeps all of them |
None
|
player_target
|
bool | None
|
True for a fight against a player, False for an NPC; None keeps effects flagged for either |
None
|
space_id
|
int | None
|
The castle space, for effects limited to one |
None
|
relation
|
str | None
|
Relationship to the target - |
None
|
raid_boss_id
|
int | None
|
The raid boss being fought, for boss-scoped effects |
None
|
include_economy
|
bool
|
Keep economy effects, which combat normally drops |
False
|
ignore_cap
|
bool
|
Skip capping entirely |
False
|
Returns:
| Type | Description |
|---|---|
float
|
The summed strength, 0.0 when nothing applies |
effect_for
¶
The effect a bonus grants, resolved in the bonus's own id space.
An equipment effect id names the effect in its row's effectID; an id
with no row is taken as a plain effect id.
Client: EquipmentXml.getEquippableEffectByEquipmentEffect (bundle
line 144136).
overrides_cap
¶
Whether an equipment bonus escapes its effect's cap.
Client: EquipmentBonusVO.parseBasic (bundle line 20942) reads the
equipment effect row's ignoreCap.
attack_multiplier
¶
attack_multiplier(bonuses: Iterable[Bonus], *, melee: bool, area_type: int | None = None, player_target: bool | None = None) -> float
The attacker's melee or ranged multiplier for a flank.
Mirrors getFullAttackBonusForLordByFlankAndAreaType: the general
attack bonus plus the matching side's plain and offensive bonuses, as a
percentage, on top of a base of 1.0.
flank_unit_bonus
¶
flank_unit_bonus(bonuses: Iterable[Bonus], *, area_type: int | None = None, player_target: bool | None = None) -> float
Percentage bonus to units on a side flank.
front_unit_bonus
¶
front_unit_bonus(bonuses: Iterable[Bonus], *, area_type: int | None = None, player_target: bool | None = None) -> float
Percentage bonus to units in the middle.
yard_capacity_bonus
¶
yard_capacity_bonus(bonuses: Iterable[Bonus], *, area_type: int | None = None, player_target: bool | None = None) -> float
Absolute unit bonus to the courtyard wave.
yard_capacity_boost
¶
yard_capacity_boost(bonuses: Iterable[Bonus], *, area_type: int | None = None, player_target: bool | None = None) -> float
Percentage boost to the courtyard wave, applied as a multiplier.
fortification_reductions
¶
fortification_reductions(bonuses: Iterable[Bonus], *, area_type: int | None = None, player_target: bool | None = None) -> tuple[float, float, float]
Wall, gate and moat reductions as fractions.
The client divides each by 100 before subtracting them from the defender's matching bonus.
parse_bonus_entries
¶
parse_bonus_entries(entries: Iterable[Any], *, via_relic: bool = False, via_equipment: bool = False) -> list[Bonus]
Parse the bonus encodings the server uses.
Three shapes occur and all are handled: [effect_id, value] or
[effect_id, [value]] as on an item that is not a relic,
[effect_id, [value], source_tag] as sent for a commander's own effects,
and [relic_effect_id, power, [value]] as on a relic item. The id is
always first; the strength is the first number found after it, preferring a
nested list, which is where the value sits when one is present.
Unparseable entries are skipped rather than failing the batch.
Client: BasicEquipmentVO.parseBonuses (bundle line 7135),
RelicBonusVO.parseRelicFromValueArray (bundle line 45132),
LordVO.parseRawEffects (bundle line 26483).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entries
|
Iterable[Any]
|
Raw bonus entries |
required |
via_relic
|
bool
|
The ids index the relic effect table, as they do for the bonuses inside a relic equipment item |
False
|
via_equipment
|
bool
|
The ids index the equipment effect table, as they do for the bonuses inside any other equipment item |
False
|
effect_value_bonuses
¶
effect_value_bonuses(effects: Iterable[EffectValue | EquipmentEffectValue]) -> list[Bonus]
The bonuses of an items row's effects: one per entry with a value.
A single number is the bonus's value. Anything else is a keyed value,
wod_id+value pairs joined by #, which the bonus keeps flattened as
EffectValueMap.parseFromValueString reads it (bundle line 31617): a
part without a +, or whose value is no number, adds a 0; a part whose
id is no number is left out. An equipment effect id is marked
via_equipment.
construction_item_bonuses
¶
Bonuses from the construction items placed on a castle's buildings.
These are the decorations players call look items, and they carry real combat bonuses - the flank unit limit item is where a +30% flank bonus comes from.
alliance_buff_bonuses
¶
Bonuses from the alliance's researched buffs, at their current levels.
global_effect_bonuses
¶
Bonuses from the global event effects currently running.
sceat_skill_bonuses
¶
Bonuses from unlocked sceat skills (the Hall of Legends trees).
general_skill_bonuses
¶
Bonuses from the skills unlocked on the general leading the attack.
general_passive_bonuses
¶
The general's passive effects, which join its commander's bonuses.
Client: GeneralVO.getPassiveEffects / getPassiveSkills (bundle line
26781): the boni of every unlocked skill that is not an ability skill, one
with an EFFECT_TYPE_UNLOCK_ABILITY effect (GeneralSkillVO.isAbilitySkill,
bundle line 113404). Skill effects parse as effectID&value
(GeneralSkillVO.parseXML, bundle line 113364).
legend_skill_value
¶
Total value of one legend skill effect type.
Legend skills sit outside the effect and cap pipeline: they name an effect type directly and the client sums their values as plain numbers, so they are returned as a number rather than as bonuses.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded tables |
required |
skill_ids
|
Iterable[int]
|
The player's unlocked legend skill ids |
required |
effect_type
|
str
|
The effect type name, e.g. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The summed value, 0.0 when none match |
global_unit_attack_bonuses
¶
global_unit_attack_bonuses(game_data: GameData, global_effects: Iterable[int | Sequence[int]], *, player_level: int | None = None, boosts: Mapping[GlobalEffect | int, float] | None = None) -> dict[int, float]
Per-unit attack bonuses from the global effects currently running.
This is the only thing that buffs a unit's attack value. The client reads it
in SoldierUnitVO.buffedMeleeAttack as
rawAttack + int(globalEffectData.getBonusByEffectType(ATTACK_BONUS_UNIT,
-1, -1, wodId)), and that getter looks only at the active global-effect
event: a commander carrying the same effect type does not buff units
through this path.
The rows encode a per-unit map, 273&<wod_id>+<strength>#<wod_id>+<strength>.
No condition is applied: the client resolves an unset area or kingdom to the
castle the player happens to be looking at, and the one effect row this path
can reach restricts neither, so there is nothing to check.
The strengths in the table are only a fallback: each running effect is a
[id, seconds_left, strength] row in the GE of the global effects
event's sei entry (Event.GLOBAL_EFFECT), and setEffectStrength
writes a strength above -1 onto every unit in the map
(GlobalEffectEventVO.parseParamObject, bundle lines 116399-116411). The
state reads those rows into GlobalEffectEvent.effects.
A boosted effect - one bie lists - gets the booster event's boost on top
(GlobalEffectData.parse_GIE, bundle lines 143676-143680):
addBuffStrengthValue writes the effect's strength (its first unit's) plus
the boost onto every unit of the map, as an int (GlobalEffectVO, bundle
lines 143711-143718), and the effect's bonus is that buffed copy while
bie lists it (bundle line 143712). Without a boost every unit keeps its
own strength, where the client reads the first unit's for all
(EffectValueMap.strength, bundle line 31649); live maps are uniform.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded tables |
required |
global_effects
|
Iterable[int | Sequence[int]]
|
Which global effects are active - either plain ids, or
the |
required |
player_level
|
int | None
|
The attacker's level, which some effects are bracketed to; without it the brackets are ignored |
None
|
boosts
|
Mapping[GlobalEffect | int, float] | None
|
What the booster adds to each boosted effect: the effects
|
None
|
Returns:
| Type | Description |
|---|---|
dict[int, float]
|
|
commander_bonuses
¶
commander_bonuses(game_data: GameData, commander: Commander, *, area_effects: Sequence[Bonus] | None = None) -> list[Bonus]
Every bonus a commander grants, resolved into the right id space.
Five sources from the gli payload: the commander's own effects (E),
its area effects (AE), the bonus list inside each equipped item, the
gem slotted in each item (see gem_bonuses), and the alien
equipment of AIE/TAE when EQ is empty, whose rows the client
builds as EquipmentBonusVO (AlienLordEquipmentVO and
AlienLordHeroVO.parseAlienBoniData, bundle lines 67479 and 67502) in
the commander's equipment slots. A relic item's
bonuses are tagged so they resolve through the relic effect table, and any
other item's so they resolve through the equipment effect table.
The bonuses come in the client's order, equipment slots first and then
E and AE, which matters once a capped total mixes signs. The gems of
alien equipment (GEM) count while AIE/TAE stand in for EQ.
The bonuses of the equipment sets worn (see equipment_set_bonuses)
come last, after AE, as the client adds them.
A default commander (commander_id below 0, such as -14, the bought
premium commander, or -15, a robber baron attack's) has no slots and no
sets: its bonuses are its lords row's effects (see
GameData.get_default_lord), which name equipment effect ids, then
E and AE. Without a row the client builds no commander, so only
area_effects count. Client: LordFactory.createLord (bundle lines
26399-26401) returns CastleLordData.getDefaultLordByID for an id below
0; DefaultLordVO.parseFromXml (bundle lines 101999-102002) reads the
row and DefaultLordVO.getUniqueBoni (bundle lines 102005-102011) returns
its effects, E and AE, with getCountOfSetId always 0 (bundle
line 102012).
A movement's commander resolves the same way. The client builds it from
the UM L block with LordFactory.createLord(e.L, true)
(BasicMapmovementVO.parseUnitMovement, bundle line 19385), the factory
of the gli roster too, and shows its effects through the same
LordVO.getUniqueBoni (CastleArmyListDialog.updateLord, bundle line
95640, into LordEffectTooltip.createContent, bundle line 65364) that the
attack dialog reads a roster commander's from (CastleEffectsHelper,
bundle line 4130). The movement tooltip leaves the general's passive
effects out and the attack dialog adds them (see
attack_dialog_bonuses); a movement's commander carries its general's
general_skill_ids for that. Legend and sceat skills belong to the
attacking player, not the commander, and a movement does not carry them.
A movement's commander comes with the area effects (AE) of the castle
it was sent from, so it matches a roster commander resolved with that
castle's aci AE as area_effects, not its bare gli entry.
Client: LordVO.getUniqueBoni (bundle line 26496). The attack dialog
replaces the commander's area effects with the aci AE list
(AttackDialogController.updateAreEffects, bundle line 4332), but the
areaEffects setter ignores an empty list (bundle line 26652), so the
commander keeps its own gli AE when aci sends none.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded tables, for the gems and equipment sets |
required |
commander
|
Commander
|
The commander's |
required |
area_effects
|
Sequence[Bonus] | None
|
The |
None
|
equipment_set_bonuses
¶
The bonuses of the equipment sets the worn items and their gems complete.
Each item whose set_id is not -1 counts once toward its set, and so does
each distinct gem slotted in an item whose gem row names a set_id above
- A relic item's relic gem counts toward no set. Every threshold row of a
set (
GameData.equipment_sets) whoseneeded_itemsthe count reaches grants its bonuses, which name equipment effect ids.
Client: LordVO.setCounts (bundle line 26576) and LordVO.getUniqueBoni
(bundle lines 26510-26518). BasicEquipmentVO.hasSetbonus (bundle line
7213) reads setID != -1; RelicGemVO keeps BasicEquippableVO's
setID of 0 (bundle line 4820). The alien equipment of AIE/TAE is
left out: the client counts it under set 0, which no set row uses.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded tables, for the gems and equipment sets |
required |
items
|
Iterable[Equipment]
|
The items worn, see |
required |
gem_bonuses
¶
The bonuses of the gem slotted in an item.
A gem id at index 10 other than -1 names a gems row, whose effects
are plain effectID&value bonuses; an id missing from the table grants
nothing. A relic item that carries index 12 has its relic gem there
instead, and that gem's bonuses resolve through the relic effect table.
Client: BasicEquipmentVO.parseEquipFromArray (bundle line 7116),
RelicEquipmentVO.parseEquipFromArray (bundle line 25039), which replaces
the index 10 gem with the index 12 one, CastleGemVO.parseXML (bundle
line 28287) and LordVO.getUniqueBoni (bundle line 26496).
effect_bonuses
¶
effect_bonuses(effects: Iterable[CommanderEffect]) -> list[Bonus]
The bonuses of a list of commander effects, such as a gli E or an aci AE.
attack_dialog_bonuses
¶
attack_dialog_bonuses(game_data: GameData, commander: Commander | None, *, area_effects: Sequence[Bonus] | None = None, general_skill_ids: Iterable[int] | None = None) -> list[Bonus]
The one bonus list the attack dialog reads its commander's effects from.
Client: LordVO.getUniqueBoni with the assigned general included, as
CastleEffectsHelper.getAccumulatedEquipmentBonusByEffectTypeForArea
calls it (bundle line 4136): the commander's equipment, effects and area
effects (see commander_bonuses) plus the general's passive effects.
Sceat skills are not part of it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded tables |
required |
commander
|
Commander | None
|
The commander leading the attack. Without one, only
|
required |
area_effects
|
Sequence[Bonus] | None
|
The |
None
|
general_skill_ids
|
Iterable[int] | None
|
The unlocked skills of the commander's general |
None
|
Returns:
| Type | Description |
|---|---|
list[Bonus]
|
Every bonus, unmerged; |
tool_effect_strength
¶
tool_effect_strength(game_data: GameData, tool: ToolStats, effect_type: CombatEffectType | int) -> float
The summed strength of a tool's own effects of one type, with no condition.
Client: ToolUnitVO.getEffectValue with NULL_CONDITION (bundle line
6640), as getBonusByEffect reads it for a mapped tool effect type: area,
space and wod id are all -1, so no effect is filtered out. The effects parse
as effectID&value (ToolUnitVO.parseEffects, bundle line 6644).
attacker_flank_effects
¶
attacker_flank_effects(resolver: 'EffectResolver', bonuses: Iterable[Bonus], *, area_type: int | None = None, player_target: bool | None = None, legend_skill_ids: Iterable[int] = (), legendary: bool = False, support_tools: Sequence[ToolStats] = ()) -> 'AttackerFlankEffects'
Build a flank's attacker multipliers from resolved bonuses.
Client: FightScreenHelper.getAttackerFlankEffectVO (bundle line 19179),
in its order: the commander's reductions and multipliers, then the legend
skills in a legendary fight, then each tool. The auto-fill starts from an
empty wave, so the tools here are only the support tools, one of each
AST entry. Tools the solver places later are added by
AttackerFlankEffects.apply_tool.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resolver
|
'EffectResolver'
|
Resolver over the loaded game data |
required |
bonuses
|
Iterable[Bonus]
|
The commander's bonuses, see |
required |
area_type
|
int | None
|
The target's area type, for scoping, such as a movement's
|
None
|
player_target
|
bool | None
|
True when attacking a player. The client passes no filter strategy here, which keeps PvP and PvE effects alike; that is what None does |
None
|
legend_skill_ids
|
Iterable[int]
|
The player's unlocked legend skills |
()
|
legendary
|
bool
|
|
False
|
support_tools
|
Sequence[ToolStats]
|
The support tools picked for the attack |
()
|
Returns:
| Type | Description |
|---|---|
'AttackerFlankEffects'
|
The flank's attacker effects |
support_tool_waves
¶
Extra waves the support tools grant.
Client: CastleFightItemContainer.getTotalBonusByToolEffect(ADDITIONAL_WAVE)
(bundle line 20719)
over the support container, one of each AST entry.
ToolEffectType.ADDITIONAL_WAVE is absolute (bundle line 9615), so the
effect's strength is taken as it is; it maps to effect type 156.
combat.capacity¶
How big a wave may be, ported from the client's CombatConst.
A wave's capacity is a function of one effective level: the attacker's own level, or the target's minimum defense level when that is higher. The client derives every number below from it, so a caller does not have to guess how many troops a flank holds.
Classes:
| Name | Description |
|---|---|
LegendaryFight |
Which legend skills an attack gets. The client checks three different rules. |
WaveCapacity |
The size of one wave at a given effective level. |
Functions:
| Name | Description |
|---|---|
owner_id_from_row |
The target owner's player id, as the client's map object reads it. |
is_npc_player |
Client: |
is_npc_pvp_player |
An NPC the game fights like a player: an alien invasion or a collector. |
max_attackers |
Total units a wave holds ( |
min_soldiers |
The fewest units an attack must carry at this level ( |
min_attack_soldiers |
The fewest units the waves of an attack on this target must carry together. |
flank_soldier_capacity |
Units one side flank holds ( |
middle_soldier_capacity |
Units the middle holds ( |
flank_tool_capacity |
Tools one side flank holds ( |
middle_tool_capacity |
Tools the middle holds ( |
boost_to_modifier |
Turn a "boost" effect into a multiplier ( |
minimum_owner_level |
The level a target defends at, whoever owns it. |
wave_level |
The level a wave is actually sized by. |
yard_capacity |
Units the courtyard - the final assault - wave holds. |
unlocked_slots |
How many of a flank's slots exist at this level. |
max_wave_count |
How many waves an attack may carry ( |
Attributes:
| Name | Type | Description |
|---|---|---|
WAVE_UNLOCK_LEVELS |
|
|
CONQUER_ADDITIONAL_WAVES |
|
|
UNIT_SLOT_LEVELS_FLANK |
|
|
UNIT_SLOT_LEVELS_MIDDLE |
|
|
TOOL_SLOT_LEVELS_FLANK |
|
|
TOOL_SLOT_LEVELS_MIDDLE |
|
|
TOOL_SLOT_TYPE_MIDDLE |
|
|
TOOL_SLOT_TYPE_FLANK |
|
|
LEVEL_CAP |
|
|
ALIEN_INVASION_PLAYER_IDS |
|
|
COLLECTOR_PLAYER_IDS |
The collector owners |
|
LANDMARK_AREA_TYPES |
|
|
ALIEN_INVASION_AREA_TYPES |
dict[int, int]
|
The area types built as an |
OTHER_PLAYER_INFO_AREA_TYPES |
Area types whose map object answers |
|
ROW_OWNER_AREA_TYPES |
Area types whose map row carries the owner's player id at index 4: |
|
AREA_TYPE_LEVEL_FLOORS |
dict[int, int]
|
|
LANDMARK_FLOOR_AREA_TYPES |
|
|
YARD_SLOTS |
Slots the courtyard wave offers. All eight are open from level 0. |
ALIEN_INVASION_PLAYER_IDS
module-attribute
¶
PlayerHelper.isAlienInvasion (bundle line 4688).
COLLECTOR_PLAYER_IDS
module-attribute
¶
COLLECTOR_PLAYER_IDS = frozenset({NPCOwner.COLLECTOR_CARNIVAL, NPCOwner.COLLECTOR_CHRISTMAS, NPCOwner.COLLECTOR_CHRISTMAS2, NPCOwner.COLLECTOR_SUMMER, NPCOwner.COLLECTOR_10TH_ANNIVERSARY, NPCOwner.COLLECTOR_ELEMENTAL, NPCOwner.COLLECTOR_HALLOWEEN, NPCOwner.COLLECTOR_HALLOWEEN2, NPCOwner.COLLECTOR_SPRING})
The collector owners PlayerHelper.isCollectorPlayer (bundle line 4690)
lists. The other five collectors are not in its switch.
LANDMARK_AREA_TYPES
module-attribute
¶
LANDMARK_AREA_TYPES = frozenset({int(MapItemType.METROPOL), int(MapItemType.CAPITAL), int(MapItemType.KINGS_TOWER), int(MapItemType.MONUMENT), int(MapItemType.LABORATORY)})
MapObjectHelper.isLandmark (bundle line 38531).
ALIEN_INVASION_AREA_TYPES
module-attribute
¶
ALIEN_INVASION_AREA_TYPES: dict[int, int] = {int(MapItemType.ALIEN_CAMP): int(NPCOwner.ALIEN_INVASION), int(MapItemType.RED_ALIEN_CAMP): int(NPCOwner.RED_ALIEN_INVASION)}
The area types built as an AAlienInvasionMapobjectVO (WorldmapObjectFactory,
bundle line 5357; its only subclasses close at bundle lines 65057 and 76481),
and the owner each one's constructor sets (_alienPlayerID, bundle lines
65049 and 76471). parseAreaInfo takes the owner from that id, not from the
row (bundle line 41545).
OTHER_PLAYER_INFO_AREA_TYPES
module-attribute
¶
OTHER_PLAYER_INFO_AREA_TYPES = frozenset({int(MapItemType.CASTLE), int(MapItemType.OUTPOST), int(MapItemType.CAPITAL), int(MapItemType.METROPOL), int(MapItemType.VILLAGE), int(MapItemType.ISLE_RESOURCE), int(MapItemType.KINGS_TOWER), int(MapItemType.MONUMENT), int(MapItemType.LABORATORY), int(MapItemType.FACTION_CAMP), int(MapItemType.ALLIANCE_BATTLE_GROUND_TOWER)})
Area types whose map object answers hasOtherPlayerInfo with true when
another player owns it: CastleMapobjectVO (bundle line 18933),
OutpostMapobjectVO (18838) and its capital and metropolis subclasses
(18760, 21637), VillageMapobjectVO (22675) and the resource isle
(34659), KingstowerMapobjectVO (19077), UpgradableLandmarkMapobjectVO
(42650) for monuments and laboratories (21675, 25920),
FactionCampMapobjectVO (21547) and ABGAllianceTowerMapobjectVO
(32307). The base InteractiveMapobjectVO answers false (3684), and so do
the Berimond faction capital, tower and village (22776, 22822, 28502).
ROW_OWNER_AREA_TYPES
module-attribute
¶
ROW_OWNER_AREA_TYPES = frozenset({int(MapItemType.CASTLE), int(MapItemType.OUTPOST), int(MapItemType.CAPITAL), int(MapItemType.METROPOL), int(MapItemType.VILLAGE), int(MapItemType.ISLE_RESOURCE), int(MapItemType.KINGS_TOWER), int(MapItemType.MONUMENT), int(MapItemType.LABORATORY), int(MapItemType.FACTION_CAMP)})
Area types whose map row carries the owner's player id at index 4:
InteractiveMapobjectVO.parseAreaInfo (bundle line 3631), which castles
(18910), outposts (through ContainerBuilderMapobjectVO, 6855) and faction
camps (FactionCampMapobjectVO.parseAreaInfo calls it through
FactionInteractiveMapobjectVO and ContainerBuilderMapobjectVO) use, and
the capital (18729), metropolis (21609), village (22623), resource isle
(34603), king's tower (19055), monument (21652) and laboratory (25900)
overrides.
AREA_TYPE_LEVEL_FLOORS
module-attribute
¶
AREA_TYPE_LEVEL_FLOORS: dict[int, int] = {int(MapItemType.KINGS_TOWER): 70, int(MapItemType.MONUMENT): 70, int(MapItemType.LABORATORY): 70}
LANDMARK_FLOOR_AREA_TYPES
module-attribute
¶
LANDMARK_FLOOR_AREA_TYPES = frozenset({int(MapItemType.CAPITAL), int(MapItemType.METROPOL)})
YARD_SLOTS
module-attribute
¶
Slots the courtyard wave offers. All eight are open from level 0.
LegendaryFight
pydantic-model
¶
Bases: EmpireModel
Which legend skills an attack gets. The client checks three different rules.
"Legend" means a legend level above zero (CastleUserData.isLegend,
bundle line 10085; WorldMapOwnerInfoVO.isLegend, bundle line 10892),
not a level of 70.
Fields:
-
unit_amount(bool) -
extra_wave(bool) -
flank_tools(bool)
unit_amount
pydantic-field
¶
unit_amount: bool
AttackDialogHelper.isLegendaryFight (bundle line 17378). Gates the
additionalUnitAmountOnFlank / OnFront skills.
extra_wave
pydantic-field
¶
extra_wave: bool
CastleAttackArmyVO.init (bundle line 55833). Gates the additionalWave skill.
flank_tools
pydantic-field
¶
flank_tools: bool
CastleAttackWaveVO constructor (bundle line 99927). Gates the
additionalAttackToolAmountFlank skill.
evaluate
classmethod
¶
evaluate(*, attacker_level: int, attacker_legend_level: int, target_owner_level: int, wave_level: int, owner_id: int | None, owner_legend_level: int, area_type: int | None, has_other_player_info: bool) -> 'LegendaryFight'
Apply the client's three rules.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attacker_level
|
int
|
The attacker's level ( |
required |
attacker_legend_level
|
int
|
The attacker's legend level |
required |
target_owner_level
|
int
|
|
required |
wave_level
|
int
|
The level a wave is sized by, |
required |
owner_id
|
int | None
|
The target owner's player id ( |
required |
owner_legend_level
|
int
|
The target owner's legend level
( |
required |
area_type
|
int | None
|
The target's area type |
required |
has_other_player_info
|
bool
|
The target's |
required |
WaveCapacity
pydantic-model
¶
Bases: EmpireModel
The size of one wave at a given effective level.
Build it with for_level rather than by hand:
capacity = WaveCapacity.for_level(70)
capacity.soldier_capacity(Flank.MIDDLE) # 192
capacity.unit_slots(Flank.LEFT) # 2
Fields:
-
level(int) -
flank_soldiers(int) -
middle_soldiers(int) -
flank_tools(int) -
middle_tools(int) -
flank_unit_slots(int) -
middle_unit_slots(int) -
flank_tool_slots(int) -
middle_tool_slots(int)
for_level
classmethod
¶
for_level(level: int, *, flank_bonus_percent: float = 0.0, front_bonus_percent: float = 0.0, tool_bonus: float = 0.0) -> 'WaveCapacity'
Derive a wave's capacity from the effective level and the unit bonuses.
The client resizes the flank containers after building a wave, using two separate effects: a "units on the flank" bonus for the two sides and a "units on the front" bonus for the middle, both read from the selected commander's equipment for the target's area type. Pass them to get the numbers the attack dialog shows; with both at zero these are the unbuffed base capacities.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
level
|
int
|
The target owner's level. The client sizes a wave from
|
required |
flank_bonus_percent
|
float
|
Total percentage bonus to units on each side flank - commander equipment, the general's skills, and legend skills when the fight is legendary |
0.0
|
front_bonus_percent
|
float
|
The same for the middle |
0.0
|
tool_bonus
|
float
|
Extra flank tool capacity, e.g. from the ADDITIONAL_ATTACK_TOOL_AMOUNT_FLANK legend skill |
0.0
|
tool_slot_type
¶
Which slot type a tool must fit for this flank.
owner_id_from_row
¶
is_npc_player
¶
Client: PlayerHelper.isNPCPlayer (bundle line 4687).
is_npc_pvp_player
¶
An NPC the game fights like a player: an alien invasion or a collector.
Client: PlayerHelper.isNpcPvpPlayer (bundle line 4689).
max_attackers
¶
Total units a wave holds (CombatConst.getMaxAttackers).
min_soldiers
¶
The fewest units an attack must carry at this level (CombatConst.getMinSoldiers).
A tenth of what one wave holds, rounded down: 8 at level 16, 26 from level 51 to 69, and 32 from level 70.
Client: CombatConst.getMinSoldiers (ggs.dll line 18895)
min_attack_soldiers
¶
min_attack_soldiers(target_owner_level: int, area_type: int | None = None, *, landmark_min_level: int = 0) -> int
The fewest units the waves of an attack on this target must carry together.
The client refuses to send fewer and shows errorCode_100 with this
number. The server refused a player's castle below it with
MOVEMENT_HAS_NO_UNITS (100); whether it enforces it on NPC camps is
unverified. Only the waves' units count: not their tools, and not the
courtyard wave. The level is the target owner's; a capital or metropolis
uses its landmark's defense level when that is higher, and a king's
tower, monument or laboratory always 70. The result equals
min_soldiers of the level the waves are sized by.
Client: AttackDialogStartAttackCheck.onAttack (bundle lines 56306-56313),
with CombatConst.getMinSoldiers (ggs.dll line 18895) and
OutpostConst's default levels (ggs.dll line 19549)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target_owner_level
|
int
|
The client's |
required |
area_type
|
int | None
|
The target's area type |
None
|
landmark_min_level
|
int
|
The capital's or metropolis's |
0
|
Returns:
| Type | Description |
|---|---|
int
|
The fewest units the waves must carry |
flank_soldier_capacity
¶
Units one side flank holds (getAmountSoldiersFlank).
bonus_percent is the client's "units on the flank" bonus, which comes
from the selected commander's equipment for the target's area type.
middle_soldier_capacity
¶
Units the middle holds (getAmountSoldiersMiddle).
The middle takes whatever the two side flanks leave, so their unbonused
size is what is subtracted. bonus_percent is the client's "units on the
front" bonus, which is a different effect from the flank bonus - the two
scale the middle and the sides independently.
flank_tool_capacity
¶
Tools one side flank holds (getTotalAmountTools for a flank).
middle_tool_capacity
¶
Tools the middle holds (getTotalAmountTools for the middle).
boost_to_modifier
¶
Turn a "boost" effect into a multiplier (EffectConst.boostToModifier).
A boost of 0 is a multiplier of 1.0; the client adds it to a base of 100 and scales by 1/100, flooring at zero.
minimum_owner_level
¶
minimum_owner_level(target_level: int, area_type: int | None = None, *, landmark_min_level: int = 0) -> int
The level a target defends at, whoever owns it.
AInteractiveMapobjectVO.minimumOwnerLevel returns the owner's own level,
and the landmark subclasses override it with a fixed one: a king's tower,
monument and laboratory answer 70, a capital and a metropolis their
landmark's minDefenseLevel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target_level
|
int
|
The target owner's level |
required |
area_type
|
int | None
|
The target's area type |
None
|
landmark_min_level
|
int
|
The landmark's |
0
|
Returns:
| Type | Description |
|---|---|
int
|
The level the area type defends at |
wave_level
¶
The level a wave is actually sized by.
Some targets defend at a floor of their own however low their owner's level is: attack a level 12 player's monument and the wave is built for level 70.
Returns:
| Type | Description |
|---|---|
int
|
The higher of the owner's level and the area type's own level |
yard_capacity
¶
yard_capacity(attacker_level: int, target_level: int, *, bonus: float = 0.0, boost: float = 0.0) -> int
Units the courtyard - the final assault - wave holds.
CombatConst.getMaxUnitsInReinforcementWave: unlike a flank, this one
grows with both levels, and the additions are absolute units rather than
percentages.
Verified against four captured dialogs from one account, which give an identical implied bonus of 2872 at target levels 1, 13, 45 and 70: 3109, 3349, 3989 and 4489.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attacker_level
|
int
|
The attacker's own level |
required |
target_level
|
int
|
The target owner's level, or the minimum owner level when the target is under conquer control |
required |
bonus
|
float
|
Absolute unit bonus, effect type 179 |
0.0
|
boost
|
float
|
Percentage boost, effect type 180, applied as a multiplier last |
0.0
|
Returns:
| Type | Description |
|---|---|
int
|
The courtyard wave's capacity |
unlocked_slots
¶
How many of a flank's slots exist at this level.
max_wave_count
¶
combat.defense¶
Building a defender's flank strength from an army composition.
Two sources of composition are supported: an NPC camp, whose defenders the items payload lists offline, and an explicit stack list, which is what a spy report gives for a player's castle.
Functions:
| Name | Description |
|---|---|
camp_level |
A camp's level, from its victory count ( |
invasion_camp_level |
The level an invasion event target is fought at, which its row does not carry. |
camp_fortification_levels |
A camp's wall and gate levels ( |
fortification_bonuses |
Wall, gate and moat protection as fractions, from building levels. |
tool_defense_bonus |
How much a defending tool raises both defender multipliers through its effects. |
defender_flank_effects |
Aggregate a flank's defenders into strength values. |
castellan_defense_multiplier |
How much a defending castellan multiplies its own defenders by. |
castellan_fortification |
What a defending castellan adds to its castle's wall, gate and moat. |
spied_castle_defense |
What defends a spied castle, per flank. |
npc_camp_defense |
What defends an NPC camp, per flank, without asking the server. |
event_camp_defense |
What defends an event camp, per flank. |
CAMP_KINGDOM_OFFSETS
module-attribute
¶
CAMP_KINGDOM_OFFSETS: dict[Kingdom, int] = {Kingdom.GREEN: 1, Kingdom.ICE: 20, Kingdom.SANDS: 35, Kingdom.FIRE: 45}
camp_level
¶
invasion_camp_level
¶
invasion_camp_level(game_data: GameData, item: MapAreaItem, player_level: int) -> int | None
The level an invasion event target is fought at, which its row does not carry.
The four types do not share a rule, because only three of them are camps:
- A samurai or nomad camp starts where the attacking player's league band in its event starts and climbs by one for every defeat it has taken.
- A daimyo castle names a rank instead of a defeat count, and the rank carries the level.
- A daimyo township is not a camp. The game counts the attacking player as its owner, so it is fought at that player's own level and its rank says nothing about it.
A row that names a difficulty scaling camp overrides the level of the camps, which is how a chosen difficulty raises them for one player.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded items payload |
required |
item
|
MapAreaItem
|
The target's map row |
required |
player_level
|
int
|
The attacking player's level |
required |
Returns:
| Type | Description |
|---|---|
int | None
|
The level to size the wave at, or None when this is not one of the |
int | None
|
four or the tables do not describe it |
camp_fortification_levels
¶
fortification_bonuses
¶
fortification_bonuses(game_data: GameData, *, wall_level: int = 0, gate_level: int = 0, moat_level: int = 0) -> tuple[float, float, float]
Wall, gate and moat protection as fractions, from building levels.
Each level indexes the matching building, whose items column is a percentage; the client divides by 100 before using it as a defense.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded tables |
required |
wall_level
|
int
|
The defender's wall level, 0 for none |
0
|
gate_level
|
int
|
The defender's gate level |
0
|
moat_level
|
int
|
The defender's moat level |
0
|
Returns:
| Type | Description |
|---|---|
tuple[float, float, float]
|
|
tool_defense_bonus
¶
How much a defending tool raises both defender multipliers through its effects.
Client: ToolUnitVO.getBonusByEffect(ToolEffectType.DEFENSE_BONUS)
(bundle line 6562) with getEffectValue under NULL_CONDITION (bundle
line 6640). It sums the value of every effect of type 31 on the tool, with
no area, space or cap filter (EffectValueSimple.add, bundle line 17753),
and scales by 0.01 because DEFENSE_BONUS is not an absolute bonus
(bundle line 9609). Type 31 is the one effect type mapped to
ToolEffectType.DEFENSE_BONUS.
defender_flank_effects
¶
defender_flank_effects(stacks: Stacks, game_data: GameData, *, flank: Flank | None = None, melee_bonus: float = 1.0, range_bonus: float = 1.0, wall_bonus: float = 0.0, gate_bonus: float = 0.0, moat_bonus: float = 0.0) -> DefenderFlankEffects
Aggregate a flank's defenders into strength values.
Client: FightScreenHelper.getDefendingUnitStrength (bundle line 19118)
and the item loop of getDefenceBonuses (bundle line 19148). A soldier
adds both its melee and its ranged defense, times its count, to the melee
group when its role is melee and to the ranged group otherwise. A tool
adds, once per stack whatever its count, its defMeleeBonus and
defRangeBonus to the melee and ranged multipliers, then its
DEFENSE_BONUS effect (tool_defense_bonus) to both, and its
wall, gate and moat columns to the fortification.
Say which flank this is and the gate is dropped everywhere but the middle,
which is what getDefenceBonuses returns - only the middle meets the gate,
so gate tools are wasted anywhere else.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stacks
|
Stacks
|
|
required |
game_data
|
GameData
|
Loaded stats, for the defenders' defense values |
required |
flank
|
Flank | None
|
Which flank these defenders hold; without it the gate is left alone |
None
|
melee_bonus
|
float
|
Defender melee multiplier before the tools (1.0 = unbuffed) |
1.0
|
range_bonus
|
float
|
Defender ranged multiplier before the tools |
1.0
|
wall_bonus
|
float
|
Base wall bonus as a fraction |
0.0
|
gate_bonus
|
float
|
Base gate bonus as a fraction |
0.0
|
moat_bonus
|
float
|
Base moat bonus as a fraction |
0.0
|
Returns:
| Type | Description |
|---|---|
DefenderFlankEffects
|
The flank's defender effects |
castellan_defense_multiplier
¶
castellan_defense_multiplier(resolver: EffectResolver, bonuses: Sequence[Bonus], *, flank: Flank, melee: bool, area_type: int | None = None) -> float
How much a defending castellan multiplies its own defenders by.
CastleEffectsHelper.getFullDefenseBonusForLordByFlankAndAreaType: the
all-flank defense bonus, plus the melee or ranged one, plus whichever
position-specific boost applies. The result is a percentage the caller adds
to 1.0.
One client quirk is kept: the courtyard boost (type 32) is added on the
middle flank, not the courtyard, so it never reaches the courtyard fight
through this path. The client reads it as rawValues[0] rather than
strength, which for a plain value is the same number - both return
_value, and the cap was already applied when the effects were merged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resolver
|
EffectResolver
|
Effect resolver over the loaded tables |
required |
bonuses
|
Sequence[Bonus]
|
The castellan's bonuses, from |
required |
flank
|
Flank
|
Which flank is being defended |
required |
melee
|
bool
|
True for the melee multiplier, False for the ranged one |
required |
area_type
|
int | None
|
The target's area type, which scopes the effects |
None
|
Returns:
| Type | Description |
|---|---|
float
|
The percentage to add to 1.0 |
castellan_fortification
¶
castellan_fortification(resolver: EffectResolver, bonuses: Sequence[Bonus], *, area_type: int | None = None) -> tuple[float, float, float]
What a defending castellan adds to its castle's wall, gate and moat.
getDefenceBonuses adds these on top of the structures' own protection,
as fractions.
spied_castle_defense
¶
spied_castle_defense(game_data: GameData, spy_army: 'SpyArmy', *, wall_bonus: float = 0.0, gate_bonus: float = 0.0, moat_bonus: float = 0.0, melee_bonus: float = 1.0, range_bonus: float = 1.0, castellan: 'Commander | None' = None, defender_legend_skill_ids: Sequence[int] | None = None, area_type: int | None = None) -> dict[Flank, DefenderFlankEffects]
What defends a spied castle, per flank.
Client: FightScreenHelper.getDefendingUnitStrength (bundle line 19118)
and getDefenceBonuses (bundle line 19148), which build one of these per
flank from that flank's own stacks plus the support stacks. The flanks
differ: a defending tool raises the multipliers and fortification of the
flank it sits on and no other.
Each value is summed in the client's order: the base, then the flank's
items, then the castellan, then the defender's legend skills one at a time.
defenseMeleeBonus and defenseRangeBonus skills add a hundredth of
their totalEffectValue to the melee and ranged multipliers, and
wallBonus, gateBonus and moatBonus skills to the fortification.
The gate is then dropped everywhere but the middle.
The keep's stacks become the courtyard flank.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded stats |
required |
spy_army
|
'SpyArmy'
|
The report's positional army, from |
required |
wall_bonus
|
float
|
The castle's own wall protection, as a fraction |
0.0
|
gate_bonus
|
float
|
Its gate protection; only the middle flank keeps it |
0.0
|
moat_bonus
|
float
|
Its moat protection |
0.0
|
melee_bonus
|
float
|
Defender melee multiplier before anything is added |
1.0
|
range_bonus
|
float
|
Defender ranged multiplier before anything is added |
1.0
|
castellan
|
'Commander | None'
|
The defending castellan the attack pre-calculation names. Its equipment raises both the fortification and the defenders, and differently per flank |
None
|
defender_legend_skill_ids
|
Sequence[int] | None
|
The defender's legend skill ids, which the
client takes from |
None
|
area_type
|
int | None
|
The target's area type, which scopes the castellan's effects |
None
|
Returns:
| Type | Description |
|---|---|
dict[Flank, DefenderFlankEffects]
|
Effects per flank |
Raises:
| Type | Description |
|---|---|
ValueError
|
A legend skill id is not in the items payload. The client
looks each one up ( |
npc_camp_defense
¶
npc_camp_defense(game_data: GameData, victories: int, kingdom_id: Kingdom = GREEN) -> dict[Flank, DefenderFlankEffects] | None
What defends an NPC camp, per flank, without asking the server.
The items payload lists a camp's defenders by victory count, so a robber baron camp's defense can be computed offline instead of spying it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded stats |
required |
victories
|
int
|
The camp's victory count (negative for a robber baron camp) |
required |
kingdom_id
|
Kingdom
|
Kingdom the camp sits in |
GREEN
|
Returns:
| Type | Description |
|---|---|
dict[Flank, DefenderFlankEffects] | None
|
Effects per flank, or None when the payload lists no such camp. The |
dict[Flank, DefenderFlankEffects] | None
|
camp's own wall and gate protection is included, derived from the level |
dict[Flank, DefenderFlankEffects] | None
|
its victory count implies. |
event_camp_defense
¶
event_camp_defense(game_data: GameData, table: str, victories: int) -> dict[Flank, DefenderFlankEffects] | None
What defends an event camp, per flank.
Nomad, samurai, faction invasion and alliance invasion camps are described differently from robber baron camps: instead of a per-flank composition they give a defense strength, a unit and tool list, and wall and gate bonuses. The same units defend every flank, so the composition is split evenly and the fortification applied to each.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
game_data
|
GameData
|
Loaded tables |
required |
table
|
str
|
One of |
required |
victories
|
int
|
The camp's victory count |
required |
Returns:
| Type | Description |
|---|---|
dict[Flank, DefenderFlankEffects] | None
|
Effects per flank, or None when no such camp is listed. Unit counts are |
dict[Flank, DefenderFlankEffects] | None
|
not given by these tables, so the strengths reflect one of each listed |
dict[Flank, DefenderFlankEffects] | None
|
unit; treat the wall and gate bonuses as the reliable part. |
combat.effects¶
Flank effect values, ported from the game client's combat maths.
The client feeds an attacker-side and a defender-side effect object into its
auto-fill strategy. Both are plain value holders: the interesting part is how
their inputs are aggregated, which lives in empire_core.combat.defense.
Classes:
| Name | Description |
|---|---|
AttackerFlankEffects |
The attacker's multipliers for one flank. |
DefenderFlankEffects |
The defender's strength on one flank. |
AttackerFlankEffects
pydantic-model
¶
Bases: EmpireModel
The attacker's multipliers for one flank.
Bonuses are multipliers, so 1.0 means an unbuffed attack. Reductions are subtracted from the defender's matching bonus.
Fields:
-
melee_bonus(float) -
range_bonus(float) -
defender_range_reduction(float) -
defender_melee_reduction(float) -
gate_reduction(float) -
wall_reduction(float) -
moat_reduction(float)
apply_tool
¶
apply_tool(tool: ToolStats, count: int, *, range_malus: float = 0.0, melee_malus: float = 0.0) -> 'AttackerFlankEffects'
Fold a placed tool's contribution into these effects.
AttackerFlankEffectVO.updateEffectsWithNewTool: each tool adds its
fortification and defense reductions times the number placed, which is
why the client places tools before soldiers - the soldiers are then
picked against a defense the tools have already dented.
A tool's own effects may add range or melee defense maluses on top of
its columns. Resolving those needs the game data and the target's area
type, which this layer does not have, so the caller resolves them and
passes them in - the same terms the strategy scored the tool on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tool
|
ToolStats
|
The tool that was placed |
required |
count
|
int
|
How many of it |
required |
range_malus
|
float
|
Range-defense malus from the tool's effects, type 217 |
0.0
|
melee_malus
|
float
|
Melee-defense malus from the tool's effects, type 215 |
0.0
|
Returns:
| Type | Description |
|---|---|
'AttackerFlankEffects'
|
A new effects object; the original is unchanged |
soldier_stack_attack_value
¶
soldier_stack_attack_value(unit: UnitStats, free_items: int, available: int, attack_bonus: float = 0.0) -> int
What a stack of this unit is worth on this flank.
Mirrors AttackerFlankEffectVO.getSoldierStackAttackValue: the unit's
attack times the matching bonus, truncated, times however many of it
actually fit. Which of the two attack columns is read follows the unit's
role, not which column happens to be filled in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
unit
|
UnitStats
|
The unit being considered |
required |
free_items
|
int
|
Remaining troop capacity of the flank |
required |
available
|
int
|
How many of the unit the player owns |
required |
attack_bonus
|
float
|
A flat addition to this unit's attack from an active global effect, added before the multiplier as the client does |
0.0
|
Returns:
| Type | Description |
|---|---|
int
|
The stack's attack value, 0 for a unit with no matching role |
DefenderFlankEffects
pydantic-model
¶
Bases: EmpireModel
The defender's strength on one flank.
The four strengths are sums over the defending army, split by the defenders'
role: melee-role defenders contribute to melee_units_*, ranged-role
defenders to range_units_*. Bonuses are multipliers starting at 1.0.
Fields:
-
melee_units_melee_strength(float) -
melee_units_range_strength(float) -
range_units_melee_strength(float) -
range_units_range_strength(float) -
melee_bonus(float) -
range_bonus(float) -
wall_bonus(float) -
gate_bonus(float) -
moat_bonus(float)
has_melee_defenders
property
¶
has_melee_defenders: bool
Whether any melee-role defender holds this flank.
has_range_defenders
property
¶
has_range_defenders: bool
Whether any ranged-role defender holds this flank.
melee_defense_value
¶
Defense against a melee attack (getMeleeDefenceValue).
range_defense_value
¶
Defense against a ranged attack (getRangeDefenceValue).
combat.tools¶
Placing tools in a wave's tool slots.
Ported from AFillFlankStrategy.fillFlankWithTools and checkFlank. The
mechanics are here; the strategies that choose which tool are supplied by the
caller, because the client's five are still being derived and a plausible
substitute would produce waves that look right and are not what the game builds.
Classes:
| Name | Description |
|---|---|
TargetContext |
What a strategy needs to know about the target to judge a tool. |
FilledTools |
What a flank's tool pass produced. |
ToolStrategy |
Chooses one tool for a slot, or None when it has nothing to offer. |
ReduceDefenceBonusStrategy |
Picks the tool that best cancels one of the defender's bonuses. |
Functions:
| Name | Description |
|---|---|
conditioned_effect_bonus |
A tool's contribution from its effects rather than its columns. |
can_use_tool_on_target |
Whether this tool may be carried against this target at all. |
is_tool_usable_against_active_raid_boss |
Whether a tool's raid-boss ties, if any, allow it now. |
default_tool_strategies |
The client's five strategies, in pool order. |
fill_flank_with_tools |
Fill one flank's tool slots. |
check_flank |
Drop a flank's tools when it has no units to use them. |
Attributes:
| Name | Type | Description |
|---|---|---|
ALIEN_INVASION_AREA_TYPES |
|
ALIEN_INVASION_AREA_TYPES
module-attribute
¶
ALIEN_INVASION_AREA_TYPES = frozenset({MapItemType.ALIEN_CAMP, MapItemType.RED_ALIEN_CAMP})
TargetContext
dataclass
¶
TargetContext(area_type: int | None = None, space_id: int | None = None, is_player: bool = True, active_raid_boss_id: int | None = None)
What a strategy needs to know about the target to judge a tool.
The client passes the defender's area and space id down into
pickToolByStrategy; their None defaults are permissive, matching a tool
that names no restriction.
active_raid_boss_id is the boss of the alliance raid-boss event that is
running now, None when none is. The client reads it from
AllianceRaidbossEventEventVO.getActiveEventVO().raidBossServerDataVO
(EffectConditionHelper.isEffectForActiveRaidBoss, bundle line 32868),
whatever the target is.
Attributes:
| Name | Type | Description |
|---|---|---|
area_type |
int | None
|
|
space_id |
int | None
|
|
is_player |
bool
|
|
active_raid_boss_id |
int | None
|
|
is_alien_invasion |
bool
|
Whether the target is an alien invasion camp. |
FilledTools
¶
Bases: NamedTuple
What a flank's tool pass produced.
effects is the attacker's side of the flank after the tools were
placed. The client passes one AttackerFlankEffectVO per flank into both
passes of fillWave and updateEffectsWithNewTool mutates it, so the
soldier pass that follows scores units against the dented defense. The VO is
rebuilt for each wave (autoFillWave calls getAttackerEffectVO afresh),
so nothing carries from one wave to the next.
Attributes:
| Name | Type | Description |
|---|---|---|
placed |
list[tuple[int, int]]
|
|
effects |
AttackerFlankEffects
|
|
ToolStrategy
¶
Bases: Protocol
Chooses one tool for a slot, or None when it has nothing to offer.
The client keeps five of these in a pool and pops one whenever it fails to produce a tool that fits the slot, so a strategy returning None retires for the rest of the flank.
ReduceDefenceBonusStrategy
dataclass
¶
ReduceDefenceBonusStrategy(name: str, tool_bonus: Callable[[ToolStats], float], defender_bonus: Callable[[AttackerFlankEffects, DefenderFlankEffects], float], requires: Callable[[DefenderFlankEffects], bool] | None = None, malus_effect_type: CombatEffectType | int | None = None)
Picks the tool that best cancels one of the defender's bonuses.
Ported from AReduceDefenseBonusStrategy.pickToolByStrategy. Given how
much of a defense remains after the attacker's own reduction, it prefers the
tool that cancels it outright with the fewest units; failing that, the one
that removes the most in the units that fit. A defense already at or below
zero needs no tool, and the strategy retires.
The client builds five of these, one per defender bonus; see
default_tool_strategies.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
|
tool_bonus |
Callable[[ToolStats], float]
|
|
defender_bonus |
Callable[[AttackerFlankEffects, DefenderFlankEffects], float]
|
|
requires |
Callable[[DefenderFlankEffects], bool] | None
|
|
malus_effect_type |
CombatEffectType | int | None
|
|
defender_bonus
instance-attribute
¶
defender_bonus: Callable[[AttackerFlankEffects, DefenderFlankEffects], float]
requires
class-attribute
instance-attribute
¶
requires: Callable[[DefenderFlankEffects], bool] | None = None
malus_effect_type
class-attribute
instance-attribute
¶
malus_effect_type: CombatEffectType | int | None = None
conditioned_effect_bonus
¶
conditioned_effect_bonus(game_data: GameData, tool: ToolStats, effect_type: CombatEffectType | int, area_type: int | None = None) -> float
A tool's contribution from its effects rather than its columns.
AReduceDefenseBonusStrategy.getConditionedEffectBonus: sum a hundredth
of each matching effect's strength, keeping only effects that apply to the
target's area. Six tools today carry a defense malus this way and no column
at all, so without this they look useless to the strategies.
can_use_tool_on_target
¶
can_use_tool_on_target(tool: ToolStats, target: TargetContext, game_data: GameData) -> bool
Whether this tool may be carried against this target at all.
Client: AttackHelper.canUseToolForAttackOnTarget (bundle line 8583). A
tool flagged canBeUsedToAttackNPC="0" is only allowed against another
player or an alien invasion camp, allowedToAttack restricts a tool to
named kingdoms and area types, and a tool tied to raid bosses needs one of
them to be the active one.
The client's last branch, an event tool with no inventory left, cannot be reached from the fill path - it needs an amount of zero, which the pick already skips.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tool
|
ToolStats
|
The tool being considered |
required |
target
|
TargetContext
|
Where the attack is going |
required |
game_data
|
GameData
|
Loaded effects, to read which raid bosses the tool's effects are tied to |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when the tool may be used |
is_tool_usable_against_active_raid_boss
¶
is_tool_usable_against_active_raid_boss(tool: ToolStats, game_data: GameData, active_raid_boss_id: int | None) -> bool
Whether a tool's raid-boss ties, if any, allow it now.
Client: AttackHelper.isToolUsableAgainstActiveRaidBoss (bundle line
8593) with EffectConditionHelper.isEffectForActiveRaidBoss (bundle line
32868). A tool with no effects, or with one effect tied to no raid boss,
is usable. Otherwise one of its effects must be tied to the active boss.
An effect id the effects table does not know is skipped, as the client
skips a BonusVO without an effect, so a tool whose effects are all
unknown stays usable.
ToolUnitVO.parseEffects (bundle line 6644) makes one entry per
non-empty effects segment and resolves the id before &.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tool
|
ToolStats
|
The tool being considered |
required |
game_data
|
GameData
|
Loaded effects |
required |
active_raid_boss_id
|
int | None
|
The active raid-boss event's boss, None when no raid-boss event is running |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when the tool may be used |
default_tool_strategies
¶
default_tool_strategies() -> list[ReduceDefenceBonusStrategy]
The client's five strategies, in pool order.
fillToolStrategyPool pushes moat, range, melee, gate then wall, and pops
from the end - so wall is tried first and moat last.
fill_flank_with_tools
¶
fill_flank_with_tools(capacity: int, slots: int, slot_type: int, inventory: 'Inventory', game_data: GameData, strategies: Sequence[ToolStrategy], *, attacker: AttackerFlankEffects | None = None, defender: DefenderFlankEffects | None = None, target: TargetContext | None = None, used_per_type: dict[str, int] | None = None) -> FilledTools
Fill one flank's tool slots.
Follows the client: for each free slot, ask the last strategy in the pool for a tool. A tool that does not fit this slot type retires that strategy and the next one is tried; when the pool empties, filling stops. A tool already present in the flank merges into its existing slot rather than taking a new one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
capacity
|
int
|
The flank's tool capacity |
required |
slots
|
int
|
How many unlocked tool slots the flank has |
required |
slot_type
|
int
|
The slot type a tool must fit, from |
required |
inventory
|
'Inventory'
|
Pool to draw from; placed tools are deducted |
required |
game_data
|
GameData
|
Loaded tool stats |
required |
strategies
|
Sequence[ToolStrategy]
|
The strategy pool, tried from the end |
required |
attacker
|
AttackerFlankEffects | None
|
Attacker multipliers, which each placed tool feeds |
None
|
defender
|
DefenderFlankEffects | None
|
The defender's strength on this flank |
None
|
target
|
TargetContext | None
|
Where the attack is going, which scopes a tool's effects and decides whether it may be carried at all |
None
|
used_per_type
|
dict[str, int] | None
|
The wave's per-tool-type budget so far, shared across its three flanks and updated in place |
None
|
Returns:
| Type | Description |
|---|---|
FilledTools
|
The |
FilledTools
|
the soldier pass should then use |
Note
One deliberate divergence. The client deducts a stack inside the pick, before it knows whether the tool fits the slot, and drops it on the floor when it does not - the tools are gone from the inventory and never placed. This deducts only what it places. The loss is reachable on a side flank through the range strategy, so a wave built here can carry tools a wave built in the client would have lost.
The effects passed in are not mutated; the updated ones come back in
FilledTools.effects.
check_flank
¶
check_flank(tools: list[tuple[int, int]], units: list[tuple[int, int]], inventory: 'Inventory', *, game_data: GameData | None = None, used_per_type: dict[str, int] | None = None) -> bool
Drop a flank's tools when it has no units to use them.
AFillFlankStrategy.checkFlank: tools without units are returned to the
inventory rather than sent, since they would be spent for nothing.
The per-wave budget is given back too. The client reads it live off the
wave's containers (getSumOfToolsByTool), so emptying one flank frees that
allowance for the flanks filled after it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tools
|
list[tuple[int, int]]
|
The flank's placed tools |
required |
units
|
list[tuple[int, int]]
|
The flank's placed units |
required |
inventory
|
'Inventory'
|
Pool the tools return to |
required |
game_data
|
GameData | None
|
Tool stats, to credit the budget back; |
None
|
used_per_type
|
dict[str, int] | None
|
The wave's per-tool-type budget, updated in place |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
True when the flank stands, False when its tools were taken back |
Raises:
| Type | Description |
|---|---|
GameDataNotLoadedError
|
Tools are taken back against |