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
2 changes: 2 additions & 0 deletions .knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

"client/apps/game/src/ui/modules/chat/**",

"client/apps/game-docs/env.ts",

"config/**",
"contracts/**",

Expand Down
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
49 changes: 33 additions & 16 deletions client/apps/game/src/hooks/context/dojo-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,25 @@ 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 { getRandomRealmEntity } from "@/utils/realms";
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 { getComponentValue, 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 { useNavigateToHexView } from "../helpers/use-navigate";
import { useNavigateToRealmViewByAccount } from "../helpers/use-navigate-to-realm-view-by-account";

export const NULL_ACCOUNT = {
address: "0x0",
privateKey: "0x0",
} as const;

const requiredEnvs: (keyof Env)[] = [
"VITE_PUBLIC_MASTER_ADDRESS",
Expand Down Expand Up @@ -81,7 +89,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 +108,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 @@ -115,6 +121,8 @@ const DojoContextProvider = ({

const [accountsInitialized, setAccountsInitialized] = useState(false);

const navigateToHexView = useNavigateToHexView();

const [retries, setRetries] = useState(0);

const connectWallet = async () => {
Expand All @@ -127,14 +135,23 @@ const DojoContextProvider = ({
}
};

const [accountToUse, setAccountToUse] = useState<Account | AccountInterface>(
new Account(value.network.provider.provider, NULL_ACCOUNT.address, NULL_ACCOUNT.privateKey),
);

const onSpectatorModeClick = () => {
setSpectatorMode(true);
handleUrlChange(new Position({ x: 0, y: 0 }).toMapLocationUrl());
window.dispatchEvent(new Event(ACCOUNT_CHANGE_EVENT));
showBlankOverlay(false);
const randomRealmEntity = getRandomRealmEntity(value.components);
const position = randomRealmEntity && getComponentValue(value.components.Position, randomRealmEntity);
position && navigateToHexView(new Position(position));
Comment on lines 142 to +145
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

Implement the navigateToHexView function.

The spectator mode click handler relies on an unimplemented navigation function, which will throw an error when used.

Consider implementing the navigation function or using an existing navigation utility from your codebase. The function should handle the Position type and navigate to the appropriate hex view.

Also applies to: 252-254

};

const accountToUse = isSpectatorMode ? new Account(value.network.provider.provider, "0x0", "0x0") : controllerAccount;
useEffect(() => {
if (!controllerAccount) {
setAccountToUse(new Account(value.network.provider.provider, NULL_ACCOUNT.address, NULL_ACCOUNT.privateKey));
} else {
setAccountToUse(controllerAccount);
}
}, [controllerAccount]);

useEffect(() => {
const setUserName = async () => {
Expand Down Expand Up @@ -188,7 +205,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 Down Expand Up @@ -226,7 +243,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,26 @@
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 { NULL_ACCOUNT } from "../context/dojo-context";
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;
navigateToHexView(new Position(position || { x: 0, y: 0 }));
} else {
const playerRealm = getPlayerFirstRealm(components, ContractAddress(account?.address || NULL_ACCOUNT.address));
const position = playerRealm ? getComponentValue(components.Position, playerRealm) : undefined;
position && navigateToHexView(new Position(position));
}
}, [account?.address]);
};
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) => {
if (!isMapView) {
setIsLoadingScreenEnabled(true);
}
showBlankOverlay(false);
setPreviewBuilding(null);
handleUrlChange(position.toMapLocationUrl());
};
};
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
Loading
Loading