Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pillage simulation tool #2131

Merged
merged 9 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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 .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
scarb 2.8.4
dojo 1.0.0
dojo 1.0.1
9 changes: 7 additions & 2 deletions client/src/ui/components/military/EntitiesArmyTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import Button from "@/ui/elements/Button";
import { Headline } from "@/ui/elements/Headline";
import { HintModalButton } from "@/ui/elements/HintModalButton";
import { ResourceIcon } from "@/ui/elements/ResourceIcon";
import { BattleSimulation } from "@/ui/modules/battle-simulation/BattleSimulation";
import { BattleSimulation } from "@/ui/modules/simulation/BattleSimulation";
import { PillageSimulation } from "@/ui/modules/simulation/pillage-simulation";
import { divideByPrecision } from "@/ui/utils/utils";
import { ID, ResourcesIds } from "@bibliothecadao/eternum";
import { HintSection } from "../hints/HintModal";
import { battleSimulation } from "../navigation/Config";
import { battleSimulation, pillageSimulation } from "../navigation/Config";
import { ArmyChip } from "./ArmyChip";

export const EntitiesArmyTable = () => {
Expand All @@ -22,8 +23,12 @@ export const EntitiesArmyTable = () => {
<Button variant="primary" className="mx-auto" size="md" onClick={() => togglePopup(battleSimulation)}>
Simulate a battle
</Button>
<Button variant="primary" className="mx-auto" size="md" onClick={() => togglePopup(pillageSimulation)}>
Simulate a pillage
</Button>
</div>
<BattleSimulation />
<PillageSimulation />
{playerStructures().map((entity: any, index: number) => {
return (
<div key={entity.entity_id} className="p-2">
Expand Down
4 changes: 3 additions & 1 deletion client/src/ui/components/navigation/Config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ type OSWindows =
| "Assistant"
| "Quests"
| "Social"
| "BattleSimulation";
| "BattleSimulation"
| "PillageSimulation";

export interface OSInterface {
onClick: () => void;
Expand All @@ -35,3 +36,4 @@ export const construction: OSWindows = "Construction";
export const quests: OSWindows = "Quests";
export const social: OSWindows = "Social";
export const battleSimulation: OSWindows = "BattleSimulation";
export const pillageSimulation: OSWindows = "PillageSimulation";
85 changes: 33 additions & 52 deletions client/src/ui/components/worldmap/battles/BattleSimulationPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { configManager } from "@/dojo/setup";
import { useDojo } from "@/hooks/context/DojoContext";
import { NumberInput } from "@/ui/elements/NumberInput";
import { ResourceIcon } from "@/ui/elements/ResourceIcon";
import { currencyFormat, formatTime } from "@/ui/utils/utils";
import { formatTime } from "@/ui/utils/utils";
import {
Battle,
ResourcesIds,
Expand All @@ -13,6 +11,7 @@ import {
import { getComponentValue } from "@dojoengine/recs";
import { getEntityIdFromKeys } from "@dojoengine/utils";
import { useMemo, useState } from "react";
import { Troops } from "./Troops";

export const BattleSimulationPanel = () => {
const {
Expand Down Expand Up @@ -57,8 +56,8 @@ export const BattleSimulationPanel = () => {
return new Battle(
aymericdelab marked this conversation as resolved.
Show resolved Hide resolved
attacker,
defender,
{ current: attacker.fullHealth(troopConfigSimulation), lifetime: attacker.fullHealth(troopConfigSimulation) },
{ current: defender.fullHealth(troopConfigSimulation), lifetime: defender.fullHealth(troopConfigSimulation) },
attacker.fullHealth(troopConfigSimulation),
defender.fullHealth(troopConfigSimulation),
aymericdelab marked this conversation as resolved.
Show resolved Hide resolved
troopConfigSimulation,
);
}, [attackingTroopsNumber, defendingTroopsNumber, troopConfig]);
Expand All @@ -82,59 +81,41 @@ export const BattleSimulationPanel = () => {
}, [battle]);

return (
<div className="w-full mb-2">
<div className="p-2 flex flex-row justify-around gap-4 mx-auto">
<Troops troops={attackingTroopsNumber} setTroops={setAttackingTroopsNumber} />
<Troops troops={defendingTroopsNumber} setTroops={setDefendingTroopsNumber} />
</div>
<div className="h1 text-xl mx-auto text-center">Battle results</div>
<div className="p-2 flex flex-row justify-around gap-4 mx-auto">
{remainingTroops && <Troops troops={remainingTroops.attackerRemainingTroops} />}
{remainingTroops && <Troops troops={remainingTroops.defenderRemainingTroops} />}
<div className="w-full mb-4 p-6 rounded-lg shadow-lg">
<div className="grid grid-cols-2 gap-8">
<div className="text-center">
<h2 className="text-xl font-bold mb-4">Attackers</h2>
<Troops troops={attackingTroopsNumber} setTroops={setAttackingTroopsNumber} />
</div>
<div className="text-center">
<h2 className="text-xl font-bold mb-4">Defenders</h2>
<Troops troops={defendingTroopsNumber} setTroops={setDefendingTroopsNumber} />
</div>
</div>
{battle && <div className="text-center text-lg">⏳ {formatTime(battle?.calculateDuration() ?? 0)}</div>}
</div>
);
};

const Troops = ({
troops,
setTroops,
}: {
troops: Partial<Record<ResourcesIds, bigint>>;
setTroops?: React.Dispatch<React.SetStateAction<Partial<Record<ResourcesIds, bigint>>>>;
}) => {
return (
<div className={`grid grid-${setTroops ? "rows" : "cols"}-3`}>
{Object.entries(troops).map(([resource, count]) => (
<div className={`p-2 bg-gold/10 hover:bg-gold/30 `} key={resource}>
<div className="font-bold mb-4">
<div className="flex justify-between text-center">
<div className="text-md">
{(ResourcesIds[resource as keyof typeof ResourcesIds] as unknown as string).length > 7
? (ResourcesIds[resource as keyof typeof ResourcesIds] as unknown as string).slice(0, 7) + "..."
: ResourcesIds[resource as keyof typeof ResourcesIds]}
</div>
{remainingTroops && (
<div className="mt-8">
<h2 className="text-xl font-bold text-center mb-4">Battle Results</h2>

<div className="text-center mb-8">
<div className="bg-black rounded-lg p-3">
<div className="text-sm">Battle Duration</div>
<div className="text-xl font-bold">⏳ {formatTime(battle?.calculateDuration() ?? 0)}</div>
</div>
</div>

<div className="grid grid-cols-2 gap-8">
<div className="text-center">
<h3 className="text-lg font-bold mb-3">Attackers Remaining</h3>
<Troops troops={remainingTroops.attackerRemainingTroops} />
</div>
<div className="py-1 flex flex-row justify-between">
<ResourceIcon
withTooltip={false}
resource={ResourcesIds[resource as keyof typeof ResourcesIds] as unknown as string}
size="lg"
/>
{!setTroops && <div className="text-lg w-full">{currencyFormat(Number(count), 0)}</div>}
{setTroops && (
<NumberInput
min={0}
step={100}
value={Number(count)}
onChange={(amount) => setTroops({ ...troops, [resource]: BigInt(amount) })}
/>
)}
<div className="text-center">
<h3 className="text-lg font-bold mb-3">Defenders Remaining</h3>
<Troops troops={remainingTroops.defenderRemainingTroops} />
</div>
</div>
</div>
))}
)}
</div>
);
};
195 changes: 195 additions & 0 deletions client/src/ui/components/worldmap/battles/PillageSimulationPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { configManager } from "@/dojo/setup";
import { useDojo } from "@/hooks/context/DojoContext";
import {
getChancesOfSuccess,
getMaxResourceAmountStolen,
getTroopLossOnRaidPerTroopType,
roundDownToPrecision,
roundUpToPrecision,
} from "@/ui/modules/military/battle-view/utils";
import { currencyFormat } from "@/ui/utils/utils";
import {
ResourcesIds,
TroopConfig as TroopConfigClass,
TroopsSimulator,
WORLD_CONFIG_ID,
} from "@bibliothecadao/eternum";
import { getComponentValue } from "@dojoengine/recs";
import { getEntityIdFromKeys } from "@dojoengine/utils";
import { useMemo, useState } from "react";
import { Troops } from "./Troops";

export const PillageSimulationPanel = () => {
const {
setup: {
components: { TroopConfig },
},
} = useDojo();

const defaultTroops = {
[ResourcesIds.Crossbowman]: 0n,
[ResourcesIds.Knight]: 0n,
[ResourcesIds.Paladin]: 0n,
};
const [attackingTroopsNumber, setAttackingTroopsNumber] =
useState<Partial<Record<ResourcesIds, bigint>>>(defaultTroops);
const [defendingTroopsNumber, setDefendingTroopsNumber] =
useState<Partial<Record<ResourcesIds, bigint>>>(defaultTroops);

const troopConfig = useMemo(() => getComponentValue(TroopConfig, getEntityIdFromKeys([WORLD_CONFIG_ID])), []);

aymericdelab marked this conversation as resolved.
Show resolved Hide resolved
const simulationResults = useMemo(() => {
if (!troopConfig) return;

const troopConfigSimulation = new TroopConfigClass(
troopConfig.health,
troopConfig.knight_strength,
troopConfig.paladin_strength,
troopConfig.crossbowman_strength,
troopConfig.advantage_percent,
troopConfig.disadvantage_percent,
troopConfig.battle_time_scale,
troopConfig.battle_max_time_seconds,
);

const attacker = new TroopsSimulator(
(attackingTroopsNumber[ResourcesIds.Knight] ?? 0n) * BigInt(configManager.getResourcePrecision()),
(attackingTroopsNumber[ResourcesIds.Paladin] ?? 0n) * BigInt(configManager.getResourcePrecision()),
(attackingTroopsNumber[ResourcesIds.Crossbowman] ?? 0n) * BigInt(configManager.getResourcePrecision()),
);

const defender = new TroopsSimulator(
(defendingTroopsNumber[ResourcesIds.Knight] ?? 0n) * BigInt(configManager.getResourcePrecision()),
(defendingTroopsNumber[ResourcesIds.Paladin] ?? 0n) * BigInt(configManager.getResourcePrecision()),
(defendingTroopsNumber[ResourcesIds.Crossbowman] ?? 0n) * BigInt(configManager.getResourcePrecision()),
);

if (attacker.count() === 0n || defender.count() === 0n) {
return;
}

const attackerArmyInfo = {
troops: attacker.troops(),
health: attacker.fullHealth(troopConfigSimulation),
};

const defenderArmyInfo = {
troops: defender.troops(),
health: defender.fullHealth(troopConfigSimulation),
};

const raidSuccessPercentage = getChancesOfSuccess(attackerArmyInfo, defenderArmyInfo, troopConfig) * 100;
const maxResourceAmountStolen = getMaxResourceAmountStolen(attackerArmyInfo, defenderArmyInfo, troopConfig);
const {
attackerKnightLost,
attackerPaladinLost,
attackerCrossbowmanLost,
defenderKnightLost,
defenderPaladinLost,
defenderCrossbowmanLost,
} = getTroopLossOnRaidPerTroopType(attackerArmyInfo, defenderArmyInfo, troopConfig);

const attackerRemainingTroops = {
aymericdelab marked this conversation as resolved.
Show resolved Hide resolved
[ResourcesIds.Crossbowman]: roundDownToPrecision(
attackerArmyInfo.troops.crossbowman_count - BigInt(attackerCrossbowmanLost),
configManager.getResourcePrecision(),
),
[ResourcesIds.Knight]: roundDownToPrecision(
attackerArmyInfo.troops.knight_count - BigInt(attackerKnightLost),
configManager.getResourcePrecision(),
),
[ResourcesIds.Paladin]: roundDownToPrecision(
attackerArmyInfo.troops.paladin_count - BigInt(attackerPaladinLost),
configManager.getResourcePrecision(),
),
};
const defenderRemainingTroops = {
[ResourcesIds.Crossbowman]: roundDownToPrecision(
defenderArmyInfo.troops.crossbowman_count - BigInt(defenderCrossbowmanLost),
configManager.getResourcePrecision(),
),
[ResourcesIds.Knight]: roundDownToPrecision(
defenderArmyInfo.troops.knight_count - BigInt(defenderKnightLost),
configManager.getResourcePrecision(),
),
[ResourcesIds.Paladin]: roundDownToPrecision(
defenderArmyInfo.troops.paladin_count - BigInt(defenderPaladinLost),
configManager.getResourcePrecision(),
),
};

const attackerTroopsLoss =
roundUpToPrecision(BigInt(attackerKnightLost), configManager.getResourcePrecision()) +
roundUpToPrecision(BigInt(attackerPaladinLost), configManager.getResourcePrecision()) +
roundUpToPrecision(BigInt(attackerCrossbowmanLost), configManager.getResourcePrecision());
const defenseTroopsLoss =
roundUpToPrecision(BigInt(defenderKnightLost), configManager.getResourcePrecision()) +
roundUpToPrecision(BigInt(defenderPaladinLost), configManager.getResourcePrecision()) +
roundUpToPrecision(BigInt(defenderCrossbowmanLost), configManager.getResourcePrecision());
return {
attackerRemainingTroops,
defenderRemainingTroops,
raidSuccessPercentage,
maxResourceAmountStolen,
attackerTroopsLoss,
defenseTroopsLoss,
};
}, [attackingTroopsNumber, defendingTroopsNumber, troopConfig]);
aymericdelab marked this conversation as resolved.
Show resolved Hide resolved

return (
<div className="w-full mb-4 p-6 rounded-lg shadow-lg">
<div className="grid grid-cols-2 gap-8">
<div className="text-center">
<h2 className="text-xl font-bold mb-4">Raiders</h2>
<Troops troops={attackingTroopsNumber} setTroops={setAttackingTroopsNumber} />
</div>
<div className="text-center">
<h2 className="text-xl font-bold mb-4">Defenders</h2>
<Troops troops={defendingTroopsNumber} setTroops={setDefendingTroopsNumber} />
</div>
</div>

{simulationResults ? (
<div className="mt-8">
<h2 className="text-xl font-bold text-center mb-4">Battle Results</h2>

<div className="grid grid-cols-2 gap-4 text-center mb-8">
<div className="bg-black rounded-lg p-3">
<div className="text-sm">Success Chance</div>
<div className="text-xl font-bold">{simulationResults.raidSuccessPercentage.toFixed(2)}%</div>
</div>
<div className="bg-black rounded-lg p-3">
<div className="text-sm">Resources Stolen</div>
<div className="text-xl font-bold">{simulationResults.maxResourceAmountStolen}</div>
</div>
<div className="bg-black rounded-lg p-3">
<div className="text-sm">Raiders Lost</div>
<div className="text-xl font-bold">{currencyFormat(Number(simulationResults.attackerTroopsLoss), 0)}</div>
aymericdelab marked this conversation as resolved.
Show resolved Hide resolved
</div>
<div className="bg-black rounded-lg p-3">
<div className="text-sm">Defenders Lost</div>
<div className="text-xl font-bold">{currencyFormat(Number(simulationResults.defenseTroopsLoss), 0)}</div>
</div>
</div>

<div className="grid grid-cols-2 gap-8">
{simulationResults.attackerRemainingTroops && (
<div className="text-center">
<h3 className="text-lg font-bold mb-3">Raiders Remaining</h3>
<Troops troops={simulationResults.attackerRemainingTroops} />
</div>
)}
{simulationResults.defenderRemainingTroops && (
<div className="text-center">
<h3 className="text-lg font-bold mb-3">Target Remaining</h3>
<Troops troops={simulationResults.defenderRemainingTroops} />
</div>
)}
</div>
</div>
) : (
<div className="text-center mt-8">Please select troops to simulate battle</div>
)}
</div>
);
};
Loading
Loading