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

feat: Add user battles tab #2102

Open
wants to merge 7 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
32 changes: 32 additions & 0 deletions client/src/hooks/helpers/battles/useBattles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import {
Entity,
Has,
HasValue,
NotValue,
getComponentValue,
runQuery,
} from "@dojoengine/recs";
import { getEntityIdFromKeys } from "@dojoengine/utils";
import { useMemo } from "react";
import { useDojo } from "../../context/DojoContext";
import { useEntities } from "../useEntities";

export type BattleInfo = ComponentValue<ClientComponents["Battle"]["schema"]> & {
isStructureBattle: boolean;
Expand Down Expand Up @@ -75,3 +77,33 @@ export const useBattlesByPosition = ({ x, y }: Position) => {
const battleEntityIds = useEntityQuery([Has(Battle), HasValue(Position, { x, y })]);
return getExtraBattleInformation(battleEntityIds, Battle, Position, Structure);
};

export const useUserBattles = () => {
const {
setup: {
components: { Army, Battle, EntityOwner, Position, Structure },
},
} = useDojo();

const { playerRealms } = useEntities();
const realms = playerRealms();

const battleEntityIds = realms
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be memod?

Copy link
Collaborator

Choose a reason for hiding this comment

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

id say so as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

my bad, added that on my commit

.map((realm) => {
const userArmiesInBattleEntityIds = runQuery([
Has(Army),
NotValue(Army, { battle_id: 0 }),
HasValue(EntityOwner, { entity_owner_id: realm.entity_id }),
]);
const battleEntityIds = Array.from(userArmiesInBattleEntityIds)
.map((armyEntityId) => {
const army = getComponentValue(Army, armyEntityId);
if (!army) return;
return getEntityIdFromKeys([BigInt(army.battle_id)]);
})
.filter((battleEntityId): battleEntityId is Entity => Boolean(battleEntityId));
return battleEntityIds;
})
.flatMap((battleEntityIds) => Array.from(battleEntityIds));
return getExtraBattleInformation(battleEntityIds, Battle, Position, Structure);
};
edisontim marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 2 additions & 1 deletion client/src/hooks/helpers/useEntities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ export const useEntities = () => {

const getPlayerStructures = (filterFn?: (structure: PlayerStructure) => boolean) => {
return useMemo(() => {
return filterFn ? playerStructures.filter(filterFn) : playerStructures;
const structures = filterFn ? playerStructures.filter(filterFn) : playerStructures;
edisontim marked this conversation as resolved.
Show resolved Hide resolved
return structures.sort((a, b) => a.name.localeCompare(b.name));
}, [otherRealms, filterFn]);
edisontim marked this conversation as resolved.
Show resolved Hide resolved
};

Expand Down
40 changes: 8 additions & 32 deletions client/src/ui/components/battles/BattleListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@ import { ReactComponent as Eye } from "@/assets/icons/common/eye.svg";
import { BattleManager } from "@/dojo/modelManager/BattleManager";
import { useDojo } from "@/hooks/context/DojoContext";
import { BattleInfo } from "@/hooks/helpers/battles/useBattles";
import { ArmyInfo, getUserArmyInBattle } from "@/hooks/helpers/useArmies";
import { ArmyInfo } from "@/hooks/helpers/useArmies";
import { useEntitiesUtils } from "@/hooks/helpers/useEntities";
import useUIStore from "@/hooks/store/useUIStore";
import { getComponentValue, HasValue, runQuery } from "@dojoengine/recs";
import React, { useMemo, useState } from "react";
import { ViewOnMapIcon } from "../military/ArmyManagementCard";
import { TroopMenuRow } from "../military/TroopChip";
import { InventoryResources } from "../resources/InventoryResources";
import { StructureMergeTroopsPanel } from "../structures/worldmap/StructureCard";

type BattleListItemProps = {
battle: BattleInfo;
ownArmySelected: ArmyInfo | undefined;
showCompass?: boolean;
};

export const BattleListItem = ({ battle, ownArmySelected }: BattleListItemProps) => {
export const BattleListItem = ({ battle, ownArmySelected, showCompass = false }: BattleListItemProps) => {
const dojo = useDojo();

const [showMergeTroopsPopup, setShowMergeTroopsPopup] = useState(false);
const { getAddressNameFromEntity } = useEntitiesUtils();

const nextBlockTimestamp = useUIStore((state) => state.nextBlockTimestamp);
Expand All @@ -31,8 +31,6 @@ export const BattleListItem = ({ battle, ownArmySelected }: BattleListItemProps)
const setBattleView = useUIStore((state) => state.setBattleView);
const setTooltip = useUIStore((state) => state.setTooltip);

const userArmyInBattle = getUserArmyInBattle(battle.entity_id);

const battleManager = useMemo(() => new BattleManager(battle.entity_id, dojo), [battle]);

const updatedBattle = useMemo(() => {
Expand Down Expand Up @@ -80,15 +78,7 @@ export const BattleListItem = ({ battle, ownArmySelected }: BattleListItemProps)
/>
);

if (userArmyInBattle) {
if (isBattleOngoing && ownArmySelected) {
// check battle and join
return [swordButton];
} else {
// check battle to claim or leave (if battle is finished) or just check
return [eyeButton];
}
} else if (ownArmySelected && isBattleOngoing) {
if (ownArmySelected && isBattleOngoing) {
// join battle
return [swordButton];
} else {
Expand All @@ -101,16 +91,13 @@ export const BattleListItem = ({ battle, ownArmySelected }: BattleListItemProps)
!battleManager.isEmpty() && (
edisontim marked this conversation as resolved.
Show resolved Hide resolved
<React.Fragment>
<div className="flex flex-row justify-between mt-2">
<div
className={`flex flex-col w-[27rem] h-full justify-between bg-red/20 ${
userArmyInBattle ? "animate-pulse" : ""
} rounded-md border-gold/20 p-2`}
>
<div className={`flex flex-col w-[27rem] h-full justify-between bg-red/20 rounded-md border-gold/20 p-2`}>
<div className="flex w-full justify-between">
<div className="flex flex-col w-[40%]">
<TroopMenuRow troops={updatedBattle?.attack_army?.troops} />
</div>
<div className="flex flex-col font-bold m-auto relative top-2">
<div className="flex flex-col font-bold m-auto relative">
{showCompass && <ViewOnMapIcon hideTooltip={true} position={battle?.position} />}
<div
className="font-bold m-auto animate-pulse"
onMouseEnter={() =>
Expand Down Expand Up @@ -145,17 +132,6 @@ export const BattleListItem = ({ battle, ownArmySelected }: BattleListItemProps)
</div>
{buttons}
</div>
{showMergeTroopsPopup && (
<div className="flex flex-col w-[100%]">
{ownArmySelected && (
<StructureMergeTroopsPanel
giverArmy={ownArmySelected}
takerArmy={userArmyInBattle}
setShowMergeTroopsPopup={setShowMergeTroopsPopup}
/>
)}
</div>
)}
</React.Fragment>
)
);
Expand Down
19 changes: 19 additions & 0 deletions client/src/ui/components/military/UserBattles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useUserBattles } from "@/hooks/helpers/battles/useBattles";
import { BattleListItem } from "../battles/BattleListItem";

export const UserBattles = () => {
const battles = useUserBattles();

edisontim marked this conversation as resolved.
Show resolved Hide resolved
return (
<div className="w-[31rem] py-2 pl-2">
{battles.length > 0 && (
<>
<h5>Your battles</h5>
{battles.map((battle) => (
<BattleListItem key={battle.entity_id} battle={battle} ownArmySelected={undefined} showCompass />
))}
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 13, 2024

Choose a reason for hiding this comment

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

💡 Codebase verification

Sorting battles is recommended for consistent display

Looking at the implementation of useUserBattles hook, there's currently no sorting logic implemented. The battles are returned in the order they are retrieved from the query, which may lead to inconsistent ordering across renders.

  • Add sorting logic in UserBattles.tsx before mapping through the battles
  • Consider sorting by battle start time, battle ID, or position coordinates for consistency
🔗 Analysis chain

Consider sorting the battles list.

According to Issue #2085, there's a requirement for consistent sorting. Consider sorting the battles list before rendering.

Let's verify if the useUserBattles hook already handles sorting:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if useUserBattles hook implements sorting

# Search for sorting logic in the hook implementation
ast-grep --pattern 'useUserBattles = () => {
  $$$
  sort($$$)
  $$$
}'

# Alternatively, look for any sorting-related code
rg -A 5 'sort\(' "$(fd -t f useBattles.tsx)"

Length of output: 166


Script:

#!/bin/bash
# Let's try a broader search for the hook implementation and any sorting logic

# Find the hook implementation
rg -l "useUserBattles" --type typescript

# Once we find the file, let's examine its content
rg -A 10 "useUserBattles" --type typescript

# Also search for any battle-related sorting in the codebase
rg -A 5 "battles?.sort" --type typescript
rg -A 5 "sortBattles" --type typescript

Length of output: 308


Script:

#!/bin/bash
# Let's try without specifying the file type

# Find the hook implementation
rg -l "useUserBattles"

# Once we find the file, let's examine its content
rg -A 10 "useUserBattles"

# Also search for any battle-related sorting in the codebase
rg -A 5 "battles?.sort"
rg -A 5 "sortBattles"

# Look for the actual file containing UserBattles component
fd UserBattles

Length of output: 2309


Script:

#!/bin/bash
# Let's examine the complete implementation of useBattles hook to understand how battles are processed

# Get the full content of useBattles.tsx
cat client/src/hooks/helpers/battles/useBattles.tsx

# Also look for any timestamp or date fields in Battle type/interface
rg -A 5 "type Battle" 
rg -A 5 "interface Battle"

Length of output: 10478

Copy link
Collaborator

Choose a reason for hiding this comment

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

could you sort battles so that they always render in same order @edisontim ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

</>
)}
edisontim marked this conversation as resolved.
Show resolved Hide resolved
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ type MergeTroopsPanelProps = {
structureEntityId?: ID;
};

export const StructureMergeTroopsPanel = ({
const StructureMergeTroopsPanel = ({
giverArmy,
setShowMergeTroopsPopup,
structureEntityId,
Expand Down
26 changes: 18 additions & 8 deletions client/src/ui/elements/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import clsx from "clsx";
import { ReactComponent as Checkmark } from "@/assets/icons/common/checkmark.svg";
import clsx from "clsx";
type CheckboxProps = {
enabled: boolean;
onClick?: () => void;
text?: string;
edisontim marked this conversation as resolved.
Show resolved Hide resolved
};

export const Checkbox = ({ enabled }: CheckboxProps) => (
<div
className={clsx(
"w-3 h-3 flex items-center justify-center rounded-[3px] bg-dark-green-accent border transition-all duration-300 ease-in-out hover:border-white",
enabled ? "border-grey" : "border-gold",
export const Checkbox = ({ enabled, onClick, text }: CheckboxProps) => (
<div className="flex flex-row justify-center items-center text-center space-x-2">
<div
onClick={onClick}
className={clsx(
"w-3 h-3 flex items-center justify-center rounded-[3px] bg-dark-green-accent border transition-all duration-300 ease-in-out hover:border-white",
enabled ? "border-grey" : "border-gold",
)}
>
{enabled && <Checkmark className="fill-gold" />}
</div>
{text && (
<div onClick={onClick} className="text-sm text-gray-300 hover:text-white transition-colors duration-200">
{text}
</div>
)}
>
edisontim marked this conversation as resolved.
Show resolved Hide resolved
{enabled && <Checkmark className="fill-gold" />}
</div>
);
2 changes: 1 addition & 1 deletion client/src/ui/modules/entity-details/Battles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BattleListItem } from "@/ui/components/battles/BattleListItem";

export const Battles = ({ ownArmy, battles }: { ownArmy: ArmyInfo | undefined; battles: BattleInfo[] }) => {
return (
<div className="px-2 w-[31rem] py-2">
<div className="w-[31rem] py-2 pl-2">
{battles.length > 0 && (
<>
<h5>Battles</h5>
Expand Down
20 changes: 2 additions & 18 deletions client/src/ui/modules/entity-details/CombatEntityDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import { HintModalButton } from "@/ui/elements/HintModalButton";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui/elements/Select";
import { Tabs } from "@/ui/elements/tab";
import { ID } from "@bibliothecadao/eternum";
import { useEffect, useMemo, useState } from "react";
import { Battles } from "./Battles";
import { useMemo, useState } from "react";
import { Entities } from "./Entities";

export const CombatEntityDetails = () => {
Expand Down Expand Up @@ -53,16 +52,7 @@ export const CombatEntityDetails = () => {
<div>Entities</div>
</div>
),
component: selectedHex && <Entities position={hexPosition} ownArmy={ownArmy} />,
},
{
key: "battles",
label: (
<div className="flex relative group flex-col items-center">
<div>Battles</div>
</div>
),
component: <Battles ownArmy={ownArmy} battles={battles} />,
component: selectedHex && <Entities position={hexPosition} ownArmy={ownArmy} battles={battles} />,
},
...(structure
? [
Expand All @@ -83,12 +73,6 @@ export const CombatEntityDetails = () => {

const [selectedTab, setSelectedTab] = useState(0);

useEffect(() => {
if (battles.length > 0) {
setSelectedTab(1);
}
}, []);

return (
hexPosition && (
<div className="px-2 h-full">
Expand Down
13 changes: 8 additions & 5 deletions client/src/ui/modules/entity-details/EnemyArmies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,14 @@ export const EnemyArmies = ({
return (
<div className="flex flex-col mt-2 w-[31rem]">
{armies.length !== 0 && (
<React.Fragment>
<div className="grid grid-cols-1 gap-2 p-2">
{armies.length > 0 && armies.map((army: ArmyInfo, index) => getArmyChip(army, index)).filter(Boolean)}
</div>
</React.Fragment>
<>
<h5 className="pl-2 ">Enemy armies</h5>
<React.Fragment>
<div className="grid grid-cols-1 gap-2 p-2">
{armies.length > 0 && armies.map((army: ArmyInfo, index) => getArmyChip(army, index)).filter(Boolean)}
</div>
</React.Fragment>
</>
)}
</div>
);
Expand Down
32 changes: 27 additions & 5 deletions client/src/ui/modules/entity-details/Entities.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import { BattleInfo } from "@/hooks/helpers/battles/useBattles";
import { ArmyInfo, useEnemyArmiesByPosition } from "@/hooks/helpers/useArmies";
import { useEntities } from "@/hooks/helpers/useEntities";
import { Position } from "@/types/Position";
import { StructureCard } from "@/ui/components/structures/worldmap/StructureCard";
import { Checkbox } from "@/ui/elements/Checkbox";
import { useState } from "react";
import { Battles } from "./Battles";
import { EnemyArmies } from "./EnemyArmies";

export const Entities = ({ position, ownArmy }: { position: Position; ownArmy: ArmyInfo | undefined }) => {
export const Entities = ({
position,
ownArmy,
battles,
}: {
position: Position;
ownArmy: ArmyInfo | undefined;
battles: BattleInfo[];
}) => {
const [showStructure, setShowStructure] = useState(true);
const [showBattles, setShowBattles] = useState(true);
const [showArmies, setShowArmies] = useState(true);
const { playerStructures } = useEntities();

const enemyArmies = useEnemyArmiesByPosition({
Expand All @@ -13,10 +28,17 @@ export const Entities = ({ position, ownArmy }: { position: Position; ownArmy: A
});

return (
<div className="py-2">
<StructureCard position={position} ownArmySelected={ownArmy} />

{enemyArmies.length > 0 && <EnemyArmies armies={enemyArmies} ownArmySelected={ownArmy} position={position} />}
<div className="pb-2">
<div className="w-full grid grid-cols-3">
<Checkbox enabled={showStructure} onClick={() => setShowStructure((prev) => !prev)} text="Show structure" />
<Checkbox enabled={showBattles} onClick={() => setShowBattles((prev) => !prev)} text="Show battles" />
<Checkbox enabled={showArmies} onClick={() => setShowArmies((prev) => !prev)} text="Show armies" />
</div>
{showStructure && <StructureCard position={position} ownArmySelected={ownArmy} />}
{showBattles && <Battles ownArmy={ownArmy} battles={battles} />}
{showArmies && enemyArmies.length > 0 && (
<EnemyArmies armies={enemyArmies} ownArmySelected={ownArmy} position={position} />
)}
</div>
);
};
39 changes: 36 additions & 3 deletions client/src/ui/modules/military/Military.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,51 @@ import { useEntities } from "@/hooks/helpers/useEntities";
import { useQuery } from "@/hooks/helpers/useQuery";
import { EntityArmyList } from "@/ui/components/military/ArmyList";
import { EntitiesArmyTable } from "@/ui/components/military/EntitiesArmyTable";
import { UserBattles } from "@/ui/components/military/UserBattles";
import { Tabs } from "@/ui/elements/tab";
import { ID } from "@bibliothecadao/eternum";
import { useState } from "react";

export const Military = ({ entityId, className }: { entityId: ID | undefined; className?: string }) => {
const { isMapView } = useQuery();

const { playerStructures } = useEntities();

const selectedStructure = playerStructures().find((structure) => structure.entity_id === entityId);

const [selectedTab, setSelectedTab] = useState(0);

const tabs = [
edisontim marked this conversation as resolved.
Show resolved Hide resolved
{
label: "Army",
component: isMapView ? (
<EntitiesArmyTable />
) : (
selectedStructure && <EntityArmyList structure={selectedStructure} />
),
},
{ label: "Battles", component: <UserBattles /> },
];

return (
<div className={`relative ${className}`}>
{isMapView ? <EntitiesArmyTable /> : selectedStructure && <EntityArmyList structure={selectedStructure} />}
<Tabs
selectedIndex={selectedTab}
onChange={(index: any) => {
setSelectedTab(index);
}}
Comment on lines +33 to +35
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace 'any' type with a specific type for onChange handler.

The onChange handler uses 'any' type which reduces type safety. Consider using a more specific type.

-onChange={(index: any) => {
+onChange={(index: number) => {
   setSelectedTab(index);
 }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onChange={(index: any) => {
setSelectedTab(index);
}}
onChange={(index: number) => {
setSelectedTab(index);
}}

className="h-full"
>
<Tabs.List>
{tabs.map((tab, index) => (
<Tabs.Tab key={index}>{tab.label}</Tabs.Tab>
))}
</Tabs.List>

<Tabs.Panels className="overflow-hidden">
{tabs.map((tab, index) => (
<Tabs.Panel key={index}>{tab.component}</Tabs.Panel>
))}
</Tabs.Panels>
</Tabs>
</div>
);
};
Loading
Loading