From 7d5c42b864d9c954d549ba5164b26caa541f28cc Mon Sep 17 00:00:00 2001 From: platz1de <51201131+platz1de@users.noreply.github.com> Date: Tue, 15 Oct 2024 20:05:17 +0200 Subject: [PATCH] Add debug renderer for boat areas as a setting (writing the same boilerplate for the 7th time is enough) This also adds a new setting "type", which will be used more in the future --- src/renderer/GameRenderer.ts | 2 + .../layer/debug/BoatMeshDebugRenderer.ts | 30 +++++++ src/renderer/layer/debug/DebugRenderer.ts | 60 ++++++++++++++ .../layer/debug/DebugRendererRegistry.ts | 10 +++ src/util/ManagedSetting.ts | 11 +++ src/util/MultiSelectSetting.ts | 80 +++++++++++++++++++ src/util/SettingRegistry.ts | 9 +++ src/util/UserSettingManager.ts | 2 + 8 files changed, 204 insertions(+) create mode 100644 src/renderer/layer/debug/BoatMeshDebugRenderer.ts create mode 100644 src/renderer/layer/debug/DebugRenderer.ts create mode 100644 src/renderer/layer/debug/DebugRendererRegistry.ts create mode 100644 src/util/ManagedSetting.ts create mode 100644 src/util/MultiSelectSetting.ts diff --git a/src/renderer/GameRenderer.ts b/src/renderer/GameRenderer.ts index 0af4e31..65996f9 100644 --- a/src/renderer/GameRenderer.ts +++ b/src/renderer/GameRenderer.ts @@ -5,6 +5,7 @@ import {backgroundLayer} from "./layer/BackgroundLayer"; import {territoryRenderer} from "./layer/TerritoryRenderer"; import {nameRenderer} from "./layer/NameRenderer"; import {boatRenderer} from "./layer/BoatRenderer"; +import {debugRenderer} from "./layer/debug/DebugRenderer"; import {gameStartRegistry} from "../game/Game"; /** @@ -42,6 +43,7 @@ export class GameRenderer { this.registerLayer(territoryRenderer); this.registerLayer(nameRenderer); this.registerLayer(boatRenderer); + this.registerLayer(debugRenderer); } /** diff --git a/src/renderer/layer/debug/BoatMeshDebugRenderer.ts b/src/renderer/layer/debug/BoatMeshDebugRenderer.ts new file mode 100644 index 0000000..3435b78 --- /dev/null +++ b/src/renderer/layer/debug/BoatMeshDebugRenderer.ts @@ -0,0 +1,30 @@ +import {areaCalculator} from "../../../map/area/AreaCalculator"; +import {DebugRendererLayer} from "./DebugRendererRegistry"; +import {mapNavigationHandler} from "../../../game/action/MapNavigationHandler"; + +export class BoatMeshDebugRenderer implements DebugRendererLayer { + readonly useCache = false; + + render(context: CanvasRenderingContext2D): void { + context.strokeStyle = "red"; + context.fillStyle = "orange"; + for (const nodes of areaCalculator.nodeIndex) { + for (const node of nodes) { + context.beginPath(); + context.arc((node.x + 0.5) * mapNavigationHandler.zoom + mapNavigationHandler.x, (node.y + 0.5) * mapNavigationHandler.zoom + mapNavigationHandler.y, mapNavigationHandler.zoom / 2, 0, 2 * Math.PI); + context.fill(); + } + } + for (const nodes of areaCalculator.nodeIndex) { + for (const node of nodes) { + for (const neighbor of node.edges) { + if (neighbor.node.x < node.x || (neighbor.node.x === node.x && neighbor.node.y < node.y)) continue; // Only draw each edge once + context.beginPath(); + context.moveTo((node.x + 0.5) * mapNavigationHandler.zoom + mapNavigationHandler.x, (node.y + 0.5) * mapNavigationHandler.zoom + mapNavigationHandler.y); + context.lineTo((neighbor.node.x + 0.5) * mapNavigationHandler.zoom + mapNavigationHandler.x, (neighbor.node.y + 0.5) * mapNavigationHandler.zoom + mapNavigationHandler.y); + context.stroke(); + } + } + } + } +} diff --git a/src/renderer/layer/debug/DebugRenderer.ts b/src/renderer/layer/debug/DebugRenderer.ts new file mode 100644 index 0000000..00af2fe --- /dev/null +++ b/src/renderer/layer/debug/DebugRenderer.ts @@ -0,0 +1,60 @@ +import {CachedLayer} from "../CachedLayer"; +import {mapTransformHandler} from "../../../event/MapTransformHandler"; +import {gameMap} from "../../../game/GameData"; +import {registerSettingListener} from "../../../util/UserSettingManager"; +import {RendererLayer} from "../RendererLayer"; +import {DebugRendererLayer} from "./DebugRendererRegistry"; +import {gameStartRegistry} from "../../../game/Game"; + +/** + * Debug renderer. + * Renders debug information on the map if toggled. + * @internal + */ +class DebugRenderer extends CachedLayer { + private readonly mapLayers: RendererLayer[] = []; + private readonly liveLayers: RendererLayer[] = []; + + /** + * Updates the layers to be rendered by the debug renderer. + * @param layers layers to be rendered + */ + updateLayers(layers: DebugRendererLayer[]): void { + this.mapLayers.length = 0; + this.liveLayers.length = 0; + for (const layer of layers) { + if (layer.useCache) { + this.mapLayers.push(layer); + } else { + this.liveLayers.push(layer); + } + } + } + + render(context: CanvasRenderingContext2D) { + super.render(context); + this.liveLayers.forEach(layer => layer.render(context)); + } + + init(): void { + this.resizeCanvas(gameMap.width, gameMap.height); + this.mapLayers.forEach(layer => layer.render(this.context)); + } + + onMapMove(this: void, x: number, y: number): void { + debugRenderer.dx = x; + debugRenderer.dy = y; + } + + onMapScale(this: void, scale: number): void { + debugRenderer.scale = scale; + } +} + +export const debugRenderer = new DebugRenderer(); + +mapTransformHandler.scale.register(debugRenderer.onMapScale); +mapTransformHandler.move.register(debugRenderer.onMapMove); +gameStartRegistry.register(debugRenderer.init.bind(debugRenderer)); + +registerSettingListener("debug-renderer", value => debugRenderer.updateLayers(value.getEnabledOptions()), true); \ No newline at end of file diff --git a/src/renderer/layer/debug/DebugRendererRegistry.ts b/src/renderer/layer/debug/DebugRendererRegistry.ts new file mode 100644 index 0000000..d1182ab --- /dev/null +++ b/src/renderer/layer/debug/DebugRendererRegistry.ts @@ -0,0 +1,10 @@ +import {MultiSelectSetting} from "../../../util/MultiSelectSetting"; +import {BoatMeshDebugRenderer} from "./BoatMeshDebugRenderer"; +import {RendererLayer} from "../RendererLayer"; + +export const debugRendererLayers = MultiSelectSetting.init() + .option("boat-navigation-mesh", new BoatMeshDebugRenderer(), "Boat Navigation Mesh", false); + +export interface DebugRendererLayer extends Omit { + useCache: boolean; +} \ No newline at end of file diff --git a/src/util/ManagedSetting.ts b/src/util/ManagedSetting.ts new file mode 100644 index 0000000..eabc79a --- /dev/null +++ b/src/util/ManagedSetting.ts @@ -0,0 +1,11 @@ +export interface ManagedSetting { + /** + * Returns a string representation of this settings value. + */ + toString(): string; + + /** + * Parses a string to set the value of this setting. + */ + fromString(value: string): void; +} \ No newline at end of file diff --git a/src/util/MultiSelectSetting.ts b/src/util/MultiSelectSetting.ts new file mode 100644 index 0000000..c399746 --- /dev/null +++ b/src/util/MultiSelectSetting.ts @@ -0,0 +1,80 @@ +import {InvalidArgumentException} from "./Exceptions"; +import {ManagedSetting} from "./ManagedSetting"; + +export class MultiSelectSetting>> implements ManagedSetting { + private options: T = {} as T; + + static init() { + return new MultiSelectSetting<{}>(); + } + + /** + * Registers a new option with the given key and value. + * @param key The key of the option. Must be unique, not contain ',' and shouldn't change in the future + * @param value The value of the option + * @param label The label of the option, displayed in the UI + * @param defaultStatus The default status of the option + * @throws InvalidArgumentException if the key contains ',' + */ + option(key: K & Exclude, value: V, label: string, defaultStatus: boolean) { + if (key.includes(",")) throw new InvalidArgumentException("Key cannot contain ','"); + (this.options as unknown as Record>)[key] = {value, label, status: defaultStatus}; + return this as unknown as MultiSelectSetting>>; + } + + /** + * Checks if the option with the given key is selected. + * @param key The key of the option + * @returns true if the option is selected, false otherwise + */ + isSelected(key: keyof T) { + return this.options[key].status; + } + + /** + * Selects the option with the given key. + * @param key The key of the option + * @param status The status to set + */ + select(key: keyof T, status: boolean) { + this.options[key].status = status; + } + + /** + * Returns the enabled options. + * @returns An array of the values of the enabled options + */ + getEnabledOptions(): AnyValue[] { + return Object.keys(this.options).filter(key => this.options[key].status).map(key => this.options[key].value); + } + + /** + * Returns all options. + * @returns An array of the values of all options + */ + getAllOptions(): AnyValue[] { + return Object.keys(this.options).map(key => this.options[key].value); + } + + toString() { + return Object.keys(this.options).filter(key => this.options[key].status).join(","); + } + + fromString(value: string) { + const selected = value.split(","); + for (const key in this.options) { + this.options[key].status = selected.includes(key); + } + return this; + } +} + +type Option = { + value: T; + label: string; + status: boolean; +} + +type AnyValue>> = { + [K in keyof T]: T[K]["value"]; +}[keyof T]; \ No newline at end of file diff --git a/src/util/SettingRegistry.ts b/src/util/SettingRegistry.ts index e96a4bb..8064a25 100644 --- a/src/util/SettingRegistry.ts +++ b/src/util/SettingRegistry.ts @@ -1,5 +1,6 @@ import {EventHandlerRegistry} from "../event/EventHandlerRegistry"; import {InvalidArgumentException, UnsupportedDataException} from "./Exceptions"; +import {ManagedSetting} from "./ManagedSetting"; /** * Important Note: For types to work correctly, all register calls must be chained together. @@ -94,6 +95,7 @@ export class SettingRegistry>> { return value; } + //TODO: Turn all of these into managed settings, so we can differentiate between the different types registerString(key: K & Exclude, defaultValue: string, version: number = 0) { return this.registerUpdatable(key, defaultValue, String, version); } @@ -110,6 +112,13 @@ export class SettingRegistry>> { return this.registerUpdatable(key, defaultValue, value => value === "true", version); } + registerManaged(key: K & Exclude, setting: S, version: number = 0) { + return this.registerUpdatable(key, setting, value => { + setting.fromString(value); + return setting; + }, version); + } + /** * Register an updater for a setting * @param key the key of the setting diff --git a/src/util/UserSettingManager.ts b/src/util/UserSettingManager.ts index 2a7d393..5a83910 100644 --- a/src/util/UserSettingManager.ts +++ b/src/util/UserSettingManager.ts @@ -1,5 +1,6 @@ import {SettingRegistry} from "./SettingRegistry"; import {getTheme} from "../renderer/GameTheme"; +import {debugRendererLayers} from "../renderer/layer/debug/DebugRendererRegistry"; /** * Setting registry, all register calls need to be chained together @@ -10,6 +11,7 @@ const registry = SettingRegistry.init("wf") .registerBoolean("hud-clock", true) .registerString("api-location", "https://warfront.io/api") //This needs to enforce no trailing slash, no query parameters and a protocol .registerString("game-server", "warfront.io") + .registerManaged("debug-renderer", debugRendererLayers); registry.load();