Skip to content

Commit

Permalink
ref: combat (#1120)
Browse files Browse the repository at this point in the history
* Heavy refactor + addition of a testing framework

* Add CI

* scarb fmt
  • Loading branch information
edisontim authored Jul 18, 2024
1 parent 8592a45 commit 6773351
Show file tree
Hide file tree
Showing 102 changed files with 2,221 additions and 2,041 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/test-client.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: client

on:
push:
branches:
- main
pull_request: {}

jobs:
test-client:
runs-on: ubuntu-latest
env:
VITE_PUBLIC_MASTER_ADDRESS: "0x0"
VITE_PUBLIC_MASTER_PRIVATE_KEY: "0x0"
VITE_PUBLIC_ACCOUNT_CLASS_HASH: "0x0"

strategy:
matrix:
node-version: [21.x]

steps:
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- uses: pnpm/action-setup@v4
with:
version: 9.2.0

- uses: actions/checkout@v4
- name: Install dependencies
run: pnpm i
- name: Build packages
run: pnpm run build-packages
- name: Execute Unit tests
run: cd client && pnpm run test
File renamed without changes.
22 changes: 16 additions & 6 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
"dev": "vite --host 0.0.0.0",
"build": "tsc && vite build",
"preview": "vite preview",
"components": "npx @dojoengine/core ../contracts/manifests/dev/manifest.json src/dojo/contractComponents.ts http://localhost:5050 0x177a3f3d912cf4b55f0f74eccf3b7def7c6144efeba033e9f21d9cdb0230c64"
"components": "npx @dojoengine/core ../contracts/manifests/dev/manifest.json src/dojo/contractComponents.ts http://localhost:5050 0x161b08e252b353008665e85ab5dcb0044a61186eb14b999657d14c04c94c824",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage"
},
"peerDependencies": {
"starknet": "6.7.0"
Expand All @@ -30,6 +34,7 @@
"@react-three/fiber": "^8.16.1",
"@react-three/postprocessing": "2.16.2",
"@reactour/tour": "^3.6.1",
"@testing-library/react-hooks": "^8.0.1",
"@vercel/analytics": "^1.2.2",
"@web3mq/client": "^1.0.25",
"buffer": "^6.0.3",
Expand All @@ -44,6 +49,7 @@
"lodash": "^4.17.21",
"lucide-react": "^0.365.0",
"postprocessing": "^6.35.2",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-blurhash": "^0.3.0",
"react-dom": "^18.2.0",
Expand All @@ -63,30 +69,34 @@
},
"devDependencies": {
"@svgr/rollup": "^8.1.0",
"vite-plugin-top-level-await": "^1.4.1",
"vite-plugin-wasm": "^3.3.0",
"@tailwindcss/typography": "^0.5.13",
"@testing-library/react": "^16.0.0",
"@types/lodash": "^4.14.202",
"@types/node": "^20.11.10",
"@types/react": "^18.2.74",
"@types/react-dom": "^18.2.21",
"@types/three": "^0.163.0",
"@types/node": "^20.11.10",
"@types/react-resizable": "^3.0.7",
"@types/three": "^0.163.0",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/ui": "^2.0.1",
"autoprefixer": "^10.4.18",
"eslint": "^8.57.0",
"eslint-config-standard-with-typescript": "^43.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^17.0.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.34.1",
"jsdom": "^24.1.0",
"leva": "^0.9.35",
"postcss": "^8.4.35",
"r3f-perf": "^7.2.1",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.4",
"vite": "^5.2.8",
"vite-plugin-svgr": "^4.2.0"
"vite-plugin-svgr": "^4.2.0",
"vite-plugin-top-level-await": "^1.4.1",
"vite-plugin-wasm": "^3.3.0",
"vitest": "^2.0.1"
}
}
1 change: 0 additions & 1 deletion client/src/dojo/createClientComponents.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { overridableComponent } from "@dojoengine/recs";
import { SetupNetworkResult } from "./setupNetwork";
import { Position } from "@/ui/elements/BaseThreeTooltip";

export type ClientComponents = ReturnType<typeof createClientComponents>;

Expand Down
167 changes: 122 additions & 45 deletions client/src/dojo/modelManager/BattleManager.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,25 @@
import { Component, ComponentValue, OverridableComponent, Type, getComponentValue } from "@dojoengine/recs";
import { ArmyInfo } from "@/hooks/helpers/useArmies";
import { BattleSide, EternumGlobalConfig, Troops } from "@bibliothecadao/eternum";
import { Component, ComponentValue, Components, getComponentValue } from "@dojoengine/recs";
import { getEntityIdFromKeys } from "@dojoengine/utils";
import { BattleType } from "./types";
import { ClientComponents } from "../createClientComponents";

export class BattleManager {
battleModel: Component<BattleType> | OverridableComponent<BattleType>;
battleId: bigint;
battleModel: Component<ClientComponents["Battle"]["schema"]>;

constructor(battleModel: Component<BattleType> | OverridableComponent<BattleType>, battleId: bigint) {
this.battleModel = battleModel;
constructor(battleId: bigint, battleModel: Component<ClientComponents["Battle"]["schema"]>) {
this.battleId = battleId;
this.battleModel = battleModel;
}

public getUpdatedBattle(currentTick: number) {
public getUpdatedBattle(currentTimestamp: number) {
const battle = this.getBattle();
if (!battle) return;

const battleClone = structuredClone(battle);

const durationPassed: number = this.getElapsedTime(currentTick);

const attackDelta = this.attackingDelta();
const defenceDelta = this.defendingDelta();

const damagesDoneToAttack = this.damagesDone(defenceDelta, durationPassed);
const damagesDoneToDefence = this.damagesDone(attackDelta, durationPassed);

battleClone.attack_army_health.current =
damagesDoneToAttack > BigInt(battleClone.attack_army_health.current)
? 0n
: BigInt(battleClone.attack_army_health.current) - damagesDoneToAttack;

battleClone.defence_army_health.current =
damagesDoneToDefence > BigInt(battleClone.defence_army_health.current)
? 0n
: BigInt(battleClone.defence_army_health.current) - damagesDoneToDefence;
this.updateHealth(battleClone, currentTimestamp);

battleClone.defence_army.troops = this.getUpdatedTroops(
battleClone.defence_army_health,
Expand All @@ -47,10 +33,10 @@ export class BattleManager {
return battleClone;
}

public getElapsedTime(currentTick: number): number {
public getElapsedTime(currentTimestamp: number): number {
const battle = this.getBattle();
if (!battle) return 0;
const duractionSinceLastUpdate = currentTick - Number(battle.last_updated);
const duractionSinceLastUpdate = currentTimestamp - Number(battle.last_updated);
if (Number(battle.duration_left) >= duractionSinceLastUpdate) {
return duractionSinceLastUpdate;
} else {
Expand All @@ -74,51 +60,142 @@ export class BattleManager {
}
}

public isBattleActive(currentTick: number): boolean {
public isBattleActive(currentTimestamp: number): boolean {
const battle = this.getBattle();
const timeSinceLastUpdate = this.getElapsedTime(currentTick);
const timeSinceLastUpdate = this.getElapsedTime(currentTimestamp);
return battle ? timeSinceLastUpdate < battle.duration_left : false;
}

public getBattle() {
private getBattle(): ComponentValue<ClientComponents["Battle"]["schema"]> | undefined {
return getComponentValue(this.battleModel, getEntityIdFromKeys([this.battleId]));
}

public getUpdatedArmy(army: ArmyInfo, battle?: ComponentValue<ClientComponents["Battle"]["schema"]>) {
if (BigInt(army.battle_id) !== this.battleId) {
throw new Error("Army is not in the battle");
}
if (!battle) return army;

const cloneArmy = structuredClone(army);

let battle_army, battle_army_lifetime;
if (String(army.battle_side) === BattleSide[BattleSide.Defence]) {
battle_army = battle.defence_army;
battle_army_lifetime = battle.defence_army_lifetime;
} else {
battle_army = battle.attack_army;
battle_army_lifetime = battle.attack_army_lifetime;
}

cloneArmy.health.current = this.getTroopFullHealth(battle_army.troops);

cloneArmy.troops.knight_count =
cloneArmy.troops.knight_count === 0n
? 0n
: BigInt(
Math.floor(
Number(
(cloneArmy.troops.knight_count * battle_army.troops.knight_count) /
battle_army_lifetime.troops.knight_count,
),
),
);

cloneArmy.troops.paladin_count =
cloneArmy.troops.paladin_count === 0n
? 0n
: BigInt(
Math.floor(
Number(
(cloneArmy.troops.paladin_count * battle_army.troops.paladin_count) /
battle_army_lifetime.troops.paladin_count,
),
),
);

cloneArmy.troops.crossbowman_count =
cloneArmy.troops.crossbowman_count === 0n
? 0n
: BigInt(
Math.floor(
Number(
(cloneArmy.troops.crossbowman_count * battle_army.troops.crossbowman_count) /
battle_army_lifetime.troops.crossbowman_count,
),
),
);
return cloneArmy;
}

private getTroopFullHealth(troops: Troops): bigint {
const health = EternumGlobalConfig.troop.health;
let total_knight_health = health * Number(troops.knight_count);
let total_paladin_health = health * Number(troops.paladin_count);
let total_crossbowman_health = health * Number(troops.crossbowman_count);
return BigInt(
Math.floor(
(total_knight_health + total_paladin_health + total_crossbowman_health) /
(EternumGlobalConfig.resources.resourceMultiplier * Number(EternumGlobalConfig.troop.healthPrecision)),
),
);
}

private getUpdatedTroops = (
health: { current: bigint; lifetime: bigint },
currentTroops: ComponentValue<
{ knight_count: Type.BigInt; paladin_count: Type.BigInt; crossbowman_count: Type.BigInt },
unknown
>,
): ComponentValue<
{ knight_count: Type.BigInt; paladin_count: Type.BigInt; crossbowman_count: Type.BigInt },
unknown
> => {
currentTroops: { knight_count: bigint; paladin_count: bigint; crossbowman_count: bigint },
): { knight_count: bigint; paladin_count: bigint; crossbowman_count: bigint } => {
if (health.lifetime === 0n) {
return {
knight_count: 0n,
paladin_count: 0n,
crossbowman_count: 0n,
};
}

return {
knight_count: (BigInt(health.current) * BigInt(currentTroops.knight_count)) / BigInt(health.lifetime),
paladin_count: (BigInt(health.current) * BigInt(currentTroops.paladin_count)) / BigInt(health.lifetime),
crossbowman_count: (BigInt(health.current) * BigInt(currentTroops.crossbowman_count)) / BigInt(health.lifetime),
knight_count: (health.current * currentTroops.knight_count) / health.lifetime,
paladin_count: (health.current * currentTroops.paladin_count) / health.lifetime,
crossbowman_count: (health.current * currentTroops.crossbowman_count) / health.lifetime,
};
};

private attackingDelta() {
const battle = this.getBattle();
return battle ? battle.attack_delta : 0n;
private updateHealth(battle: ComponentValue<Components["Battle"]["schema"]>, currentTimestamp: number) {
const durationPassed: number = this.getElapsedTime(currentTimestamp);

const attackDelta = this.attackingDelta(battle);
const defenceDelta = this.defendingDelta(battle);

battle.attack_army_health.current = this.getUdpdatedHealth(attackDelta, battle.attack_army_health, durationPassed);
battle.defence_army_health.current = this.getUdpdatedHealth(
defenceDelta,
battle.defence_army_health,
durationPassed,
);
}

private defendingDelta() {
const battle = this.getBattle();
return battle ? battle.defence_delta : 0n;
private getUdpdatedHealth(
delta: bigint,
health: ComponentValue<Components["Health"]["schema"]>,
durationPassed: number,
) {
const damagesDone = this.damagesDone(delta, durationPassed);
const currentHealthAfterDamage = this.getCurrentHealthAfterDamage(health, damagesDone);
return currentHealthAfterDamage;
}

private attackingDelta(battle: ComponentValue<Components["Battle"]["schema"]>) {
return battle.attack_delta;
}

private defendingDelta(battle: ComponentValue<Components["Battle"]["schema"]>) {
return battle.defence_delta;
}

private damagesDone(delta: bigint, durationPassed: number): bigint {
return delta * BigInt(durationPassed);
}

private getCurrentHealthAfterDamage(health: ComponentValue<Components["Health"]["schema"]>, damages: bigint) {
return damages > health.current ? 0n : health.current - damages;
}
}
Loading

0 comments on commit 6773351

Please sign in to comment.