diff --git a/games/xmas/src/screens/play/dev/dev-config-panel.tsx b/games/xmas/src/screens/play/dev/dev-config-panel.tsx new file mode 100644 index 00000000..40126bea --- /dev/null +++ b/games/xmas/src/screens/play/dev/dev-config-panel.tsx @@ -0,0 +1,156 @@ +import 'react'; +import styled from 'styled-components'; +import { useDevConfig, useDevMode } from './dev-config'; + +export function DevConfigPanel() { + const isDevMode = useDevMode(); + const [config, updateConfig] = useDevConfig(); + + if (!isDevMode) { + return null; + } + + const handleToggle = (key: keyof typeof config) => { + updateConfig({ [key]: !config[key] }); + }; + + return ( + + Dev Configuration + +
+ Game Features + + handleToggle('enableAISantas')} + /> + AI Santas + +
+ +
+ Rendering + + handleToggle('renderSnow')} /> + Snow Effects + + + handleToggle('renderFire')} /> + Fire Effects + + + handleToggle('renderTrees')} /> + Trees + + + handleToggle('renderMountains')} + /> + Mountains + + + handleToggle('renderSnowGround')} + /> + Snow Ground + +
+
+ ); +} + +const Panel = styled.div` + position: fixed; + top: 10px; + right: 10px; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 15px; + border-radius: 8px; + min-width: 200px; + z-index: 1000; + font-family: 'Arial', sans-serif; + backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +`; + +const Title = styled.h3` + margin: 0 0 15px 0; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 1px; + color: #fff; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + padding-bottom: 8px; +`; + +const Section = styled.div` + margin-bottom: 15px; + + &:last-child { + margin-bottom: 0; + } +`; + +const SectionTitle = styled.h4` + margin: 0 0 8px 0; + font-size: 12px; + color: rgba(255, 255, 255, 0.7); + font-weight: normal; +`; + +const ToggleContainer = styled.label` + display: flex; + align-items: center; + margin-bottom: 8px; + cursor: pointer; + font-size: 12px; + + &:last-child { + margin-bottom: 0; + } + + &:hover { + color: #4a9eff; + } +`; + +const ToggleInput = styled.input` + margin-right: 8px; + cursor: pointer; + + /* Custom checkbox styling */ + appearance: none; + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 3px; + background: transparent; + position: relative; + + &:checked { + background: #4a9eff; + border-color: #4a9eff; + } + + &:checked::after { + content: '✓'; + position: absolute; + color: white; + font-size: 12px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + &:hover { + border-color: #4a9eff; + } +`; diff --git a/games/xmas/src/screens/play/dev/dev-config.ts b/games/xmas/src/screens/play/dev/dev-config.ts new file mode 100644 index 00000000..5fba36bf --- /dev/null +++ b/games/xmas/src/screens/play/dev/dev-config.ts @@ -0,0 +1,153 @@ +import { useEffect, useState } from 'react'; + +/** + * Interface defining the development configuration options + */ +export interface DevConfig { + // AI Features + enableAISantas: boolean; + + // Rendering Features + renderSnow: boolean; + renderFire: boolean; + renderTrees: boolean; + renderMountains: boolean; + renderSnowGround: boolean; +} + +/** + * Default configuration values + */ +const DEFAULT_CONFIG: DevConfig = { + enableAISantas: true, + renderSnow: true, + renderFire: true, + renderTrees: true, + renderMountains: true, + renderSnowGround: true, +}; + +/** + * Custom event for config updates + */ +const CONFIG_UPDATE_EVENT = 'devConfigUpdate'; + +/** + * Event interface for config updates + */ +interface DevConfigUpdateEvent extends CustomEvent { + detail: Partial; +} + +/** + * Class managing the development configuration + */ +class DevConfigManager { + private config: DevConfig; + private listeners: Set<(config: DevConfig) => void>; + + constructor() { + this.config = { ...DEFAULT_CONFIG }; + this.listeners = new Set(); + this.initHashListener(); + } + + /** + * Initialize hash change listener for dev mode + */ + private initHashListener() { + window.addEventListener('hashchange', this.checkDevMode.bind(this)); + this.checkDevMode(); // Initial check + } + + /** + * Check if dev mode is enabled via URL hash + */ + private checkDevMode() { + const isDevMode = window.location.hash.includes('#dev'); + if (!isDevMode) { + // Reset to default config when dev mode is disabled + this.updateConfig(DEFAULT_CONFIG); + } + } + + /** + * Get current configuration state + */ + getConfig(): Readonly { + return { ...this.config }; + } + + /** + * Update configuration + */ + updateConfig(updates: Partial) { + this.config = { + ...this.config, + ...updates, + }; + + // Notify all listeners + this.listeners.forEach((listener) => listener(this.getConfig())); + + // Dispatch custom event + const event = new CustomEvent(CONFIG_UPDATE_EVENT, { + detail: updates, + }) as DevConfigUpdateEvent; + window.dispatchEvent(event); + } + + /** + * Subscribe to configuration changes + */ + subscribe(listener: (config: DevConfig) => void): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + /** + * Check if dev mode is currently active + */ + isDevMode(): boolean { + return window.location.hash.includes('#dev'); + } +} + +// Create singleton instance +export const devConfig = new DevConfigManager(); + +/** + * React hook for accessing dev configuration + */ +export function useDevConfig(): [DevConfig, (updates: Partial) => void] { + const [config, setConfig] = useState(devConfig.getConfig()); + + useEffect(() => { + // Subscribe to config updates + const unsubscribe = devConfig.subscribe((newConfig) => { + setConfig(newConfig); + }); + + return unsubscribe; + }, []); + + return [config, (updates) => devConfig.updateConfig(updates)]; +} + +/** + * React hook for checking dev mode status + */ +export function useDevMode(): boolean { + const [isDevMode, setIsDevMode] = useState(devConfig.isDevMode()); + + useEffect(() => { + const handleHashChange = () => { + setIsDevMode(devConfig.isDevMode()); + }; + + window.addEventListener('hashchange', handleHashChange); + return () => window.removeEventListener('hashchange', handleHashChange); + }, []); + + return isDevMode; +} \ No newline at end of file diff --git a/games/xmas/src/screens/play/game-ai/ai-santa-spawner.ts b/games/xmas/src/screens/play/game-ai/ai-santa-spawner.ts index cd06bf2a..2483e97c 100644 --- a/games/xmas/src/screens/play/game-ai/ai-santa-spawner.ts +++ b/games/xmas/src/screens/play/game-ai/ai-santa-spawner.ts @@ -3,6 +3,7 @@ import { createSanta } from '../game-world/game-world-manipulate'; import { AI_CONFIG, AI_DIFFICULTY, AISanta, WaveState } from './ai-santa-types'; import { initializeAIState } from './ai-santa-decision'; import { GAME_WORLD_HEIGHT, GAME_WORLD_WIDTH } from '../game-world/game-world-consts'; +import { devConfig } from '../dev/dev-config'; /** * Calculate a safe spawn position for AI Santa @@ -93,6 +94,12 @@ function spawnWaveSantas(gameState: GameWorldState, waveState: WaveState): void * Main update function for AI Santa spawning system */ export function updateAISpawner(gameState: GameWorldState, waveState: WaveState): void { + if (!devConfig.getConfig().enableAISantas) { + // Reset wave state when AI is disabled + waveState.currentWave = AI_DIFFICULTY.WAVE_1; + return; + } + // Check if current wave is completed if (areAllSantasEliminated(gameState)) { if (waveState.nextSpawnTime === null) { diff --git a/games/xmas/src/screens/play/game-render/game-renderer.ts b/games/xmas/src/screens/play/game-render/game-renderer.ts index 6bb8fdda..bf0e84b7 100644 --- a/games/xmas/src/screens/play/game-render/game-renderer.ts +++ b/games/xmas/src/screens/play/game-render/game-renderer.ts @@ -6,6 +6,7 @@ import { renderSanta } from './santa-renderer'; import { renderFireballs } from './fireball-renderer'; import { renderSky } from './landscape/sky/sky-renderer'; import { renderGifts } from './gift-renderer'; +import { devConfig } from '../dev/dev-config'; export const renderGame = (ctx: CanvasRenderingContext2D, world: GameWorldState, renderState: RenderState) => { const { viewport } = renderState; @@ -19,20 +20,33 @@ export const renderGame = (ctx: CanvasRenderingContext2D, world: GameWorldState, // Apply viewport translation ctx.translate(viewport.x, viewport.y); - // Render all game elements with the applied translation - renderLandscape(ctx, renderState.landscape, renderState.viewport); - renderSnow(ctx, renderState); + // Get current dev configuration + const config = devConfig.getConfig(); + + // Conditionally render landscape elements based on dev config + if (config.renderMountains || config.renderTrees || config.renderSnowGround) { + // Pass dev config to control individual landscape elements + renderLandscape(ctx, renderState.landscape, renderState.viewport); + } + + // Conditionally render snow effects + if (config.renderSnow) { + renderSnow(ctx, renderState); + } // Render gifts before Santas for proper depth renderGifts(ctx, world.gifts, world.time); - // Render all santas + // Always render Santas as they're essential game elements world.santas.forEach((santa) => { renderSanta(ctx, santa, world.time); }); - renderFireballs(ctx, renderState); + // Conditionally render fire effects + if (config.renderFire) { + renderFireballs(ctx, renderState); + } // Restore the context state ctx.restore(); -}; \ No newline at end of file +}; diff --git a/games/xmas/src/screens/play/game-render/landscape/landscape-renderer.ts b/games/xmas/src/screens/play/game-render/landscape/landscape-renderer.ts index d5197a7c..c52e2b1b 100644 --- a/games/xmas/src/screens/play/game-render/landscape/landscape-renderer.ts +++ b/games/xmas/src/screens/play/game-render/landscape/landscape-renderer.ts @@ -4,8 +4,11 @@ import { renderMountains } from './mountain/mountain-renderer'; import { renderTrees } from './tree/tree-renderer'; import { renderSnowGrounds } from './snow-ground/snow-ground-renderer'; import { ViewportState } from '../render-state'; +import { devConfig } from '../../dev/dev-config'; export function renderLandscape(ctx: CanvasRenderingContext2D, state: LandscapeState, viewport: ViewportState): void { + const config = devConfig.getConfig(); + // Save the current context state ctx.save(); @@ -16,14 +19,20 @@ export function renderLandscape(ctx: CanvasRenderingContext2D, state: LandscapeS renderStars(ctx, state.stars.stars); // Render mountains (back to front) - renderMountains(ctx, state.mountains.mountains, viewport); + if (config.renderMountains) { + renderMountains(ctx, state.mountains.mountains, viewport); + } // Render snow ground (between mountains and trees) - renderSnowGrounds(ctx, state.snowGround.grounds, viewport); + if (config.renderSnowGround) { + renderSnowGrounds(ctx, state.snowGround.grounds, viewport); + } // Render trees (back to front) - renderTrees(ctx, state.trees.trees, viewport); + if (config.renderTrees) { + renderTrees(ctx, state.trees.trees, viewport); + } // Restore the context state ctx.restore(); -} \ No newline at end of file +} diff --git a/games/xmas/src/screens/play/game-world/game-world-update.ts b/games/xmas/src/screens/play/game-world/game-world-update.ts index ce77f68e..6012b036 100644 --- a/games/xmas/src/screens/play/game-world/game-world-update.ts +++ b/games/xmas/src/screens/play/game-world/game-world-update.ts @@ -12,6 +12,7 @@ import { } from './game-world-manipulate'; import { dropGift } from './game-world-gift-manipulate'; import { updateGifts } from './game-world-update-gifts'; +import { devConfig } from '../dev/dev-config'; /** * Check and handle collisions between fireballs and all Santas @@ -34,21 +35,25 @@ function processFireballSantaCollisions(state: GameWorldState) { } }); - // Check collisions with AI Santas - state.santas.forEach((santa) => { - fireballs.forEach((fireball) => { - if (checkFireballSantaCollision(fireball, santa)) { - handleFireballSantaCollision(state, fireball, santa); - // Drop gift if Santa was carrying one - if (santa.carriedGift) { - const gift = state.gifts.find((g) => g.id === santa.carriedGift); - if (gift) { - dropGift(state, santa.id); + // Check collisions with AI Santas only if AI is enabled + if (devConfig.getConfig().enableAISantas) { + state.santas.forEach((santa) => { + if (santa === state.playerSanta) return; // Skip player Santa + + fireballs.forEach((fireball) => { + if (checkFireballSantaCollision(fireball, santa)) { + handleFireballSantaCollision(state, fireball, santa); + // Drop gift if Santa was carrying one + if (santa.carriedGift) { + const gift = state.gifts.find((g) => g.id === santa.carriedGift); + if (gift) { + dropGift(state, santa.id); + } } } - } + }); }); - }); + } } /** @@ -100,26 +105,36 @@ export function updateGameWorld(state: GameWorldState, deltaTime: number) { // Process fireball-Santa collisions processFireballSantaCollisions(state); - // Update AI-controlled Santas - state.santas.forEach((santa) => { - if ('ai' in santa) { - // Handle AI Santa updates including fireball creation - handleAISantaFireball(state, santa as AISanta); - } + // Get current dev configuration + const config = devConfig.getConfig(); + + // Update AI-controlled Santas only if enabled + if (config.enableAISantas) { + state.santas.forEach((santa) => { + if (santa === state.playerSanta) return; // Skip player Santa + + if ('ai' in santa) { + // Handle AI Santa updates including fireball creation + handleAISantaFireball(state, santa as AISanta); + } - // Update AI Santa physics - updateSantaPhysics(santa, deltaTime); - updateSantaEnergy(santa, deltaTime); - - // Check for automatic gift collection for AI Santas - tryCollectNearbyGifts(state, santa); - }); + // Update AI Santa physics + updateSantaPhysics(santa, deltaTime); + updateSantaEnergy(santa, deltaTime); + + // Check for automatic gift collection for AI Santas + tryCollectNearbyGifts(state, santa); + }); + + // Update AI spawning system only if AI is enabled + updateAISpawner(state, state.waveState); + } else { + // If AI is disabled, ensure only player Santa remains + state.santas = state.santas.filter(santa => santa === state.playerSanta); + } // Update gifts system updateGifts(state); - // Update AI spawning system - updateAISpawner(state, state.waveState); - return state; -} +} \ No newline at end of file diff --git a/games/xmas/src/screens/play/play-screen.tsx b/games/xmas/src/screens/play/play-screen.tsx index 3e247325..5f5b88b1 100644 --- a/games/xmas/src/screens/play/play-screen.tsx +++ b/games/xmas/src/screens/play/play-screen.tsx @@ -7,6 +7,7 @@ import { createRenderState, RenderState, updateRenderState } from './game-render import { createSanta } from './game-world/game-world-manipulate'; import { GAME_WORLD_HEIGHT, GAME_WORLD_WIDTH } from './game-world/game-world-consts'; import { initializeWaveState } from './game-ai/ai-santa-spawner'; +import { DevConfigPanel } from './dev/dev-config-panel'; // Game configuration const GAME_CONFIG = { @@ -85,5 +86,10 @@ export function PlayScreen() { } }); - return ; + return ( + <> + + + + ); }