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

new onboarding flow #2672

Merged
merged 14 commits into from
Jan 30, 2025
1 change: 1 addition & 0 deletions client/apps/game/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ yarn.lock
dev-dist
.env
.env.local
*.ts.timestamp-*.mjs
39 changes: 20 additions & 19 deletions client/apps/game/src/hooks/context/dojo-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import { ReactComponent as CartridgeSmall } from "@/assets/icons/cartridge-small
import { useAccountStore } from "@/hooks/store/use-account-store";
import { useAddressStore } from "@/hooks/store/use-address-store";
import { useUIStore } from "@/hooks/store/use-ui-store";
import { Position } from "@/types/position";
import { OnboardingContainer, StepContainer } from "@/ui/layouts/onboarding";
import { OnboardingButton } from "@/ui/layouts/onboarding-button";
import { CountdownTimer, LoadingScreen } from "@/ui/modules/loading-screen";
import { ACCOUNT_CHANGE_EVENT, SpectateButton } from "@/ui/modules/onboarding/steps";
import { SpectateButton } from "@/ui/modules/onboarding/steps";
import { displayAddress } from "@/ui/utils/utils";
import { ContractAddress, SetupResult } from "@bibliothecadao/eternum";
import { DojoContext, useQuery } from "@bibliothecadao/react";
import { DojoContext } from "@bibliothecadao/react";
import ControllerConnector from "@cartridge/connector/controller";
import { HasValue, runQuery } from "@dojoengine/recs";
import { cairoShortStringToFelt } from "@dojoengine/torii-client";
import { useAccount, useConnect } from "@starknet-react/core";
import { ReactNode, useContext, useEffect, useMemo, useState } from "react";
import { Account, AccountInterface, RpcProvider } from "starknet";
import { Env, env } from "../../../env";
import { useNavigateToRealmViewByAccount } from "../helpers/use-navigate-to-realm-view-by-account";

const requiredEnvs: (keyof Env)[] = [
"VITE_PUBLIC_MASTER_ADDRESS",
Expand Down Expand Up @@ -81,7 +81,7 @@ export const DojoProvider = ({ children, value, backgroundImage }: DojoProviderP
<DojoContextProvider
value={value}
masterAccount={masterAccount}
controllerAccount={controllerAccount!}
controllerAccount={controllerAccount || null}
backgroundImage={backgroundImage}
>
{children}
Expand All @@ -100,12 +100,10 @@ const DojoContextProvider = ({
controllerAccount: AccountInterface | null;
backgroundImage: string;
}) => {
const setSpectatorMode = useUIStore((state) => state.setSpectatorMode);
const isSpectatorMode = useUIStore((state) => state.isSpectatorMode);
const showBlankOverlay = useUIStore((state) => state.setShowBlankOverlay);
const setAddressName = useAddressStore((state) => state.setAddressName);
useNavigateToRealmViewByAccount(value.components);

const { handleUrlChange } = useQuery();
const showBlankOverlay = useUIStore((state) => state.showBlankOverlay);
const setAddressName = useAddressStore((state) => state.setAddressName);

const currentValue = useContext(DojoContext);
if (currentValue) throw new Error("DojoProvider can only be used once");
Expand All @@ -127,14 +125,17 @@ const DojoContextProvider = ({
}
};

const onSpectatorModeClick = () => {
setSpectatorMode(true);
handleUrlChange(new Position({ x: 0, y: 0 }).toMapLocationUrl());
window.dispatchEvent(new Event(ACCOUNT_CHANGE_EVENT));
showBlankOverlay(false);
};
const [accountToUse, setAccountToUse] = useState<Account | AccountInterface>(
new Account(value.network.provider.provider, "0x0", "0x0"),
);

const accountToUse = isSpectatorMode ? new Account(value.network.provider.provider, "0x0", "0x0") : controllerAccount;
useEffect(() => {
if (!controllerAccount) {
setAccountToUse(new Account(value.network.provider.provider, "0x0", "0x0"));
} else {
setAccountToUse(controllerAccount);
}
}, [controllerAccount]);
aymericdelab marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
const setUserName = async () => {
Expand Down Expand Up @@ -188,7 +189,7 @@ const DojoContextProvider = ({
);
}

if (!isConnected && !isConnecting && !controllerAccount && !isSpectatorMode) {
if (!isConnected && !isConnecting && showBlankOverlay) {
aymericdelab marked this conversation as resolved.
Show resolved Hide resolved
return (
<>
<CountdownTimer backgroundImage={backgroundImage} />
Expand All @@ -197,7 +198,7 @@ const DojoContextProvider = ({
<div className="flex justify-center space-x-8 mt-2 md:mt-4">
{!isConnected && (
<>
<SpectateButton onClick={onSpectatorModeClick} />
<SpectateButton onClick={() => {}} />
aymericdelab marked this conversation as resolved.
Show resolved Hide resolved
<OnboardingButton
onClick={connectWallet}
className="!bg-[#FCB843] !text-black border-none hover:!bg-[#FCB843]/80"
Expand Down Expand Up @@ -226,7 +227,7 @@ const DojoContextProvider = ({
...value,
masterAccount,
account: {
account: accountToUse as Account | AccountInterface,
account: accountToUse,
accountDisplay: displayAddress((accountToUse as Account | AccountInterface)?.address || ""),
},
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Position } from "@/types/position";
import { getPlayerFirstRealm, getRandomRealmEntity } from "@/utils/realms";
import { ClientComponents, ContractAddress } from "@bibliothecadao/eternum";
import { getComponentValue } from "@dojoengine/recs";
import { useAccount } from "@starknet-react/core";
import { useEffect } from "react";
import { useNavigateToHexView } from "./use-navigate";

export const useNavigateToRealmViewByAccount = (components: ClientComponents) => {
const navigateToHexView = useNavigateToHexView();
const { account } = useAccount();

// navigate to random hex view if not connected or to player's first realm if connected
useEffect(() => {
if (!account) {
const randomRealmEntity = getRandomRealmEntity(components);
const position = randomRealmEntity ? getComponentValue(components.Position, randomRealmEntity) : undefined;
position && navigateToHexView(new Position(position));
} else {
const playerRealm = getPlayerFirstRealm(components, ContractAddress(account?.address || "0x0"));
const position = playerRealm ? getComponentValue(components.Position, playerRealm) : undefined;
position && navigateToHexView(new Position(position));
}
}, [account?.address]);
aymericdelab marked this conversation as resolved.
Show resolved Hide resolved
};
35 changes: 35 additions & 0 deletions client/apps/game/src/hooks/helpers/use-navigate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Position } from "@/types/position";
import { useQuery } from "@bibliothecadao/react";
import { useUIStore } from "../store/use-ui-store";

export const useNavigateToHexView = () => {
const showBlankOverlay = useUIStore((state) => state.setShowBlankOverlay);
const setIsLoadingScreenEnabled = useUIStore((state) => state.setIsLoadingScreenEnabled);
const setPreviewBuilding = useUIStore((state) => state.setPreviewBuilding);
const { handleUrlChange } = useQuery();

return (position: Position) => {
const url = position.toHexLocationUrl();

setIsLoadingScreenEnabled(true);
showBlankOverlay(false);
setPreviewBuilding(null);
handleUrlChange(url);
};
};

export const useNavigateToMapView = () => {
const showBlankOverlay = useUIStore((state) => state.setShowBlankOverlay);
const setPreviewBuilding = useUIStore((state) => state.setPreviewBuilding);
const { handleUrlChange, isMapView } = useQuery();
const setIsLoadingScreenEnabled = useUIStore((state) => state.setIsLoadingScreenEnabled);

return (position: Position) => {
showBlankOverlay(false);
setPreviewBuilding(null);
handleUrlChange(position.toMapLocationUrl());
if (!isMapView) {
setIsLoadingScreenEnabled(true);
}
};
};
aymericdelab marked this conversation as resolved.
Show resolved Hide resolved
38 changes: 9 additions & 29 deletions client/apps/game/src/hooks/helpers/use-structure-entity-id.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,33 @@
import { useUIStore } from "@/hooks/store/use-ui-store";
import { Position as PositionInterface } from "@/types/position";
import { UNDEFINED_STRUCTURE_ENTITY_ID } from "@/ui/constants";
import { ContractAddress } from "@bibliothecadao/eternum";
import { useDojo, usePlayerStructures, useQuery } from "@bibliothecadao/react";
import { useDojo, useQuery } from "@bibliothecadao/react";
import { Entity, Has, HasValue, getComponentValue, runQuery } from "@dojoengine/recs";
import { useEffect, useMemo } from "react";
import { useEffect } from "react";

export const useStructureEntityId = () => {
const {
setup: {
components: { Structure, Position, Owner },
components: { Structure, Position },
},
account: { account },
} = useDojo();

const { hexPosition, isMapView } = useQuery();
const { hexPosition } = useQuery();
const setStructureEntityId = useUIStore((state) => state.setStructureEntityId);
const isSpectatorMode = useUIStore((state) => state.isSpectatorMode);
const structureEntityId = useUIStore((state) => state.structureEntityId);

const address = isSpectatorMode ? ContractAddress("0x0") : ContractAddress(account.address);

const structures = usePlayerStructures(ContractAddress(account.address));

const defaultPlayerStructure = useMemo(() => {
return structures[0];
}, [structureEntityId, structures]);

useEffect(() => {
const { x, y } = new PositionInterface({
const position = new PositionInterface({
x: hexPosition.col,
y: hexPosition.row,
}).getContract();

const structureEntity = runQuery([Has(Structure), HasValue(Position, { x, y })])
const structureEntity = runQuery([Has(Structure), HasValue(Position, { x: position.x, y: position.y })])
.values()
.next().value;

const structure = getComponentValue(Structure, structureEntity ?? ("0" as Entity));
aymericdelab marked this conversation as resolved.
Show resolved Hide resolved
const structureOwner = getComponentValue(Owner, structureEntity ?? ("0" as Entity));

const isOwner = structureOwner?.address === BigInt(address);
const newStructureId = structure?.entity_id;

if (isMapView) {
setStructureEntityId(
isOwner ? structureOwner.entity_id : defaultPlayerStructure?.entity_id || UNDEFINED_STRUCTURE_ENTITY_ID,
);
} else {
setStructureEntityId(structure?.entity_id || UNDEFINED_STRUCTURE_ENTITY_ID);
}
}, [defaultPlayerStructure, isMapView, hexPosition, address]);
setStructureEntityId(newStructureId || UNDEFINED_STRUCTURE_ENTITY_ID);
}, [hexPosition]);
};
10 changes: 5 additions & 5 deletions client/apps/game/src/hooks/store/use-ui-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,11 @@ interface UIStore {
setShowMinimap: (show: boolean) => void;
selectedPlayer: ContractAddress | null;
setSelectedPlayer: (player: ContractAddress | null) => void;
isSpectatorMode: boolean;
setSpectatorMode: (enabled: boolean) => void;
hasAcceptedToS: boolean;
setHasAcceptedToS: (accepted: boolean) => void;
showToS: boolean;
setShowToS: (show: boolean) => void;
setModal: (content: React.ReactNode | null, show: boolean) => void;
}

export type AppStore = UIStore & PopupsStore & ThreeStore & BuildModeStore & RealmStore & WorldStore;
Expand All @@ -79,7 +78,9 @@ export const useUIStore = create(
showBlurOverlay: false,
setShowBlurOverlay: (show) => set({ showBlurOverlay: show }),
showBlankOverlay: true,
setShowBlankOverlay: (show) => set({ showBlankOverlay: show }),
setShowBlankOverlay: (show) => {
set({ showBlankOverlay: show });
},
isSideMenuOpened: true,
toggleSideMenu: () => set((state) => ({ isSideMenuOpened: !state.isSideMenuOpened })),
isSoundOn: localStorage.getItem("soundEnabled") ? localStorage.getItem("soundEnabled") === "true" : true,
Expand Down Expand Up @@ -125,15 +126,14 @@ export const useUIStore = create(
setShowMinimap: (show: boolean) => set({ showMinimap: show }),
selectedPlayer: null,
setSelectedPlayer: (player: ContractAddress | null) => set({ selectedPlayer: player }),
isSpectatorMode: false,
setSpectatorMode: (enabled: boolean) => set({ isSpectatorMode: enabled }),
hasAcceptedToS: localStorage.getItem("hasAcceptedToS") ? localStorage.getItem("hasAcceptedToS") === "true" : false,
setHasAcceptedToS: (accepted: boolean) => {
set({ hasAcceptedToS: accepted });
localStorage.setItem("hasAcceptedToS", String(accepted));
},
showToS: false,
setShowToS: (show: boolean) => set({ showToS: show }),
setModal: (content: React.ReactNode | null, show: boolean) => set({ modalContent: content, showModal: show }),
...createPopupsSlice(set, get),
...createThreeStoreSlice(set, get),
...createBuildModeStoreSlice(set),
Expand Down
18 changes: 16 additions & 2 deletions client/apps/game/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useUIStore } from "./hooks/store/use-ui-store";
import "./index.css";
import GameRenderer from "./three/game-renderer";
import { PWAUpdatePopup } from "./ui/components/pwa-update-popup";
import { NoAccountModal } from "./ui/layouts/no-account-modal";
import { LoadingScreen } from "./ui/modules/loading-screen";
import { getRandomBackgroundImage } from "./ui/utils/utils";
import { ETERNUM_CONFIG } from "./utils/config";
Expand Down Expand Up @@ -69,10 +70,23 @@ async function init() {

const setupResult = await setup(
{ ...dojoConfig },
{ vrfProviderAddress: env.VITE_PUBLIC_VRF_PROVIDER_ADDRESS, useBurner: env.VITE_PUBLIC_CHAIN === "local" },
{
vrfProviderAddress: env.VITE_PUBLIC_VRF_PROVIDER_ADDRESS,
useBurner: env.VITE_PUBLIC_CHAIN === "local",
},
{
onNoAccount: () => {
state.setModal(null, false);
state.setModal(<NoAccountModal />, true);
},
onError: (error) => {
console.error("System call error:", error);
// Handle other types of errors if needed
},
},
);

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

await initialSync(setupResult, state);
Expand Down
5 changes: 0 additions & 5 deletions client/apps/game/src/three/helpers/location-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,4 @@ export class LocationManager {
this.updateUrlParams();
return this.urlParams.has("row") && this.urlParams.has("col");
}

public static updateUrl(url: string) {
window.history.pushState({}, "", url);
window.dispatchEvent(new Event("urlChanged"));
}
}
8 changes: 6 additions & 2 deletions client/apps/game/src/three/scenes/hexception.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export default class HexceptionScene extends HexagonScene {
this.loadBuildingModels();
this.loadBiomeModels(900);

this.tileManager = new TileManager(this.dojo.components, this.dojo.network.provider, { col: 0, row: 0 });
this.tileManager = new TileManager(this.dojo.components, this.dojo.systemCalls, { col: 0, row: 0 });

this.setup();

Expand Down Expand Up @@ -304,13 +304,17 @@ export default class HexceptionScene extends HexagonScene {

const normalizedCoords = { col: hexCoords.col, row: hexCoords.row };
const buildingType = this.buildingPreview?.getPreviewBuilding();

// Check if account exists before allowing actions
const account = useAccountStore.getState().account;
if (buildingType) {
// if building mode
if (!this.tileManager.isHexOccupied(normalizedCoords)) {
this.clearBuildingMode();
try {
await this.tileManager.placeBuilding(
useAccountStore.getState().account!,
account!,
this.state.structureEntityId,
buildingType.type,
normalizedCoords,
buildingType.resource,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default class WorldmapScene extends HexagonScene {
this.biome = new Biome();

this.structurePreview = new StructurePreview(this.scene);
this.tileManager = new TileManager(this.dojo.components, this.dojo.network.provider, { col: 0, row: 0 });
this.tileManager = new TileManager(this.dojo.components, this.dojo.systemCalls, { col: 0, row: 0 });

this.loadBiomeModels(this.renderChunkSize.width * this.renderChunkSize.height);

Expand Down Expand Up @@ -298,13 +298,10 @@ export default class WorldmapScene extends HexagonScene {
this.clearCache();
this.totalStructures = this.structureManager.getTotalStructures() + 1;

const account = useAccountStore.getState().account;

this.tileManager
.placeStructure(
useAccountStore.getState().account!,
this.structureEntityId,
buildingType.type,
contractHexPosition,
)
.placeStructure(account!, this.structureEntityId, buildingType.type, contractHexPosition)
.catch(() => {
this.structureManager.structures.removeStructureFromPosition(hexCoords);
this.structureManager.structureHexCoords.get(hexCoords.col)?.delete(hexCoords.row);
Expand Down Expand Up @@ -336,6 +333,9 @@ export default class WorldmapScene extends HexagonScene {
return;
}

// Check if account exists before allowing actions
const account = useAccountStore.getState().account;

const { currentBlockTimestamp, currentArmiesTick } = getBlockTimestamp();

const { selectedEntityId, travelPaths } = this.state.armyActions;
Expand All @@ -351,13 +351,7 @@ export default class WorldmapScene extends HexagonScene {
selectedEntityId,
);
playSound(soundSelector.unitMarching1, this.state.isSoundOn, this.state.effectsLevel);
armyMovementManager.moveArmy(
useAccountStore.getState().account!,
selectedPath,
isExplored,
currentBlockTimestamp,
currentArmiesTick,
);
armyMovementManager.moveArmy(account!, selectedPath, isExplored, currentBlockTimestamp, currentArmiesTick);
this.state.updateHoveredHex(null);
}
}
Expand Down
Loading
Loading