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/client labor #2666

Draft
wants to merge 7 commits into
base: next
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions client/apps/game/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ async function init() {
{ vrfProviderAddress: env.VITE_PUBLIC_VRF_PROVIDER_ADDRESS, useBurner: env.VITE_PUBLIC_CHAIN === "local" },
);

await initialSync(setupResult, state);

const eternumConfig = await ETERNUM_CONFIG();
configManager.setDojo(setupResult.components, eternumConfig);

await initialSync(setupResult, state);

const graphic = new GameRenderer(setupResult);

graphic.initScene();
Expand Down
1 change: 1 addition & 0 deletions client/apps/game/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ID } from "@bibliothecadao/eternum";
export enum RightView {
None,
ResourceTable,
LaborTable,
}

export enum LeftView {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { NumberInput } from "@/ui/elements/number-input";
import { ResourceIcon } from "@/ui/elements/resource-icon";
import { currencyIntlFormat, divideByPrecision } from "@/ui/utils/utils";
import { getBlockTimestamp } from "@/utils/timestamp";
import { findResourceById, getBalance, getIconResourceId, ID } from "@bibliothecadao/eternum";
import { findResourceById, getBalance, ID } from "@bibliothecadao/eternum";
import { ProgressWithPercentage, useDojo } from "@bibliothecadao/react";
import { useEffect, useState } from "react";

Expand Down Expand Up @@ -70,7 +70,7 @@ export const HyperstructureResourceChip = ({
position: "top",
content: (
<>
{findResourceById(getIconResourceId(resourceId, false))?.trait as string} ({currencyIntlFormat(balance)}
{findResourceById(resourceId)?.trait as string} ({currencyIntlFormat(balance)}
)
</>
),
Expand All @@ -83,7 +83,7 @@ export const HyperstructureResourceChip = ({
<ResourceIcon
isLabor={false}
withTooltip={false}
resource={findResourceById(getIconResourceId(resourceId, false))?.trait as string}
resource={findResourceById(resourceId)?.trait as string}
size="xs"
className="mr-2 self-center"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ID, LaborIds, ResourcesIds } from "@bibliothecadao/eternum";
import { useDojo } from "@bibliothecadao/react";
import { LaborChip } from "./labor-chip";

export const EntityLaborTable = ({ entityId }: { entityId: ID | undefined }) => {
const dojo = useDojo();

if (!entityId || entityId === 0) {
return <div>No Entity Selected</div>;
}

const startProduction = async (resourceType: ResourcesIds, producedAmount: number) => {
try {
await dojo.setup.systemCalls.start_production({
signer: dojo.account.account,
entity_id: entityId,
resource_type: resourceType,
amount: producedAmount,
});
} catch (error) {
console.error("Failed to start production", error);
}
};

return Object.entries(LaborIds).map(([_, laborId]) => {
if (isNaN(Number(laborId))) return null;
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 isNaN with Number.isNaN for safer type checking.

The isNaN function performs type coercion which can lead to unexpected results. Use Number.isNaN instead for more reliable number validation.

-    if (isNaN(Number(laborId))) return null;
+    if (Number.isNaN(Number(laborId))) return null;
📝 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
if (isNaN(Number(laborId))) return null;
if (Number.isNaN(Number(laborId))) return null;
🧰 Tools
🪛 Biome (1.9.4)

[error] 26-26: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

return <LaborChip key={laborId} laborId={laborId as LaborIds} startProduction={startProduction} />;
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
configManager,
ID,
RESOURCE_TIERS,
ResourcesIds,
StructureType,
} from "@bibliothecadao/eternum";
import { useDojo } from "@bibliothecadao/react";
Expand Down Expand Up @@ -38,7 +39,7 @@ export const EntityResourceTable = ({ entityId }: { entityId: ID | undefined })
}

return Object.entries(RESOURCE_TIERS).map(([tier, resourceIds]) => {
const resources = resourceIds.map((resourceId: any) => (
const resources = resourceIds.map((resourceId: ResourcesIds) => (
<ResourceChip
key={resourceId}
resourceId={resourceId}
Expand Down
62 changes: 62 additions & 0 deletions client/apps/game/src/ui/components/resources/labor-chip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Button from "@/ui/elements/button";
import { NumberInput } from "@/ui/elements/number-input";
import { ResourceIcon } from "@/ui/elements/resource-icon";
import {
configManager,
findResourceById,
getResourceIdFromLaborId,
LaborIds,
ResourcesIds,
} from "@bibliothecadao/eternum";
import { useState } from "react";

type LaborChipProps = {
laborId: LaborIds;
startProduction: (resourceType: ResourcesIds, amount: number) => void;
};

export const LaborChip = ({ laborId, startProduction }: LaborChipProps) => {
const resourceId = getResourceIdFromLaborId(laborId);

const [productionAmount, setProductionAmount] = useState(0);

const laborInputs = configManager.laborInputs[resourceId as ResourcesIds];

return (
<>
<div className={`flex gap-2 relative group items-center text-xs px-2 p-1 hover:bg-gold/20 `}>
<div className="flex flex-col">
{laborInputs?.map(({ resource, amount }) => (
<div key={resource} className="flex flex-row items-center">
<ResourceIcon
withTooltip={false}
resource={findResourceById(resource)?.trait as string}
size="sm"
className="mr-3 self-center"
/>
<div className="text-red">-{amount * productionAmount}</div>
</div>
))}
Comment on lines +29 to +39
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null check for laborInputs before mapping.

The current implementation might throw if laborInputs is undefined.

-  {laborInputs?.map(({ resource, amount }) => (
+  {laborInputs ? (
+    laborInputs.map(({ resource, amount }) => (
       <div key={resource} className="flex flex-row items-center">
         <ResourceIcon
           withTooltip={false}
           resource={findResourceById(resource)?.trait as string}
           size="sm"
           className="mr-3 self-center"
         />
         <div className="text-red">-{amount * productionAmount}</div>
       </div>
-    ))}
+    ))
+  ) : (
+    <div>No labor inputs configured</div>
+  )}
📝 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
{laborInputs?.map(({ resource, amount }) => (
<div key={resource} className="flex flex-row items-center">
<ResourceIcon
withTooltip={false}
resource={findResourceById(resource)?.trait as string}
size="sm"
className="mr-3 self-center"
/>
<div className="text-red">-{amount * productionAmount}</div>
</div>
))}
{laborInputs ? (
laborInputs.map(({ resource, amount }) => (
<div key={resource} className="flex flex-row items-center">
<ResourceIcon
withTooltip={false}
resource={findResourceById(resource)?.trait as string}
size="sm"
className="mr-3 self-center"
/>
<div className="text-red">-{amount * productionAmount}</div>
</div>
))
) : (
<div>No labor inputs configured</div>
)}

</div>
<div className="grid grid-cols-10 w-full items-center">
<div className="flex flex-col col-span-7">
<NumberInput
value={productionAmount}
onChange={(productionAmount) => setProductionAmount(productionAmount)}
className="col-span-7"
/>
Comment on lines +43 to +47
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

Add validation for production amount input.

The NumberInput component should have minimum/maximum value constraints and proper validation.

 <NumberInput
   value={productionAmount}
   onChange={(productionAmount) => setProductionAmount(productionAmount)}
+  min={0}
+  max={999999}
   className="col-span-7"
 />
📝 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
<NumberInput
value={productionAmount}
onChange={(productionAmount) => setProductionAmount(productionAmount)}
className="col-span-7"
/>
<NumberInput
value={productionAmount}
onChange={(productionAmount) => setProductionAmount(productionAmount)}
min={0}
max={999999}
className="col-span-7"
/>

<Button variant="primary" onClick={() => startProduction(resourceId, productionAmount)}>
Start Producing
</Button>
</div>
<ResourceIcon
withTooltip={false}
resource={findResourceById(resourceId)?.trait as string}
size="md"
className=""
/>
</div>
</div>
</>
);
};
18 changes: 4 additions & 14 deletions client/apps/game/src/ui/components/resources/resource-chip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,12 @@ import { useUIStore } from "@/hooks/store/use-ui-store";
import { ResourceIcon } from "@/ui/elements/resource-icon";
import { currencyFormat, currencyIntlFormat, divideByPrecision, gramToKg } from "@/ui/utils/utils";
import { getBlockTimestamp } from "@/utils/timestamp";
import {
configManager,
findResourceById,
formatTime,
getIconResourceId,
ID,
TickIds,
TimeFormat,
} from "@bibliothecadao/eternum";
import { configManager, findResourceById, formatTime, ID, TickIds, TimeFormat } from "@bibliothecadao/eternum";
import { useResourceManager } from "@bibliothecadao/react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { RealmTransfer } from "./realm-transfer";

export const ResourceChip = ({
isLabor = false,
resourceId,
entityId,
maxStorehouseCapacityKg,
Expand Down Expand Up @@ -86,9 +77,8 @@ export const ResourceChip = ({
const icon = useMemo(
() => (
<ResourceIcon
isLabor={isLabor}
withTooltip={false}
resource={findResourceById(getIconResourceId(resourceId, isLabor))?.trait as string}
resource={findResourceById(resourceId)?.trait as string}
size="sm"
className="mr-3 self-center"
/>
Expand All @@ -103,10 +93,10 @@ export const ResourceChip = ({
const handleMouseEnter = useCallback(() => {
setTooltip({
position: "top",
content: <>{findResourceById(getIconResourceId(resourceId, isLabor))?.trait as string}</>,
content: <>{findResourceById(resourceId)?.trait as string}</>,
});
setShowPerHour(false);
}, [resourceId, isLabor, setTooltip]);
}, [resourceId, setTooltip]);

const handleMouseLeave = useCallback(() => {
setTooltip(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,7 @@ import { ResourceCost } from "@/ui/elements/resource-cost";
import { multiplyByPrecision } from "@/ui/utils/utils";
import { getBlockTimestamp } from "@/utils/timestamp";
// import { ETERNUM_CONFIG } from "@/utils/config";
import {
ID,
RESOURCE_PRECISION,
ResourcesIds,
StructureType,
configManager,
getBalance,
} from "@bibliothecadao/eternum";
import { ID, ResourcesIds, StructureType, configManager, getBalance } from "@bibliothecadao/eternum";
import { useDojo } from "@bibliothecadao/react";
import React from "react";

Expand Down Expand Up @@ -49,13 +42,8 @@ export const StructureConstructionMenu = ({ className, entityId }: { className?:
{buildingTypes.map((structureType, index) => {
const building = StructureType[structureType as keyof typeof StructureType];

// if is hyperstructure, the construction cost are only fragments
const isHyperstructure = building === StructureType["Hyperstructure"];
const cost = configManager.structureCosts[building];
// scaleResourceCostMinMax(
// configManager.getHyperstructureConstructionCosts(),
// RESOURCE_PRECISION,
// );

const hasBalance = checkBalance(isHyperstructure ? cost : []);

Expand Down Expand Up @@ -95,12 +83,7 @@ const StructureInfo = ({
}) => {
const dojo = useDojo();
const currentDefaultTick = getBlockTimestamp().currentDefaultTick;
// if is hyperstructure, the construction cost are only fragments
const isHyperstructure = structureId === StructureType["Hyperstructure"];
const cost = configManager.structureCosts[structureId];
// eternumConfig.hyperstructures.hyperstructureCreationCosts.filter(
// (cost) => !isHyperstructure || cost.resource_tier === ResourceTier.Lords,
// );

const perTick =
structureId == StructureType.Hyperstructure
Expand Down Expand Up @@ -132,8 +115,7 @@ const StructureInfo = ({
key={index}
type="horizontal"
resourceId={ResourcesIds.AncientFragment}
// amount={cost[Number(resourceId)].min_amount * RESOURCE_PRECISION}
amount={cost[Number(resourceId)].amount * RESOURCE_PRECISION}
amount={cost[Number(resourceId)].amount}
balance={balance.balance}
/>
);
Expand Down
1 change: 1 addition & 0 deletions client/apps/game/src/ui/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,5 @@ export enum MenuEnum {
resourceArrivals = "resourceArrivals",
trade = "trade",
resourceTable = "resourceTable",
laborTable = "laborTable",
}
2 changes: 1 addition & 1 deletion client/apps/game/src/ui/elements/resource-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import People from "@/assets/icons/common/people.svg?react";
import { ReactComponent as People } from "@/assets/icons/common/people.svg";
import clsx from "clsx";
import type { ReactElement } from "react";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useUIStore } from "@/hooks/store/use-ui-store";
import { RightView } from "@/types";
import { EntityLaborTable } from "@/ui/components/resources/entity-labor-table";
import { BuildingThumbs, MenuEnum } from "@/ui/config";
import Button from "@/ui/elements/button";
import CircleButton from "@/ui/elements/circle-button";
Expand All @@ -25,6 +26,7 @@ export const RightNavigationModule = () => {
<CircleButton
className="resource-table-selector"
image={BuildingThumbs.resources}
disabled={!structureEntityId}
size="xl"
tooltipLocation="top"
label="Balance"
Expand All @@ -33,6 +35,21 @@ export const RightNavigationModule = () => {
/>
),
},
{
name: MenuEnum.laborTable,
button: (
<CircleButton
className="labor-table-selector"
image={BuildingThumbs.construction}
disabled={!structureEntityId}
size="xl"
tooltipLocation="top"
label="Labor"
active={view === RightView.LaborTable}
onClick={() => setView(view === RightView.LaborTable ? RightView.None : RightView.LaborTable)}
/>
),
},
],
[view, structureEntityId],
);
Expand Down Expand Up @@ -65,24 +82,23 @@ export const RightNavigationModule = () => {
className={`w-full pointer-events-auto overflow-y-scroll h-[60vh] rounded-l-2xl border-l-2 border-y-2 border-gold/20`}
>
<Suspense fallback={<div className="p-8">Loading...</div>}>
{!!structureEntityId && (
<div className="entity-resource-table-selector p-2 flex flex-col space-y-1 overflow-y-auto">
<a
className="text-brown cursor-pointer text-lg w-full"
href={`https://empire.realms.world/trade`}
target="_blank"
rel="noopener noreferrer"
>
<Button variant="secondary" className="w-full">
<div className="flex items-center gap-2">
<ResourceIcon resource="Lords" size="xs" />
Bridge Lords & Resources
</div>
</Button>
</a>
<EntityResourceTable entityId={structureEntityId} />
</div>
)}
<div className="entity-resource-table-selector p-2 flex flex-col space-y-1 overflow-y-auto">
<a
className="text-brown cursor-pointer text-lg w-full"
href={`https://empire.realms.world/trade`}
target="_blank"
rel="noopener noreferrer"
>
<Button variant="secondary" className="w-full">
<div className="flex items-center gap-2">
<ResourceIcon resource="Lords" size="xs" />
Bridge Lords & Resources
</div>
</Button>
</a>
Comment on lines +86 to +98
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Environment-specific URL handling needs attention

While the link is secure with proper target="_blank" and rel="noopener noreferrer" attributes, there's an inconsistency in environment handling:

  • The bridge URL in right-navigation-module.tsx is hardcoded to mainnet (empire.realms.world/trade)
  • Other components (e.g., onboarding.tsx) properly switch between mainnet/non-mainnet URLs based on VITE_PUBLIC_CHAIN

Consider updating the URL to respect the environment setting:

const bridgeUrl = env.VITE_PUBLIC_CHAIN === "mainnet"
  ? "https://empire.realms.world/trade"
  : "https://empire-next.realms.world/trade";
🔗 Analysis chain

Verify security implications of external link.

The external link to empire.realms.world is properly implemented with security attributes (target="_blank" and rel="noopener noreferrer"). However, please verify:

  1. The URL is correct and points to the intended environment
  2. The feature flag or environment check is not needed for this link
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other instances of this URL to verify consistency
rg -l 'empire\.realms\.world'

# Search for environment-specific URL configurations
rg -l 'VITE_PUBLIC.*URL|VITE_PUBLIC.*ENDPOINT'

Length of output: 1059


Script:

#!/bin/bash
# Check how the URL is used in other files
rg -A 2 -B 2 'empire\.realms\.world' 

# Check environment configuration files for URL settings
cat client/apps/game/env.ts
cat client/apps/game/dojoConfig.ts

# Look for any feature flags related to trading or bridging
rg -i "feature.*flag.*bridge|feature.*flag.*trade"

Length of output: 8038

{view === RightView.ResourceTable && <EntityResourceTable entityId={structureEntityId} />}
{view === RightView.LaborTable && <EntityLaborTable entityId={structureEntityId} />}
</div>
</Suspense>
</BaseContainer>
</div>
Expand Down
1 change: 0 additions & 1 deletion contracts/game/src/models/config.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,6 @@ struct ResourceBridgeWhitelistConfig {
resource_type: u8
}

// speed
#[derive(IntrospectPacked, Copy, Drop, Serde)]
#[dojo::model]
pub struct RealmMaxLevelConfig {
Expand Down
12 changes: 0 additions & 12 deletions contracts/game/src/systems/config/contracts.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -642,18 +642,6 @@ mod config_systems {
}
}

// #[derive(IntrospectPacked, Copy, Drop, Serde)]
// #[dojo::model]
// pub struct LaborConfig {
// #[key]
// // e.g when configuring stone labor, resource_type = stone
// resource_type: u8,
// // uuid used to get the ResourceCost
// input_id: ID,
// // number of resources required to make labor
// input_count: u8,
// }

#[abi(embed_v0)]
impl TransportConfigImpl of super::ITransportConfig<ContractState> {
fn set_speed_config(ref self: ContractState, entity_type: ID, sec_per_km: u16) {
Expand Down
1 change: 0 additions & 1 deletion contracts/game/src/systems/production/contracts.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ mod production_systems {
}
};

// todo: check that entity is a realm
let (building, building_quantity) = BuildingImpl::create(
ref world, entity_id, building_category, produce_resource_type, building_coord
);
Expand Down
Loading
Loading