Skip to content
Merged
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
95 changes: 68 additions & 27 deletions src/app/game/setup/editor/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { EditorCore, VOIDSTRIKE_EDITOR_CONFIG } from '@/editor';
import { voidstrikeDataProvider } from '@/editor/providers/voidstrike';
import { useGameSetupStore, loadEditorMapDataFromStorage } from '@/store/gameSetupStore';
import { debugInitialization } from '@/utils/debugLogger';
import { MapPreviewModal, type PreviewSettings } from '@/editor/core/MapPreviewModal';
import type { EditorMapData } from '@/editor';
import type { MapListItem } from '@/editor/core/EditorHeader';
import type { MapData } from '@/data/maps/MapTypes';
Expand Down Expand Up @@ -42,6 +43,10 @@ function EditorPageContent() {
// Track current map data for preview
const currentMapDataRef = useRef<EditorMapData | null>(null);

// Preview modal state
const [showPreviewModal, setShowPreviewModal] = useState(false);
const [pendingPreviewData, setPendingPreviewData] = useState<{ editor: EditorMapData; game: MapData } | null>(null);

// Load stored editor map data from IndexedDB (returning from preview)
useEffect(() => {
const loadStoredData = async () => {
Expand Down Expand Up @@ -94,28 +99,54 @@ function EditorPageContent() {
return;
}

debugInitialization.log('Preview map:', gameData);
// Show the preview settings modal
setPendingPreviewData({ editor: data, game: gameData });
setShowPreviewModal(true);
};

// Store custom map in game setup store
const store = useGameSetupStore.getState();
const handleLaunchPreview = useCallback((settings: PreviewSettings) => {
if (!pendingPreviewData) return;

// Store the editor map data so we can restore it when returning from preview
store.setEditorMapData(data);
const { editor: editorData, game: gameData } = pendingPreviewData;

store.setCustomMap(gameData);
debugInitialization.log('Preview map with settings:', settings);

// Configure for preview: 1 human vs 1 AI
const store = useGameSetupStore.getState();

// Reset and configure
store.reset();
store.setCustomMap(gameData); // Re-set after reset
store.setEditorMapData(data); // Re-set after reset
store.setEditorPreview(true); // Mark as editor preview for "Back to Editor" button
store.setFogOfWar(false); // Disable fog for easier testing
store.setStartingResources('insane'); // Start with lots of resources for testing
store.setCustomMap(gameData);
store.setEditorMapData(editorData);
store.setEditorPreview(true);
store.setStartingResources(settings.startingResources);
store.setGameSpeed(settings.gameSpeed);
store.setFogOfWar(settings.fogOfWar);

// Configure player slots: 1 human + (numPlayers - 1) AI
// Slot 1 is already human from reset(). Add extra AI slots as needed.
for (let i = 2; i < settings.numPlayers; i++) {
store.addPlayerSlot();
}

// Set AI difficulty on all AI slots
const currentSlots = useGameSetupStore.getState().playerSlots;
for (const slot of currentSlots) {
if (slot.type === 'ai') {
store.setPlayerSlotAIDifficulty(slot.id, settings.aiDifficulty);
}
}

store.startGame();

// Navigate to game
setShowPreviewModal(false);
setPendingPreviewData(null);
router.push('/game');
};
}, [pendingPreviewData, router]);

const handleCancelPreview = useCallback(() => {
setShowPreviewModal(false);
setPendingPreviewData(null);
}, []);

const handleLoadMap = useCallback((mapId: string) => {
setCurrentMapId(mapId);
Expand All @@ -142,19 +173,29 @@ function EditorPageContent() {
}

return (
<EditorCore
key={editorKey}
config={VOIDSTRIKE_EDITOR_CONFIG}
dataProvider={voidstrikeDataProvider}
mapId={initialMapData ? undefined : currentMapId}
initialMapData={initialMapData}
onCancel={handleCancel}
onPlay={handlePreview}
onChange={handleMapChange}
mapList={mapList}
onLoadMap={handleLoadMap}
onNewMap={handleNewMap}
/>
<>
<EditorCore
key={editorKey}
config={VOIDSTRIKE_EDITOR_CONFIG}
dataProvider={voidstrikeDataProvider}
mapId={initialMapData ? undefined : currentMapId}
initialMapData={initialMapData}
onCancel={handleCancel}
onPlay={handlePreview}
onChange={handleMapChange}
mapList={mapList}
onLoadMap={handleLoadMap}
onNewMap={handleNewMap}
/>

{showPreviewModal && pendingPreviewData && (
<MapPreviewModal
maxPlayers={pendingPreviewData.game.spawns?.length ?? 2}
onLaunch={handleLaunchPreview}
onCancel={handleCancelPreview}
/>
)}
</>
);
}

Expand Down
150 changes: 150 additions & 0 deletions src/editor/core/MapPreviewModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use client';

import { useState, useCallback, useEffect } from 'react';
import { SettingSelect } from '@/components/game-setup';
import type { StartingResources, GameSpeed, AIDifficulty } from '@/store/gameSetupStore';

export interface PreviewSettings {
startingResources: StartingResources;
gameSpeed: GameSpeed;
fogOfWar: boolean;
aiDifficulty: AIDifficulty;
numPlayers: number;
}

interface MapPreviewModalProps {
maxPlayers: number;
onLaunch: (settings: PreviewSettings) => void;
onCancel: () => void;
}

export function MapPreviewModal({ maxPlayers, onLaunch, onCancel }: MapPreviewModalProps) {
const [startingResources, setStartingResources] = useState<StartingResources>('insane');
const [gameSpeed, setGameSpeed] = useState<GameSpeed>('normal');
const [fogOfWar, setFogOfWar] = useState(false);
const [aiDifficulty, setAiDifficulty] = useState<AIDifficulty>('medium');
const [numPlayers, setNumPlayers] = useState(2);

// Clamp player count if maxPlayers changes
useEffect(() => {
if (numPlayers > maxPlayers) {
setNumPlayers(maxPlayers);
}
}, [maxPlayers, numPlayers]);

const handleLaunch = useCallback(() => {
onLaunch({ startingResources, gameSpeed, fogOfWar, aiDifficulty, numPlayers });
}, [startingResources, gameSpeed, fogOfWar, aiDifficulty, numPlayers, onLaunch]);

// Close on Escape
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onCancel();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onCancel]);

// Build player count options from 2 to maxPlayers
const playerOptions = [];
for (let i = 2; i <= maxPlayers; i++) {
playerOptions.push({ value: String(i), label: `${i} Players` });
}

return (
<div className="fixed inset-0 z-[100] flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
onClick={onCancel}
/>

{/* Modal */}
<div className="relative bg-void-950 border border-void-700/50 rounded-xl shadow-2xl shadow-void-900/50 w-full max-w-sm mx-4">
{/* Header */}
<div className="px-5 pt-5 pb-3 border-b border-void-800/50">
<h2 className="font-display text-lg text-white">Preview Settings</h2>
<p className="text-void-400 text-xs mt-0.5">Configure your map test</p>
</div>

{/* Settings */}
<div className="px-5 py-4 space-y-1">
{maxPlayers > 2 && (
<SettingSelect
label="Players"
value={String(numPlayers)}
options={playerOptions}
onChange={(v) => setNumPlayers(parseInt(v, 10))}
/>
)}

<SettingSelect
label="AI Difficulty"
value={aiDifficulty}
options={[
{ value: 'easy', label: 'Easy' },
{ value: 'medium', label: 'Medium' },
{ value: 'hard', label: 'Hard' },
{ value: 'insane', label: 'Insane' },
]}
onChange={setAiDifficulty}
/>

<SettingSelect
label="Starting Resources"
value={startingResources}
options={[
{ value: 'normal', label: 'Normal' },
{ value: 'high', label: 'High' },
{ value: 'insane', label: 'Insane' },
]}
onChange={setStartingResources}
/>

<SettingSelect
label="Game Speed"
value={gameSpeed}
options={[
{ value: 'slower', label: '0.5x' },
{ value: 'normal', label: '1x' },
{ value: 'faster', label: '1.5x' },
{ value: 'fastest', label: '2x' },
]}
onChange={setGameSpeed}
/>

<div className="flex items-center justify-between py-1">
<span className="text-void-300 text-xs">Fog of War</span>
<button
onClick={() => setFogOfWar(!fogOfWar)}
className={`w-10 h-5 rounded-full transition-all duration-200 relative
${fogOfWar ? 'bg-void-500' : 'bg-void-800'}`}
>
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all duration-200
${fogOfWar ? 'left-5' : 'left-0.5'}`}
/>
</button>
</div>
</div>

{/* Footer */}
<div className="px-5 pb-5 pt-2 flex gap-3">
<button
onClick={onCancel}
className="flex-1 px-4 py-2 bg-void-800 hover:bg-void-700 text-void-300 hover:text-white
text-sm rounded-lg border border-void-700 transition-colors"
>
Cancel
</button>
<button
onClick={handleLaunch}
className="flex-1 px-4 py-2 bg-plasma-600 hover:bg-plasma-500 text-white
text-sm rounded-lg border border-plasma-500 transition-colors font-display"
>
Launch Preview
</button>
</div>
</div>
</div>
);
}
Loading