Skip to content

Commit

Permalink
Hyperstructure contributions summary (#1592)
Browse files Browse the repository at this point in the history
* change fucntion name

* added contribution summary
  • Loading branch information
aymericdelab authored Sep 15, 2024
1 parent 0beffff commit 98b2fca
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 72 deletions.
2 changes: 1 addition & 1 deletion client/src/dojo/modelManager/LeaderboardManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class LeaderboardManager {
return Array.from(pointsPerPlayer).sort(([_A, playerA], [_B, playerB]) => playerB - playerA);
}

public getShares(playerAddress: ContractAddress, hyperstructureEntityId: ID) {
public getAddressShares(playerAddress: ContractAddress, hyperstructureEntityId: ID) {
const lastChangeEvent = this.eventsCoOwnersChange.findLast(
(event) => event.hyperstructureEntityId === hyperstructureEntityId,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,21 +229,23 @@ describe("processHyperstructureCoOwnersChangeEvent", () => {

describe("getShares", () => {
it("should return undefined if no change co owner event occured", () => {
expect(leaderboardManager.getShares(OWNER_1_ADDRESS, HYPERSTRUCTURE_ENTITY_ID)).toBeUndefined();
expect(leaderboardManager.getAddressShares(OWNER_1_ADDRESS, HYPERSTRUCTURE_ENTITY_ID)).toBeUndefined();
});

it("should return undefined if an event occured but not for the right entity id", () => {
const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID);
leaderboardManager.processHyperstructureCoOwnersChangeEvent(event);

expect(leaderboardManager.getShares(OWNER_1_ADDRESS, HYPERSTRUCTURE_ENTITY_ID + 1)).toBeUndefined();
expect(leaderboardManager.getAddressShares(OWNER_1_ADDRESS, HYPERSTRUCTURE_ENTITY_ID + 1)).toBeUndefined();
});

it("should return the correct amount of shares", () => {
const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID);
leaderboardManager.processHyperstructureCoOwnersChangeEvent(event);

expect(leaderboardManager.getShares(OWNER_1_ADDRESS, HYPERSTRUCTURE_ENTITY_ID)).toBe(OWNER_1_SHARES / 10_000);
expect(leaderboardManager.getAddressShares(OWNER_1_ADDRESS, HYPERSTRUCTURE_ENTITY_ID)).toBe(
OWNER_1_SHARES / 10_000,
);
});
});

Expand Down
18 changes: 13 additions & 5 deletions client/src/hooks/helpers/useContributions.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ContractAddress, ID } from "@bibliothecadao/eternum";
import { ClientComponents } from "@/dojo/createClientComponents";
import { getTotalPointsPercentage } from "@/dojo/modelManager/utils/LeaderboardUtils";
import { ContractAddress, ID, Resource } from "@bibliothecadao/eternum";
import { useEntityQuery } from "@dojoengine/react";
import { ComponentValue, HasValue, getComponentValue, runQuery } from "@dojoengine/recs";
import { useDojo } from "../context/DojoContext";
import { ClientComponents } from "@/dojo/createClientComponents";

export const useContributions = () => {
const {
Expand All @@ -16,10 +17,10 @@ export const useContributions = () => {
runQuery([HasValue(Contribution, { hyperstructure_entity_id: hyperstructureEntityId })]),
).map((id) => getComponentValue(Contribution, id));

return contributionsToHyperstructure;
return contributionsToHyperstructure as ComponentValue<ClientComponents["Contribution"]["schema"]>[];
};

const getContributionsByPlayerAddress = (playerAddress: ContractAddress, hyperstructureEntityId: ID) => {
const useContributionsByPlayerAddress = (playerAddress: ContractAddress, hyperstructureEntityId: ID) => {
const contributionsToHyperstructure = useEntityQuery([
HasValue(Contribution, { hyperstructure_entity_id: hyperstructureEntityId, player_address: playerAddress }),
])
Expand All @@ -29,8 +30,15 @@ export const useContributions = () => {
return contributionsToHyperstructure;
};

const getContributionsTotalPercentage = (contributions: Resource[]) => {
return contributions.reduce((acc, { resourceId, amount }) => {
return acc + getTotalPointsPercentage(resourceId, BigInt(amount));
}, 0);
};

return {
getContributions,
getContributionsByPlayerAddress,
useContributionsByPlayerAddress,
getContributionsTotalPercentage,
};
};
9 changes: 7 additions & 2 deletions client/src/hooks/helpers/useHyperstructures.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { ClientComponents } from "@/dojo/createClientComponents";
import { HyperstructureResourceMultipliers } from "@bibliothecadao/eternum";
import { TOTAL_CONTRIBUTABLE_AMOUNT } from "@/dojo/modelManager/utils/LeaderboardUtils";
import { EternumGlobalConfig, HYPERSTRUCTURE_TOTAL_COSTS_SCALED, ID, ResourcesIds } from "@bibliothecadao/eternum";
import {
EternumGlobalConfig,
HYPERSTRUCTURE_TOTAL_COSTS_SCALED,
HyperstructureResourceMultipliers,
ID,
ResourcesIds,
} from "@bibliothecadao/eternum";
import { useEntityQuery } from "@dojoengine/react";
import { Component, ComponentValue, Entity, Has, HasValue, getComponentValue, runQuery } from "@dojoengine/recs";
import { toInteger } from "lodash";
Expand Down
90 changes: 90 additions & 0 deletions client/src/ui/components/hyperstructures/ContributionSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useContributions } from "@/hooks/helpers/useContributions";
import { useRealm } from "@/hooks/helpers/useRealm";
import { ResourceIcon } from "@/ui/elements/ResourceIcon";
import { currencyIntlFormat, divideByPrecision } from "@/ui/utils/utils";
import { ContractAddress, ID, ResourcesIds } from "@bibliothecadao/eternum";
import { useState } from "react";

export const ContributionSummary = ({
hyperstructureEntityId,
className,
}: {
hyperstructureEntityId: ID;
className?: string;
}) => {
const { getContributions, getContributionsTotalPercentage } = useContributions();
const { getAddressName } = useRealm();

type Resource = {
amount: number;
resourceId: number;
};

const contributions = getContributions(hyperstructureEntityId);
const groupedContributions = contributions.reduce<Record<string, Record<number, bigint>>>((acc, contribution) => {
const { player_address, resource_type, amount } = contribution;
const playerAddressString = player_address.toString();
if (!acc[playerAddressString]) {
acc[playerAddressString] = {};
}
if (!acc[playerAddressString][resource_type]) {
acc[playerAddressString][resource_type] = 0n;
}
acc[playerAddressString][resource_type] += amount;
return acc;
}, {});

const resourceContributions: Record<string, Resource[]> = Object.entries(groupedContributions).reduce(
(acc, [playerAddress, resources]) => {
acc[playerAddress] = Object.entries(resources).map(([resourceType, amount]) => ({
amount: Number(amount),
resourceId: Number(resourceType),
}));
return acc;
},
{} as Record<string, Resource[]>,
);

const [showContributions, setShowContributions] = useState(false);

// Calculate percentages and sort contributors
const sortedContributors = Object.entries(groupedContributions)
.map(([playerAddress, resources]) => ({
playerAddress,
resources,
percentage: getContributionsTotalPercentage(resourceContributions[playerAddress]) * 100,
}))
.sort((a, b) => b.percentage - a.percentage);

return (
<div className={`space-y-2 ${className || ""}`}>
<div
className="flex items-center cursor-pointer hover:text-white"
onClick={() => setShowContributions(!showContributions)}
>
<span className="mr-2">Contributors</span>
<span className={`transform transition-transform ${showContributions ? "rotate-90" : ""}`}></span>
</div>
{showContributions && (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2">
{sortedContributors.map(({ playerAddress, resources, percentage }) => (
<div key={playerAddress} className="bg-gold/10 p-1 rounded">
<div className="flex flex-row mb-1 justify-between mr-1 items-end">
<div className="text-sm font-bold">{getAddressName(ContractAddress(playerAddress))}</div>
<div className="text-xs">{percentage.toFixed(2)}%</div>
</div>
<div className="flex flex-wrap gap-2">
{Object.entries(resources).map(([resourceType, amount]) => (
<div key={resourceType} className="flex items-center">
<ResourceIcon size="xs" resource={ResourcesIds[Number(resourceType)]} />
<span className="ml-1 text-xs">{currencyIntlFormat(divideByPrecision(Number(amount)))}</span>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
);
};
84 changes: 44 additions & 40 deletions client/src/ui/components/hyperstructures/HyperstructurePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
MAX_NAME_LENGTH,
} from "@bibliothecadao/eternum";
import { useMemo, useState } from "react";
import { ContributionSummary } from "./ContributionSummary";
import { HyperstructureDetails } from "./HyperstructureDetails";
import { HyperstructureResourceChip } from "./HyperstructureResourceChip";

Expand All @@ -41,10 +42,10 @@ export const HyperstructurePanel = ({ entity }: any) => {

const structureEntityId = useUIStore((state) => state.structureEntityId);
const { useProgress } = useHyperstructures();
const { getContributionsByPlayerAddress } = useContributions();
const { useContributionsByPlayerAddress } = useContributions();

const progresses = useProgress(entity.entity_id);
const contributions = getContributionsByPlayerAddress(BigInt(account.address), entity.entity_id);
const myContributions = useContributionsByPlayerAddress(BigInt(account.address), entity.entity_id);

const updates = useUpdates(entity.entity_id);

Expand Down Expand Up @@ -93,26 +94,29 @@ export const HyperstructurePanel = ({ entity }: any) => {
/>
);
});
}, [progresses, contributions]);
}, [progresses, myContributions]);

const initialPoints = useMemo(() => {
return calculateCompletionPoints(contributions);
}, [contributions, updates]);
return calculateCompletionPoints(myContributions);
}, [myContributions, updates]);

const shares = useMemo(() => {
return LeaderboardManager.instance().getShares(ContractAddress(account.address), entity.entity_id);
}, [contributions, updates]);
const myShares = useMemo(() => {
return LeaderboardManager.instance().getAddressShares(ContractAddress(account.address), entity.entity_id);
}, [myContributions, updates]);

return (
<div className="flex flex-col h-[45vh] justify-between">
<div className="flex flex-col mb-2 bg-blueish/10 p-3 ">
<div className=" align-text-bottom uppercase text-xs">Owner: {ownerName}</div>
<div className="flex flex-row justify-between items-baseline">
<div className="flex flex-col justify-between h-full">
<div className="grid grid-cols-5 text-xxs bg-blueish/10 p-1">
<div className="flex flex-col">
<div className="">Owner:</div>
<div>{ownerName}</div>
</div>
<div className="col-span-4">
{editName ? (
<div className="flex space-x-2">
<TextInput
placeholder="Type Name"
className="h-full"
className="h-full flex-grow"
value={naming}
onChange={(name) => setNaming(name)}
maxLength={MAX_NAME_LENGTH}
Expand All @@ -138,48 +142,48 @@ export const HyperstructurePanel = ({ entity }: any) => {
</Button>
</div>
) : (
<h3 className="truncate pr-5">{entity.name}</h3>
)}

{account.address === entity.owner && (
<>
<Button size="xs" variant="default" onClick={() => setEditName(!editName)}>
edit name
</Button>
</>
<div className="flex justify-between items-center">
<h5 className="truncate pr-5">{entity.name}</h5>
{account.address === entity.owner && (
<Button size="xs" variant="default" onClick={() => setEditName(!editName)}>
edit name
</Button>
)}
</div>
)}
</div>
</div>

<div className="w-[100%] grid justify-between m-auto mb-2 gap-2 grid-cols-4">
<div className="p-3 bg-gold/10 gap-1 hover:bg-crimson/40 hover:animate-pulse">
<div className="flex flex-col justify-center items-center text-center h-full">
<div className="uppercase text-xs">Initial points received</div>
<div className="font-bold text-xl">{currencyIntlFormat(initialPoints)}</div>
<div className="w-[100%] grid justify-between m-auto mb-1 gap-1 grid-cols-4">
<div className="p-1 bg-gold/10 gap-0.5 hover:bg-crimson/40 hover:animate-pulse">
<div className="flex flex-col justify-center items-center text-center">
<div className="uppercase text-[10px]">Initial points</div>
<div className="font-bold text-sm">{currencyIntlFormat(initialPoints)}</div>
</div>
</div>
<div className="p-3 bg-gold/10 gap-1 hover:bg-crimson/40 hover:animate-pulse">
<div className="flex flex-col justify-center items-center text-center h-full">
<div className="uppercase text-xs">Progress</div>
<div className="font-bold text-xl">{currencyIntlFormat(progresses.percentage)}%</div>
<div className="p-1 bg-gold/10 gap-0.5 hover:bg-crimson/40 hover:animate-pulse">
<div className="flex flex-col justify-center items-center text-center">
<div className="uppercase text-[10px]">Progress</div>
<div className="font-bold text-sm">{currencyIntlFormat(progresses.percentage)}%</div>
</div>
</div>
<div className="p-3 bg-gold/10 gap-1 hover:bg-crimson/40 hover:animate-pulse">
<div className="flex flex-col justify-center items-center text-center h-full">
<div className="uppercase text-xs">Shares</div>
<div className="font-bold text-xl">{currencyIntlFormat((shares || 0) * 100)}%</div>
<div className="p-1 bg-gold/10 gap-0.5 hover:bg-crimson/40 hover:animate-pulse">
<div className="flex flex-col justify-center items-center text-center">
<div className="uppercase text-[10px]">Shares</div>
<div className="font-bold text-sm">{currencyIntlFormat((myShares || 0) * 100)}%</div>
</div>
</div>
<div className="p-3 bg-gold/10 gap-1 hover:bg-crimson/40 hover:animate-pulse">
<div className="flex flex-col justify-center items-center text-center h-full">
<div className="uppercase text-xs">Points/cycle</div>
<div className="font-bold text-xl ">
{currencyIntlFormat((shares || 0) * HYPERSTRUCTURE_POINTS_PER_CYCLE)}
<div className="p-1 bg-gold/10 gap-0.5 hover:bg-crimson/40 hover:animate-pulse">
<div className="flex flex-col justify-center items-center text-center">
<div className="uppercase text-[10px]">Points/cycle</div>
<div className="font-bold text-sm">
{currencyIntlFormat((myShares || 0) * HYPERSTRUCTURE_POINTS_PER_CYCLE)}
</div>
</div>
</div>
</div>
<div className="overflow-y-scroll no-scrollbar h-[40vh] bg-gold/10 p-2">
<ContributionSummary hyperstructureEntityId={entity.entity_id} className="mb-1" />
{progresses.percentage === 100 ? (
<HyperstructureDetails hyperstructureEntityId={entity.entity_id} />
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,9 @@ export const HyperstructureResourceChip = ({
}, [resetContributions]);

return (
// <div className="flex mt-1 grid grid-cols-8 gap-2">
<div className="mt-1 grid grid-cols-8 gap-2">
<div className="mt-0.5 grid grid-cols-8 gap-1 items-center">
<div
className={`flex relative items-center text-xs px-2 p-1 col-span-4`}
className={`flex relative items-center text-xs px-2 py-0.5 col-span-4`}
style={{
backgroundImage:
progress.percentage > 0
Expand Down Expand Up @@ -86,24 +85,27 @@ export const HyperstructureResourceChip = ({
isLabor={false}
withTooltip={false}
resource={findResourceById(getIconResourceId(resourceId, false))?.trait as string}
size="sm"
className="mr-3 self-center"
size="xs"
className="mr-2 self-center"
/>

<div className="flex justify-between">
<div className=" self-center text-sm font-bold">{`${progress.percentage}% (${currencyIntlFormat(
<div className="self-center text-xs font-semibold">{`${progress.percentage}% (${currencyIntlFormat(
progress.amount,
)} / ${currencyIntlFormat(progress.costNeeded)})`}</div>
</div>
</div>

<NumberInput
value={inputValue}
className="w-full col-span-3"
className="w-full text-xs col-span-3 h-6"
onChange={setInputValue}
max={maxContributableAmount}
/>
<div className="ml-2 flex items-center" onClick={() => setInputValue(maxContributableAmount)}>
<div
className="ml-1 flex items-center text-xs cursor-pointer"
onClick={() => setInputValue(maxContributableAmount)}
>
MAX
</div>
</div>
Expand Down
11 changes: 1 addition & 10 deletions client/src/ui/components/military/PillageHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,13 @@ import { ClientComponents } from "@/dojo/createClientComponents";
import { useDojo } from "@/hooks/context/DojoContext";
import { getEntitiesUtils } from "@/hooks/helpers/useEntities";
import { ResourceCost } from "@/ui/elements/ResourceCost";
import { divideByPrecision, formatSecondsLeftInDaysHoursMinutes } from "@/ui/utils/utils";
import { divideByPrecision, formatResources, formatSecondsLeftInDaysHoursMinutes } from "@/ui/utils/utils";
import { BattleSide, ID, Resource } from "@bibliothecadao/eternum";
import { ComponentValue, defineQuery, getComponentValue, HasValue, isComponentUpdate } from "@dojoengine/recs";
import { useEffect, useMemo, useState } from "react";

type PillageEvent = ComponentValue<ClientComponents["events"]["BattlePillageData"]["schema"]>;

const formatResources = (resources: any[]): Resource[] => {
return resources
.map((resource) => ({
resourceId: Number(resource[0].value),
amount: Number(resource[1].value),
}))
.filter((resource) => resource.amount > 0);
};

const PillageHistoryItem = ({ addressName, history }: { addressName: string; history: PillageEvent }) => {
const isSuccess = history.winner === BattleSide[BattleSide.Attack];
const formattedResources = useMemo(() => formatResources(history.pillaged_resources), [history.pillaged_resources]);
Expand Down
Loading

0 comments on commit 98b2fca

Please sign in to comment.