Skip to content
Open
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
.env
.env.local
*.local
12 changes: 12 additions & 0 deletions apps/client/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
VITE_SERVER_URL=ws://localhost:2567

# Minitia chain (set after weave init)
VITE_CHAIN_ID=moveshot-1
VITE_RPC_URL=http://localhost:26657
VITE_REST_URL=http://localhost:1317
VITE_INDEXER_URL=http://localhost:8080
VITE_NATIVE_DENOM=umin
VITE_CONTRACT_ADDRESS=0x1

# InitiaScan explorer (for tx links)
VITE_EXPLORER_URL=https://scan.testnet.initia.xyz/initiation-2
18 changes: 18 additions & 0 deletions apps/client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MoveShot</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=JetBrains+Mono:wght@400;600&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
36 changes: 36 additions & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@arena/client",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@arena/sim": "workspace:*",
"@initia/initia.js": "^1.1.0",
"@initia/initia.proto": "^1.0.5",
"@initia/interwovenkit-react": "^2.5.1",
"@tanstack/react-query": "^5.96.1",
"buffer": "^6.0.3",
"colyseus.js": "^0.15.0",
"nanoid": "^5",
"phaser": "^3.87.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.5.1",
"util": "^0.12.5",
"wagmi": "^2.17.2"
},
"devDependencies": {
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.3",
"vite": "^6.3.2",
"vite-plugin-node-polyfills": "^0.26.0"
}
}
14 changes: 14 additions & 0 deletions apps/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Routes, Route } from 'react-router-dom';
import HomePage from './pages/HomePage';
import PlayPage from './pages/PlayPage';
import GamePage from './pages/GamePage';

export default function App() {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/play" element={<PlayPage />} />
<Route path="/game" element={<GamePage />} />
</Routes>
);
}
83 changes: 83 additions & 0 deletions apps/client/src/bridge/EventBridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Typed event bridge connecting React components and Phaser scenes.
// Both sides import the same singleton instance.
// React → Phaser: wallet state, transaction results
// Phaser → React: match events, UI triggers

export type EventMap = {
// Wallet events (React → Phaser)
WALLET_CONNECTED: { address: string };
WALLET_DISCONNECTED: Record<string, never>;
AUTO_SIGN_ENABLED: { chainId: string };
AUTO_SIGN_DISABLED: { chainId: string };

// Transaction events (React → Phaser / Phaser → React)
TX_PENDING: { functionName: string };
TX_SUCCESS: { hash: string; functionName: string };
TX_ERROR: { error: string; functionName: string };

// Game events (Phaser → React)
MATCH_ENDED: { winner: number | null; scores: [number, number]; walletAddresses?: string[] };
MATCH_RECORDED: { txHash: string };
ROUND_ENDED: { winner: number | null; roundNumber: number };
PLAYER_KILLED: { killerIndex: number; victimIndex: number; weapon: string };

// Cosmetic events
EQUIP_CHANGED: { slot: string; itemId: string };
};

type EventHandler<K extends keyof EventMap> = (data: EventMap[K]) => void;

class TypedEventBridge {
private listeners: Map<string, Set<EventHandler<keyof EventMap>>> = new Map();

emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
const handlers = this.listeners.get(event);
if (handlers) {
handlers.forEach((h) => {
try {
(h as EventHandler<K>)(data);
} catch (err) {
console.error(`[EventBridge] Error in handler for "${event}":`, err);
}
});
}
}

on<K extends keyof EventMap>(event: K, handler: EventHandler<K>): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler as EventHandler<keyof EventMap>);
return () => this.off(event, handler);
}

off<K extends keyof EventMap>(event: K, handler: EventHandler<K>): void {
this.listeners.get(event)?.delete(handler as EventHandler<keyof EventMap>);
}

clear(event?: keyof EventMap): void {
if (event) {
this.listeners.delete(event);
} else {
this.listeners.clear();
}
}
}

export const eventBridge = new TypedEventBridge();

// Typed event name constants for auto-complete
export const BRIDGE_EVENTS = {
WALLET_CONNECTED: 'WALLET_CONNECTED',
WALLET_DISCONNECTED: 'WALLET_DISCONNECTED',
AUTO_SIGN_ENABLED: 'AUTO_SIGN_ENABLED',
AUTO_SIGN_DISABLED: 'AUTO_SIGN_DISABLED',
TX_PENDING: 'TX_PENDING',
TX_SUCCESS: 'TX_SUCCESS',
TX_ERROR: 'TX_ERROR',
MATCH_ENDED: 'MATCH_ENDED',
MATCH_RECORDED: 'MATCH_RECORDED',
ROUND_ENDED: 'ROUND_ENDED',
PLAYER_KILLED: 'PLAYER_KILLED',
EQUIP_CHANGED: 'EQUIP_CHANGED',
} as const satisfies Record<keyof EventMap, keyof EventMap>;
69 changes: 69 additions & 0 deletions apps/client/src/components/wallet/AutoSignToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';

interface Props {
enabled: boolean;
onEnable: () => Promise<boolean>;
onDisable: () => Promise<void>;
}

export function AutoSignToggle({ enabled, onEnable, onDisable }: Props) {
const handleToggle = () => {
if (enabled) {
onDisable();
} else {
onEnable();
}
};

return (
<div style={styles.row}>
<div>
<div style={styles.label}>Seamless Play</div>
<div style={styles.desc}>{enabled ? 'No wallet popups' : 'Enable to skip popups'}</div>
</div>
<button
onClick={handleToggle}
style={styles.toggle(enabled)}
title={enabled ? 'Disable auto-signing' : 'Enable auto-signing'}
>
{enabled ? 'ON' : 'OFF'}
</button>
</div>
);
}

const styles = {
row: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '8px 16px',
gap: 12,
} as React.CSSProperties,

label: {
fontSize: '0.75rem',
color: '#ccc',
fontFamily: 'JetBrains Mono, monospace',
} as React.CSSProperties,

desc: {
fontSize: '0.6rem',
color: '#666',
fontFamily: 'JetBrains Mono, monospace',
marginTop: 2,
} as React.CSSProperties,

toggle: (active: boolean) =>
({
background: active ? '#4ecdc4' : '#333',
color: active ? '#0a0a1a' : '#888',
border: 'none',
padding: '4px 10px',
fontFamily: 'JetBrains Mono, monospace',
fontSize: '0.65rem',
fontWeight: 'bold',
cursor: 'pointer',
flexShrink: 0,
}) as React.CSSProperties,
};
74 changes: 74 additions & 0 deletions apps/client/src/components/wallet/ConnectButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { useState } from 'react';
import { useWallet } from '../../hooks/useWallet';
import { useAutoSign } from '../../hooks/useAutoSign';
import { WalletDropdown } from './WalletDropdown';

export function ConnectButton() {
const { isConnected, truncatedAddress, connect, openWalletInfo } = useWallet();
const { isAutoSignEnabled } = useAutoSign();
const [dropdownOpen, setDropdownOpen] = useState(false);

if (!isConnected) {
return (
<button onClick={connect} style={styles.connectBtn}>
Connect Wallet
</button>
);
}

return (
<div style={{ position: 'relative' }}>
<button
onClick={() => setDropdownOpen((v) => !v)}
style={styles.addressBtn}
title="Wallet options"
>
<span style={styles.dot(isAutoSignEnabled)} />
{truncatedAddress}
</button>

{dropdownOpen && (
<WalletDropdown
onClose={() => setDropdownOpen(false)}
onOpenWallet={openWalletInfo}
/>
)}
</div>
);
}

const styles = {
connectBtn: {
background: '#ffd700',
color: '#0a0a1a',
border: 'none',
padding: '8px 18px',
fontFamily: 'JetBrains Mono, monospace',
fontWeight: 'bold',
fontSize: '0.8rem',
cursor: 'pointer',
letterSpacing: '0.05em',
} as React.CSSProperties,

addressBtn: {
background: 'transparent',
color: '#4ecdc4',
border: '1px solid #4ecdc4',
padding: '8px 16px',
fontFamily: 'JetBrains Mono, monospace',
fontSize: '0.75rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 8,
} as React.CSSProperties,

dot: (active: boolean) =>
({
width: 8,
height: 8,
borderRadius: '50%',
background: active ? '#4ecdc4' : '#666',
flexShrink: 0,
}) as React.CSSProperties,
};
Loading