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
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,51 @@ The application will be available at [http://localhost:3000](http://localhost:30
- `contracts/`: Solidity smart contracts for the Metarchy ecosystem.
- `data/`: JSON storage for games and citizens (mounted as a volume in Docker).

## Update Log & Versions

### v1.6.0 (Strategic Mobility - Current)
- **Teleportation System**: Relocation action cards enable tactical repositioning of actors during Phase 3 Step 3.
- **Resource Exchange**: Strategic resource swapping with opponents using Exchange cards in Phase 3 Step 4.
- **Visual Clarity**: White glow indicators on actors eligible for teleportation with subtle arrow button UI.
- **Step Navigation**: Clear "Start [Action]" buttons guide players through each Phase 3 sub-step.
- **Empty State Messaging**: Prominent "NO ACTION CARDS" notification when hand is empty for current step.

### v1.5.0 (Tactical Refinement)
- **4-Step Action Phase**: Strict sequential workflow for Phase 3: Bidding → Stop Locations → Relocation → Exchange.
- **Bidding Lock**: Material resource bets can only be placed/modified during Step 1; visible but immutable in subsequent steps.
- **Visual Disable State**: Grayscale/desaturated effect applied to disabled locations and all actors within them.
- **Adaptive Card Filtering**: Action cards panel dynamically filters by step type with fallback "no action cards" message.
- **Compact Card View**: Streamlined UI for Relocation and Exchange steps with hidden descriptions.

### v1.4.0 (Analytics & Governance)
- **Live Telemetry**: Real-time auto-updating Admin Dashboard with `Live Link` status.
- **Analytical Logs**: Standardized `[ID] [Time UTC] Action` format for high-precision auditing.
- **Session Intelligence**: Real-time citizen `Online/Offline` status tracking and heartbeats.
- **Strategic Recall**: Added the ability to recall actors and tokens during the distribution phase.

### v1.3.1 (Tactical Polish)
- **AI Emulation**: Realistic "thinking" delays for Viper and Ghost opponent actions.
- **RSP Intelligence**: Smart filtering of used tokens to prevent duplicate deployments.
- **Phase Lockdown**: Adaptive UI that prevents advancing until all tactical units are placed.

### v1.3.0 (Immersion & UX)
- **Navigable Map**: High-performance "Drag-to-Move" map panning.
- **Action Zoom**: Dynamic 1.6x zoom scaling during critical action phases.
- **Room Synthesis**: Random thematic name generator for unique game sessions.

### v1.2.0 (Identity & Persistence)
- **Decentralized Identity**: Integrated Citizen ID creation and MetaMask session management.
- **Global Lobby**: Multi-room game discovery with waiting room simulations.
- **FileSystem Persistence**: Robust JSON-based storage for long-running game sessions.

### v1.1.0 (Networking Core)
- **Dynamic Routing**: Shifted to UUID-based session addressing at `/game/board/[id]`.
- **API Foundation**: Initial REST architecture for game state and citizen records.

### v1.0.0 (Neural Genesis)
- **Core Engine**: Hex-based map and four initial Actor classes (Politician, Robot, Scientist, Artist).
- **HUD System**: Early placement markers and resource tracking.

## License

This project is licensed under the MIT License.
3 changes: 3 additions & 0 deletions frontend/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"css.lint.unknownAtRules": "ignore"
}
53 changes: 53 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Metarchy Frontend - Development Status

**Last Updated:** February 2, 2026

## 🚀 Recent Updates (Session Handover)

We have successfully implemented the **Conflict Phase (Phase 4)** with high-fidelity visuals and refined game logic.

### ✅ Completed Features
1. **Conflict Phase UI**
- **Conflicts Sidebar**: A persistent, collapsible sidebar that lists active conflicts. Visually integrates the Phase 2 "Actor Orb" (Player) and Phase 3 "HUD Marker" (Opponent) styles.
- **Resolution Arena (`ConflictResolutionView`)**: A premium close-up modal featuring:
- Full-screen location concept art backdrops.
- Full-body actor illustrations with team tinting.
- Animated reveal sequences (Entrance, Reveal, Results) using `framer-motion`.

2. **Logic Refinements**
- **Strict Role-Based Conflicts**: Conflicts now **only** occur between actors of the **same role** (e.g., Politician vs Politician). Matches based on RSP tokens alone (e.g., Rock vs Rock) are ignored.
- **Multi-Conflict Locations**: A single location can now host multiple distinct conflicts (e.g., a Robot battle and a Scientist battle at the same "Square").
- **Filtering**: Resolved conflicts effectively disappear from the active list to prevent infinite loops.

3. **Bug Fixes**
- Fixed missing Player Avatars/Names in the Sidebar by correctly mapping to `MY_ACTORS`.
- Fixed "Random Conflicts" by ensuring `actorType` is recorded during placement in `GameBoardPage.tsx`.
- Hidden opponent Bid Icons in the Sidebar (requested change).

---

## 🛠 Key Technical Notes for Next Session

* **Conflict Detection Logic**:
- Located in `GameBoardPage.tsx` -> `activeConflicts` useMemo.
- It iterates through placed actors and explicitly checks `a.actorType === playerActorRaw.actorType`.
- **Important**: The `placedActors` state has been patched to include `actorType` during `handleRSPSelect`. Ensure any *new* placement mechanics also include this property.

* **Visual Assets**:
- Location backgrounds are pulled from `/public/Locations closeups/`.
- Actor images are mapped via `MY_ACTORS` or `PLAYERS` arrays.

* **State Management**:
- `activeConflictLocId`: Tracks which specific conflict is currently being viewed in the Arena. It uses a composite ID format: `${locId}_${actorType}` (e.g., `square_politician`).
- `resolvedConflicts`: Array of composite IDs that have been finished.

## 📋 Next Steps

1. **Phase 5 (Resolution / End Game)**:
- Implement the final scoring or transition after conflicts are resolved.
- Handle the "Double Prize" logic verification fully (currently simulated).
2. **Sound Effects**:
- Add SFX for "VS" slam, Card Reveal, and Victory/Defeat states.
3. **Edge Cases**:
- Verify "Draw" mechanics with diverse bid types (Star/Recycle).
- detailed testing of the "Restart" flow for Energy bids.
14 changes: 11 additions & 3 deletions frontend/app/admin/citizens/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export default function AdminCitizensPage() {

useEffect(() => {
fetchCitizens();
const interval = setInterval(fetchCitizens, 5000);
return () => clearInterval(interval);
}, []);

const handleDelete = async (id: string) => {
Expand Down Expand Up @@ -74,9 +76,15 @@ export default function AdminCitizensPage() {
<h2 className="text-4xl font-bold font-rajdhani text-[#d4af37] uppercase tracking-wider">Citizen Management</h2>
<p className="text-gray-400 font-medium tracking-widest mt-2 uppercase text-[10px]">Manage registered network participants</p>
</div>
<div className="text-right">
<span className="text-gray-500 text-[10px] uppercase tracking-widest block">Authorized Residents</span>
<span className="text-3xl font-bold font-rajdhani text-white">{citizens.length}</span>
<div className="flex items-center gap-8">
<div className="flex items-center gap-2 bg-[#d4af37]/5 border border-[#d4af37]/20 px-3 py-1.5 rounded-lg shadow-[0_0_15px_rgba(212,175,55,0.05)]">
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-[10px] text-green-400 font-bold uppercase tracking-[0.2em] font-rajdhani">Resident Uplink Live</span>
</div>
<div className="text-right">
<span className="text-gray-500 text-[10px] uppercase tracking-widest block font-rajdhani">Authorized Residents</span>
<span className="text-3xl font-bold font-rajdhani text-white">{citizens.length}</span>
</div>
</div>
</header>

Expand Down
97 changes: 85 additions & 12 deletions frontend/app/admin/games/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import React, { useState, useEffect } from 'react';
import { Game } from '@/lib/types';
import { Trash2, Edit2, AlertCircle, CheckCircle2, Clock } from 'lucide-react';
import { Trash2, Edit2, AlertCircle, CheckCircle2, Clock, ScrollText, X } from 'lucide-react';

export default function AdminGamesPage() {
const [games, setGames] = useState<Game[]>([]);
const [loading, setLoading] = useState(true);
const [selectedGameId, setSelectedGameId] = useState<string | null>(null);
const [message, setMessage] = useState<{ text: string, type: 'success' | 'error' } | null>(null);

const fetchGames = async () => {
Expand All @@ -25,8 +26,13 @@ export default function AdminGamesPage() {

useEffect(() => {
fetchGames();
// Auto-refresh every 3 seconds for live monitoring
const interval = setInterval(fetchGames, 3000);
return () => clearInterval(interval);
}, []);

const selectedGame = games.find(g => g.id === selectedGameId);

const handleDelete = async (id: string) => {
if (!confirm("Are you sure you want to delete this game? This action cannot be undone.")) return;

Expand Down Expand Up @@ -82,9 +88,15 @@ export default function AdminGamesPage() {
<h2 className="text-4xl font-bold font-rajdhani text-[#d4af37] uppercase tracking-wider">Game Management</h2>
<p className="text-gray-400 font-medium tracking-widest mt-2 uppercase text-[10px]">Oversee and control active game sessions</p>
</div>
<div className="text-right">
<span className="text-gray-500 text-[10px] uppercase tracking-widest block">Total Games</span>
<span className="text-3xl font-bold font-rajdhani text-white">{games.length}</span>
<div className="flex items-center gap-8">
<div className="flex items-center gap-2 bg-[#d4af37]/5 border border-[#d4af37]/20 px-3 py-1.5 rounded-lg shadow-[0_0_15px_rgba(212,175,55,0.05)]">
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-[10px] text-green-400 font-bold uppercase tracking-[0.2em] font-rajdhani">Live Neural Link Established</span>
</div>
<div className="text-right">
<span className="text-gray-500 text-[10px] uppercase tracking-widest block font-rajdhani">Total Nodes</span>
<span className="text-3xl font-bold font-rajdhani text-white">{games.length}</span>
</div>
</div>
</header>

Expand All @@ -96,7 +108,7 @@ export default function AdminGamesPage() {
</div>
)}

<div className="bg-[#0d0d12]/40 backdrop-blur-md border border-white/5 rounded-2xl overflow-hidden shadow-2xl">
<div className="bg-[#0d0d12]/40 backdrop-blur-md border border-white/5 rounded-2xl overflow-hidden shadow-2xl transition-all duration-500 hover:border-[#d4af37]/20">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-white/5 text-[#d4af37] text-[10px] uppercase tracking-[0.2em] font-bold font-rajdhani">
Expand Down Expand Up @@ -158,20 +170,81 @@ export default function AdminGamesPage() {
</span>
</td>
<td className="px-8 py-6 text-right">
<button
onClick={() => handleDelete(game.id)}
className="p-3 text-gray-500 hover:text-red-500 hover:bg-red-500/10 rounded-xl transition-all"
title="Terminate Game"
>
<Trash2 size={20} />
</button>
<div className="flex items-center justify-end gap-2">
<button
onClick={() => setSelectedGameId(game.id)}
className={`p-3 rounded-xl transition-all ${selectedGameId === game.id ? 'bg-[#d4af37]/20 text-[#d4af37]' : 'text-[#d4af37] hover:bg-[#d4af37]/10'}`}
title="View Logs"
>
<ScrollText size={20} />
</button>
<button
onClick={() => handleDelete(game.id)}
className="p-3 text-gray-500 hover:text-red-500 hover:bg-red-500/10 rounded-xl transition-all"
title="Terminate Game"
>
<Trash2 size={20} />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>

{/* Log Viewer Modal */}
{selectedGameId && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-[#0d0d12] border border-[#d4af37]/30 rounded-2xl w-full max-w-2xl shadow-2xl overflow-hidden flex flex-col max-h-[80vh]">
<header className="px-8 py-6 border-b border-[#d4af37]/10 flex justify-between items-center bg-[#d4af37]/5">
<div className="flex items-center gap-4">
<div>
<h3 className="text-2xl font-bold font-rajdhani text-[#d4af37] uppercase tracking-wider">Game Command History</h3>
<p className="text-gray-500 font-mono text-[10px] tracking-tight">{selectedGameId}</p>
</div>
<div className="flex items-center gap-2 bg-green-500/10 border border-green-500/20 px-2 py-1 rounded shadow-[0_0_10px_rgba(34,197,94,0.1)]">
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
<span className="text-[8px] text-green-400 font-bold uppercase tracking-widest font-rajdhani">Live Link</span>
</div>
</div>
<button
onClick={() => setSelectedGameId(null)}
className="p-2 hover:bg-white/5 rounded-full transition-colors"
>
<X size={24} className="text-gray-400 hover:text-white" />
</button>
</header>

<div className="flex-1 overflow-y-auto p-8 space-y-4 bg-black/20 font-rajdhani">
{selectedGame && selectedGame.logs && selectedGame.logs.length > 0 ? (
[...selectedGame.logs].reverse().map((log, i) => (
<div key={i} className="flex gap-4 group animate-in slide-in-from-left-2 duration-300">
<span className="text-[#d4af37]/30 text-[10px] font-mono mt-1 shrink-0">#{String(selectedGame.logs.length - i).padStart(3, '0')}</span>
<p className="text-gray-300 font-medium tracking-wide leading-relaxed border-l border-white/5 pl-4 group-hover:border-[#d4af37]/30 transition-colors">
{log}
</p>
</div>
))
) : (
<div className="text-center py-20">
<p className="text-gray-600 uppercase tracking-[0.2em] font-rajdhani">No telemetry data recorded for this session</p>
</div>
)}
</div>

<footer className="px-8 py-4 border-t border-[#d4af37]/10 flex justify-end bg-black/40">
<button
onClick={() => setSelectedGameId(null)}
className="px-6 py-2 bg-[#d4af37]/10 border border-[#d4af37]/20 text-[#d4af37] font-bold font-rajdhani uppercase tracking-widest text-xs rounded hover:bg-[#d4af37]/20 transition-all active:scale-95"
>
Close Uplink
</button>
</footer>
</div>
</div>
)}
</div>
);
}
34 changes: 33 additions & 1 deletion frontend/app/api/citizen/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ interface Citizen {
citizenId: string;
joinedAt: string;
avatar?: string;
isOnline: boolean;
lastActive: string;
}

// Helper to ensure DB exists
Expand Down Expand Up @@ -90,7 +92,9 @@ export async function POST(request: NextRequest) {
name,
citizenId: newId,
joinedAt: new Date().toISOString(),
avatar: "/avatars/golden_avatar.png"
avatar: "/avatars/golden_avatar.png",
isOnline: true,
lastActive: new Date().toISOString()
};

citizens.push(newCitizen);
Expand All @@ -101,3 +105,31 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}

// PUT: Update citizen status (online/offline)
export async function PUT(request: NextRequest) {
try {
const body = await request.json();
const { address, status } = body;

if (!address) {
return NextResponse.json({ error: 'Address required' }, { status: 400 });
}

const citizens = readDB();
const index = citizens.findIndex(c => c.address.toLowerCase() === address.toLowerCase());

if (index === -1) {
return NextResponse.json({ error: 'Citizen not found' }, { status: 404 });
}

citizens[index].isOnline = status === 'online';
citizens[index].lastActive = new Date().toISOString();

writeDB(citizens);

return NextResponse.json(citizens[index]);
} catch (e) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
8 changes: 2 additions & 6 deletions frontend/app/api/games/[id]/ready/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,8 @@ export async function POST(request: NextRequest, { params }: { params: { id: str
let updates: any = { players: game.players };

if (allReady && isLobbyFull && game.status === 'waiting') {
updates.status = 'starting';
updates.startTime = Date.now() + 10000; // 10 seconds from now
} else if ((!allReady || !isLobbyFull) && game.status === 'starting') {
// Cancel countdown if someone un-readies
updates.status = 'waiting';
updates.startTime = undefined;
updates.status = 'playing';
updates.startTime = Date.now();
}

const updatedGame = gameService.update(params.id, updates);
Expand Down
Loading