-
Notifications
You must be signed in to change notification settings - Fork 670
Night Mode #2364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Night Mode #2364
Changes from all commits
9eee198
1159b47
8a0d5c7
927a76f
fb0bdd6
dfb8d17
b48a247
ee8bb56
d39731f
02d27cf
45437d2
b081f3b
d688bb0
195ad4d
c0b6d30
f2d3bac
3e635d1
3ce8b77
0ca18d1
865a64a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| reviews: | ||
| profile: assertive |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| import { Cell, UnitType } from "../../../core/game/Game"; | ||
| import { GameView } from "../../../core/game/GameView"; | ||
| import { UserSettings } from "../../../core/game/UserSettings"; | ||
| import { TransformHandler } from "../TransformHandler"; | ||
| import { Layer } from "./Layer"; | ||
|
|
||
| export class NightModeLayer implements Layer { | ||
| private darkenColor: [number, number, number] = [0, 0, 0]; | ||
| private darkenAlpha: number = 0.8; | ||
| private flashlightRadius: number = 50; | ||
| private userSettingsInstance = new UserSettings(); | ||
| private mouseX: number = 0; | ||
| private mouseY: number = 0; | ||
| private maxCityLightLevel: number = 15; | ||
|
|
||
| private mouseMoveHandler = (e: MouseEvent) => this.handleMouseMove(e); | ||
|
|
||
| private handleMouseMove(event: MouseEvent) { | ||
| const rect = this.transformHandler.boundingRect(); | ||
| this.mouseX = event.clientX - rect.left; | ||
| this.mouseY = event.clientY - rect.top; | ||
| } | ||
|
|
||
| init(): void {} | ||
| tick(): void {} | ||
| redraw(): void {} | ||
|
|
||
| constructor( | ||
| private game: GameView | null, | ||
| private transformHandler: TransformHandler, | ||
| ) { | ||
| document.addEventListener("mousemove", this.mouseMoveHandler); | ||
| } | ||
|
|
||
| // New method to set game reference after construction | ||
|
|
||
| renderLayer(context: CanvasRenderingContext2D): void { | ||
| if (!this.userSettingsInstance.nightMode()) return; | ||
|
|
||
| const width = this.transformHandler.width(); | ||
| const height = this.transformHandler.boundingRect().height; | ||
| const cellSize = this.transformHandler.scale; | ||
|
|
||
| // Fill the entire screen with dark | ||
| context.fillStyle = `rgba(${this.darkenColor[0]}, ${this.darkenColor[1]}, ${this.darkenColor[2]}, ${this.darkenAlpha})`; | ||
| context.fillRect(0, 0, width, height); | ||
|
|
||
| // ===== NEW: Render city lights ===== | ||
| if (this.game) { | ||
| this.renderCityLights(context, cellSize); | ||
| } | ||
|
|
||
| // Render flashlight effect around mouse | ||
| this.renderFlashlight(context, width, height, cellSize); | ||
| } | ||
|
|
||
| /** | ||
| * Renders illumination for all cities on the map. | ||
| * Creates a glow effect similar to satellite images of Earth at night. | ||
| */ | ||
|
|
||
| private glowBitmaps: Map<number, ImageBitmap> = new Map(); | ||
| //lazy generator & cache for little lag | ||
| private async getGlowBitmap( | ||
| level: number, | ||
| cellSize: number, | ||
| ): Promise<ImageBitmap> { | ||
| const cappedLevel = this.maxCityLightLevel; | ||
|
|
||
| // Check cache first | ||
| const cached = this.glowBitmaps.get(cappedLevel); | ||
| if (cached) return cached; | ||
|
|
||
| // Not in cache → generate, store, and return | ||
| const bitmap = await this.createGlowBitmap(cappedLevel, cellSize); | ||
| this.glowBitmaps.set(cappedLevel, bitmap); | ||
|
|
||
| return bitmap; | ||
| } | ||
|
Comment on lines
+64
to
+79
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The level parameter is ignored—glow bitmaps are only cached for maxCityLightLevel. The If you want uniform city lights, remove the unused parameter. If you want level-based scaling, use the actual level (capped to max): private async getGlowBitmap(
level: number,
cellSize: number,
): Promise<ImageBitmap> {
- const cappedLevel = this.maxCityLightLevel;
+ const cappedLevel = Math.min(level, this.maxCityLightLevel);
// Check cache first
const cached = this.glowBitmaps.get(cappedLevel);This way, level-1 cities get smaller glows than level-15 cities, and the cache stores one bitmap per level (0-15), not just one bitmap for all cities. 🤖 Prompt for AI Agents |
||
|
|
||
| private async createGlowBitmap( | ||
| level: number, | ||
| cellSize: number, | ||
| ): Promise<ImageBitmap> { | ||
| const lightRadius = (10 + level * 2) * cellSize; | ||
| const size = lightRadius * 2; | ||
|
|
||
| // Use OffscreenCanvas for faster bitmap creation | ||
| const offscreen = new OffscreenCanvas(size, size); | ||
| const ctx = offscreen.getContext("2d")!; | ||
|
|
||
| // Glow gradient (you can customize color stops here) | ||
| const gradient = ctx.createRadialGradient( | ||
| lightRadius, | ||
| lightRadius, | ||
| 0, | ||
| lightRadius, | ||
| lightRadius, | ||
| lightRadius, | ||
| ); | ||
| gradient.addColorStop(0, "rgba(255, 230, 120, 0.8)"); | ||
| gradient.addColorStop(0.4, "rgba(255, 180, 80, 0.4)"); | ||
| gradient.addColorStop(1, "rgba(255, 140, 40, 0)"); | ||
|
|
||
| ctx.fillStyle = gradient; | ||
| ctx.beginPath(); | ||
| ctx.arc(lightRadius, lightRadius, lightRadius, 0, Math.PI * 2); | ||
| ctx.fill(); | ||
|
|
||
| return await createImageBitmap(offscreen); | ||
| } | ||
|
|
||
| private renderCityLights( | ||
| context: CanvasRenderingContext2D, | ||
| cellSize: number, | ||
| ): void { | ||
| // Get all cities in the game | ||
| const cities = this.game!.units(UnitType.City); | ||
|
|
||
| for (const city of cities) { | ||
| // Get city position | ||
| const tileRef = city.tile(); | ||
| const cityX = this.game!.x(tileRef); | ||
| const cityY = this.game!.y(tileRef); | ||
|
|
||
| // Convert tile coordinates to screen coordinates | ||
| const screenPos = this.transformHandler.worldToScreenCoordinates( | ||
| new Cell(cityX, cityY), | ||
| ); | ||
| const screenX = screenPos.x; | ||
| const screenY = screenPos.y; | ||
|
|
||
| // Get city level for scaling the light effect | ||
| const cityLevel = city.level(); | ||
|
|
||
| // Render city glow - you can customize this pattern | ||
| this.renderCityGlow(context, screenX, screenY, cellSize, cityLevel); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Renders a glow effect for a single city. | ||
| * Customize this method to achieve your desired lighting pattern. | ||
| */ | ||
| private async renderCityGlow( | ||
| context: CanvasRenderingContext2D, | ||
| x: number, | ||
| y: number, | ||
| cellSize: number, | ||
| level: number, | ||
| ): Promise<void> { | ||
| const glow = await this.getGlowBitmap(level, cellSize); | ||
|
|
||
| // Compute radius for positioning (still capped) | ||
| const cappedLevel = this.maxCityLightLevel; | ||
| const radius = (10 + cappedLevel * 2) * cellSize; | ||
|
|
||
| context.drawImage(glow, x - radius, y - radius); | ||
| } | ||
|
|
||
| /** | ||
| * Renders the flashlight effect around the mouse cursor. | ||
| * Extracted from original renderLayer for better organization. | ||
| */ | ||
| private renderFlashlight( | ||
| context: CanvasRenderingContext2D, | ||
| width: number, | ||
| height: number, | ||
| cellSize: number, | ||
| ): void { | ||
| const startX = | ||
| Math.floor( | ||
| Math.max(this.mouseX - this.flashlightRadius * cellSize, 0) / cellSize, | ||
| ) * cellSize; | ||
| const endX = | ||
| Math.ceil( | ||
| Math.min(this.mouseX + this.flashlightRadius * cellSize, width) / | ||
| cellSize, | ||
| ) * cellSize; | ||
|
|
||
| const startY = | ||
| Math.floor( | ||
| Math.max(this.mouseY - this.flashlightRadius * cellSize, 0) / cellSize, | ||
| ) * cellSize; | ||
| const endY = | ||
| Math.ceil( | ||
| Math.min(this.mouseY + this.flashlightRadius * cellSize, height) / | ||
| cellSize, | ||
| ) * cellSize; | ||
|
|
||
| for (let y = startY; y < endY; y += cellSize) { | ||
| for (let x = startX; x < endX; x += cellSize) { | ||
| const dist = Math.hypot( | ||
| (this.mouseX - (x + cellSize / 2)) / cellSize, | ||
| (this.mouseY - (y + cellSize / 2)) / cellSize, | ||
| ); | ||
|
|
||
| const brightness = Math.max(0, 1 - dist / this.flashlightRadius); | ||
|
|
||
| if (brightness > 0) { | ||
| context.fillStyle = `rgba(200,200,130,${(this.darkenAlpha / 2) * brightness})`; | ||
| context.fillRect(x, y, cellSize, cellSize); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+191
to
+205
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Performance consideration: flashlight rendering complexity. Similar to city glow, the flashlight effect uses nested loops that could iterate many times per frame depending on Consider using a radial gradient for better performance: private renderFlashlight(
context: CanvasRenderingContext2D,
width: number,
height: number,
cellSize: number,
): void {
const radius = this.flashlightRadius * cellSize;
const gradient = context.createRadialGradient(
this.mouseX, this.mouseY, 0,
this.mouseX, this.mouseY, radius
);
gradient.addColorStop(0, `rgba(200, 200, 130, ${this.darkenAlpha / 2})`);
gradient.addColorStop(1, 'rgba(200, 200, 130, 0)');
context.fillStyle = gradient;
context.fillRect(
Math.max(0, this.mouseX - radius),
Math.max(0, this.mouseY - radius),
Math.min(width, radius * 2),
Math.min(height, radius * 2)
);
}This eliminates the nested loops while achieving a similar visual effect with much better performance. 🤖 Prompt for AI Agents |
||
| } | ||
| destroy?(): void { | ||
| document.removeEventListener("mousemove", this.mouseMoveHandler); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.