Skip to content

Commit

Permalink
refactor DevConfig management; streamline state handling and simplify…
Browse files Browse the repository at this point in the history
… access in rendering logic
  • Loading branch information
gtanczyk committed Dec 2, 2024
1 parent f058648 commit 7e8131f
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 140 deletions.
32 changes: 25 additions & 7 deletions games/xmas/src/screens/play/dev/dev-config-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import 'react';
import { useEffect, useState } from 'react';
import styled from 'styled-components';
import { useDevConfig, useDevMode } from './dev-config';
import { devConfig, DevConfig, setDevConfig, getDevMode } from './dev-config';

export function DevConfigPanel() {
const isDevMode = useDevMode();
const [config, updateConfig] = useDevConfig();

// Local state to manage panel configuration
const [config, setConfig] = useState<DevConfig>(devConfig);
const [isDevMode, setIsDevMode] = useState(getDevMode);

// Sync local state with dev config on hash changes
useEffect(() => {
const handleHashChange = () => {
setIsDevMode(getDevMode());
setConfig(devConfig);
};

window.addEventListener('hashchange', handleHashChange);
return () => window.removeEventListener('hashchange', handleHashChange);
}, []);

// Don't render panel if not in dev mode
if (!isDevMode) {
return null;
}

const handleToggle = (key: keyof typeof config) => {
updateConfig({ [key]: !config[key] });
const handleToggle = (key: keyof DevConfig) => {
const newConfig = {
...config,
[key]: !config[key],
};
setDevConfig(newConfig);
setConfig(newConfig);
};

return (
Expand Down
153 changes: 42 additions & 111 deletions games/xmas/src/screens/play/dev/dev-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { useEffect, useState } from 'react';

/**
* Interface defining the development configuration options
*/
Expand Down Expand Up @@ -28,126 +26,59 @@ const DEFAULT_CONFIG: DevConfig = {
};

/**
* Custom event for config updates
* Serializes configuration object to URL hash string
*/
const CONFIG_UPDATE_EVENT = 'devConfigUpdate';

/**
* Event interface for config updates
*/
interface DevConfigUpdateEvent extends CustomEvent {
detail: Partial<DevConfig>;
function serializeConfig(config: DevConfig): string {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(config)) {
params.set(key, String(value));
}
return `#dev?${params.toString()}`;
}

/**
* Class managing the development configuration
* Deserializes configuration from URL hash string
*/
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);
}
function deserializeConfig(hash: string): Partial<DevConfig> {
try {
const match = hash.match(/#dev\?(.*)/);
if (!match) return {};

const params = new URLSearchParams(match[1]);
const config: Partial<DevConfig> = {};
params.forEach((value, key) => {
if (key in DEFAULT_CONFIG) {
config[key as keyof DevConfig] = value === 'true';
}
});
return config;
} catch (e) {
console.warn('Failed to parse dev config from hash:', e);
return {};
}
}

/**
* Get current configuration state
*/
getConfig(): Readonly<DevConfig> {
return this.config;
export const setDevConfig = (config: Partial<DevConfig>) => {
Object.assign(devConfig, config);
const newHash = serializeConfig(devConfig);
if (window.location.hash !== newHash) {
window.history.replaceState(null, '', newHash);
}
};

/**
* Update configuration
*/
updateConfig(updates: Partial<DevConfig>) {
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);
}
export const getDevMode = () => window.location.hash.startsWith('#dev');

/**
* Subscribe to configuration changes
*/
subscribe(listener: (config: DevConfig) => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
export const setDevMode = (enabled: boolean) => {
if (enabled) {
window.location.hash = '#dev';
} else {
window.location.hash = '';
}

/**
* 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<DevConfig>) => void] {
const [config, setConfig] = useState<DevConfig>(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 a singleton instance of the configuration manager
*/
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;
}
export const devConfig: DevConfig = getDevMode()
? { ...DEFAULT_CONFIG, ...deserializeConfig(window.location.hash) }
: { ...DEFAULT_CONFIG };
2 changes: 1 addition & 1 deletion games/xmas/src/screens/play/game-ai/ai-santa-spawner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ 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) {
if (!devConfig.enableAISantas) {
// Reset wave state when AI is disabled
waveState.currentWave = AI_DIFFICULTY.WAVE_1;
return;
Expand Down
9 changes: 3 additions & 6 deletions games/xmas/src/screens/play/game-render/game-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,14 @@ export const renderGame = (ctx: CanvasRenderingContext2D, world: GameWorldState,
// Apply viewport translation
ctx.translate(viewport.x, viewport.y);

// Get current dev configuration
const config = devConfig.getConfig();

// Conditionally render landscape elements based on dev config
if (config.renderMountains || config.renderTrees || config.renderSnowGround) {
if (devConfig.renderMountains || devConfig.renderTrees || devConfig.renderSnowGround) {
// Pass dev config to control individual landscape elements
renderLandscape(ctx, renderState.landscape, renderState.viewport);
}

// Conditionally render snow effects
if (config.renderSnow) {
if (devConfig.renderSnow) {
renderSnow(ctx, renderState);
}

Expand All @@ -43,7 +40,7 @@ export const renderGame = (ctx: CanvasRenderingContext2D, world: GameWorldState,
});

// Conditionally render fire effects
if (config.renderFire) {
if (devConfig.renderFire) {
renderFireballs(ctx, renderState);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ 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();

Expand All @@ -19,17 +17,17 @@ export function renderLandscape(ctx: CanvasRenderingContext2D, state: LandscapeS
renderStars(ctx, state.stars.stars);

// Render mountains (back to front)
if (config.renderMountains) {
if (devConfig.renderMountains) {
renderMountains(ctx, state.mountains.mountains, viewport);
}

// Render snow ground (between mountains and trees)
if (config.renderSnowGround) {
if (devConfig.renderSnowGround) {
renderSnowGrounds(ctx, state.snowGround.grounds, viewport);
}

// Render trees (back to front)
if (config.renderTrees) {
if (devConfig.renderTrees) {
renderTrees(ctx, state.trees.trees, viewport);
}

Expand Down
17 changes: 7 additions & 10 deletions games/xmas/src/screens/play/game-world/game-world-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ function processFireballSantaCollisions(state: GameWorldState) {
});

// Check collisions with AI Santas only if AI is enabled
if (devConfig.getConfig().enableAISantas) {
if (devConfig.enableAISantas) {
state.santas.forEach((santa) => {
if (santa === state.playerSanta) return; // Skip player Santa

fireballs.forEach((fireball) => {
if (checkFireballSantaCollision(fireball, santa)) {
handleFireballSantaCollision(state, fireball, santa);
Expand Down Expand Up @@ -105,14 +105,11 @@ export function updateGameWorld(state: GameWorldState, deltaTime: number) {
// Process fireball-Santa collisions
processFireballSantaCollisions(state);

// Get current dev configuration
const config = devConfig.getConfig();

// Update AI-controlled Santas only if enabled
if (config.enableAISantas) {
if (devConfig.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);
Expand All @@ -121,7 +118,7 @@ export function updateGameWorld(state: GameWorldState, deltaTime: number) {
// Update AI Santa physics
updateSantaPhysics(santa, deltaTime);
updateSantaEnergy(santa, deltaTime);

// Check for automatic gift collection for AI Santas
tryCollectNearbyGifts(state, santa);
});
Expand All @@ -130,11 +127,11 @@ export function updateGameWorld(state: GameWorldState, deltaTime: number) {
updateAISpawner(state, state.waveState);
} else {
// If AI is disabled, ensure only player Santa remains
state.santas = state.santas.filter(santa => santa === state.playerSanta);
state.santas = state.santas.filter((santa) => santa === state.playerSanta);
}

// Update gifts system
updateGifts(state);

return state;
}
}

0 comments on commit 7e8131f

Please sign in to comment.