border test 9000

This commit is contained in:
scamiv
2026-01-18 01:10:36 +01:00
parent 99e7d36464
commit 06eb82b1cf
9 changed files with 186 additions and 10 deletions
+2
View File
@@ -703,6 +703,8 @@
"coordinate_grid_desc": "Toggle the alphanumeric grid overlay",
"attacking_troops_overlay_label": "Attacking Troops Overlay",
"attacking_troops_overlay_desc": "Show attacker vs defender troop counts on active front lines.",
"territory_border_mode_label": "Territory Borders",
"territory_border_mode_desc": "Select border rendering style (visual only)",
"performance_overlay_label": "Performance Overlay",
"performance_overlay_desc": "Toggle the performance overlay. When enabled, the performance overlay will be displayed. Press shift-D during game to toggle.",
"easter_writing_speed_label": "Writing Speed Multiplier",
+25
View File
@@ -300,6 +300,16 @@ export class UserSettingModal extends BaseModal {
this.requestUpdate();
}
private changeTerritoryBorderMode(e: CustomEvent<{ value: number | string }>) {
const rawValue = e.detail?.value;
const value =
typeof rawValue === "number" ? rawValue : parseInt(String(rawValue), 10);
if (!Number.isFinite(value)) return;
this.userSettings.setInt("settings.territoryBorderMode", Math.round(value));
this.requestUpdate();
}
private toggleTerritoryPatterns() {
this.userSettings.toggleTerritoryPatterns();
@@ -752,6 +762,21 @@ export class UserSettingModal extends BaseModal {
></setting-toggle>
<!-- 😊 Emojis -->
<setting-select
label="${translateText("user_setting.territory_border_mode_label")}"
description="${translateText(
"user_setting.territory_border_mode_desc",
)}"
id="territory-border-mode-select"
.value=${String(this.userSettings.territoryBorderMode())}
.options=${[
{ value: 0, label: "Off" },
{ value: 1, label: "Simple" },
{ value: 2, label: "Glow" },
]}
@change=${this.changeTerritoryBorderMode}
></setting-select>
<setting-toggle
label="${translateText("user_setting.emojis_label")}"
description="${translateText("user_setting.emojis_desc")}"
@@ -1,7 +1,7 @@
import { LitElement, html } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
type SelectOption = {
export type SettingSelectOption = {
value: number | string;
label: string;
};
@@ -10,8 +10,10 @@ type SelectOption = {
export class SettingSelect extends LitElement {
@property() label = "Setting";
@property() description = "";
@property({ type: Array }) options: SelectOption[] = [];
@property() id = "setting-select-input";
@property({ type: Array }) options: SettingSelectOption[] = [];
@property({ type: String }) value = "";
@property({ type: Boolean }) easter = false;
createRenderRoot() {
return this;
@@ -35,14 +37,18 @@ export class SettingSelect extends LitElement {
}
render() {
const rainbowClass = this.easter
? "bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)] bg-[length:1400%_1400%] animate-rainbow-bg text-white hover:bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)]"
: "";
return html`
<div
class="flex flex-col w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-3"
class="flex flex-col w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-3 ${rainbowClass}"
>
<div class="flex flex-col min-w-0">
<label
class="text-white font-bold text-base block mb-1"
for="setting-select-input"
for=${this.id}
>${this.label}</label
>
<div class="text-white/50 text-sm leading-snug">
@@ -51,7 +57,7 @@ export class SettingSelect extends LitElement {
</div>
<div class="relative w-full">
<select
id="setting-select-input"
id=${this.id}
class="w-full appearance-none py-2 pl-3 pr-9 border border-white/20 rounded-lg bg-black/40 text-white font-mono text-sm focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all"
.value=${String(this.value)}
@change=${this.handleChange}
@@ -149,6 +149,17 @@ export class SettingsModal extends LitElement implements Layer {
this.requestUpdate();
}
private onTerritoryBorderModeChange(event: Event) {
const value = (event.target as HTMLSelectElement).value;
const mode = Number.parseInt(value, 10);
if (!Number.isFinite(mode))
throw new Error(`Invalid border mode: ${value}`);
this.userSettings.setInt("settings.territoryBorderMode", mode);
this.eventBus.emit(new RefreshGraphicsEvent());
this.requestUpdate();
}
private onToggleRandomNameModeButtonClick() {
this.userSettings.toggleRandomName();
this.requestUpdate();
@@ -299,6 +310,34 @@ export class SettingsModal extends LitElement implements Layer {
</div>
</button>
<div
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
>
<img
src=${treeIcon}
alt="territoryBorderMode"
width="20"
height="20"
/>
<div class="flex-1">
<div class="font-medium">
${translateText("user_setting.territory_border_mode_label")}
</div>
<div class="text-sm text-slate-400">
${translateText("user_setting.territory_border_mode_desc")}
</div>
</div>
<select
class="shrink-0 bg-slate-900 border border-slate-600 text-white/90 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-600"
.value=${String(this.userSettings.territoryBorderMode())}
@change=${this.onTerritoryBorderModeChange}
>
<option value="0">Off</option>
<option value="1">Simple</option>
<option value="2">Glow</option>
</select>
</div>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
@click="${this.onToggleEmojisButtonClick}"
@@ -27,6 +27,7 @@ export class TerritoryLayer implements Layer {
private lastPaletteSignature: string | null = null;
private lastDefensePostsSignature: string | null = null;
private lastBorderMode: number | null = null;
private lastMousePosition: { x: number; y: number } | null = null;
private hoveredOwnerSmallId: number | null = null;
@@ -68,6 +69,7 @@ export class TerritoryLayer implements Layer {
this.refreshPaletteIfNeeded();
this.refreshDefensePostsIfNeeded();
this.refreshBorderModeIfNeeded();
const updatedTiles = this.game.recentlyUpdatedTiles();
for (let i = 0; i < updatedTiles.length; i++) {
@@ -98,6 +100,8 @@ export class TerritoryLayer implements Layer {
this.territoryRenderer = renderer;
this.territoryRenderer.setAlternativeView(this.alternativeView);
this.territoryRenderer.setHighlightedOwnerId(this.hoveredOwnerSmallId);
this.lastBorderMode = this.userSettings.territoryBorderMode();
this.territoryRenderer.setBorderMode(this.lastBorderMode);
this.territoryRenderer.markAllDirty();
this.territoryRenderer.refreshPalette();
this.lastPaletteSignature = this.computePaletteSignature();
@@ -282,6 +286,17 @@ export class TerritoryLayer implements Layer {
}
}
private refreshBorderModeIfNeeded() {
if (!this.territoryRenderer) {
return;
}
const mode = this.userSettings.territoryBorderMode();
if (mode !== this.lastBorderMode) {
this.lastBorderMode = mode;
this.territoryRenderer.setBorderMode(mode);
}
}
private computeDefensePostsSignature(): string {
// Active + completed posts only.
const parts: string[] = [];
@@ -227,6 +227,13 @@ export class TerritoryRenderer {
this.resources.setHighlightedOwnerId(ownerSmallId);
}
setBorderMode(mode: number): void {
if (!this.resources) {
return;
}
this.resources.setBorderMode(mode);
}
markTile(tile: TileRef): void {
if (this.stateUpdatePass) {
this.stateUpdatePass.markTile(tile);
@@ -78,6 +78,7 @@ export class GroundTruthData {
private viewOffsetY = 0;
private alternativeView = false;
private highlightedOwnerId = -1;
private borderMode = 1;
private constructor(
private readonly device: GPUDevice,
@@ -223,6 +224,10 @@ export class GroundTruthData {
this.highlightedOwnerId = ownerSmallId ?? -1;
}
setBorderMode(mode: number): void {
this.borderMode = Math.max(0, Math.min(2, Math.trunc(mode)));
}
// =====================
// Upload methods
// =====================
@@ -608,9 +613,11 @@ export class GroundTruthData {
}
private ensureDefensePostsByOwnerBuffer(capacityPosts: number): void {
const requested = Math.max(1, capacityPosts);
if (
this.defensePostsByOwnerBuffer &&
capacityPosts <= this.defensePostsByOwnerCapacity
requested <= this.defensePostsByOwnerCapacity &&
this.defensePostsByOwnerStaging
) {
return;
}
@@ -621,7 +628,7 @@ export class GroundTruthData {
this.defensePostsByOwnerCapacity = Math.max(
8,
Math.pow(2, Math.ceil(Math.log2(Math.max(1, capacityPosts)))),
Math.pow(2, Math.ceil(Math.log2(requested))),
);
const bytesPerPost = 8; // 2 * u32 (x,y)
@@ -846,7 +853,7 @@ export class GroundTruthData {
this.uniformData[7] = this.highlightedOwnerId;
this.uniformData[8] = this.viewWidth;
this.uniformData[9] = this.viewHeight;
this.uniformData[10] = 0;
this.uniformData[10] = this.borderMode;
this.uniformData[11] = 0;
this.device.queue.writeBuffer(this.uniformBuffer, 0, this.uniformData);
@@ -1,7 +1,7 @@
struct Uniforms {
mapResolution_viewScale_time: vec4f, // x=mapW, y=mapH, z=viewScale, w=timeSec
viewOffset_alt_highlight: vec4f, // x=offX, y=offY, z=alternativeView, w=highlightOwnerId
viewSize_pad: vec4f, // x=viewW, y=viewH, z/w unused
viewSize_pad: vec4f, // x=viewW, y=viewH, z=borderMode, w unused
};
@group(0) @binding(0) var<uniform> u: Uniforms;
@@ -30,6 +30,7 @@ fn fsMain(@builtin(position) pos: vec4f) -> @location(0) vec4f {
let altView = u.viewOffset_alt_highlight.z;
let highlightId = u.viewOffset_alt_highlight.w;
let viewSize = u.viewSize_pad.xy;
let borderMode = u.viewSize_pad.z;
// WebGPU fragment position is top-left origin and at pixel centers (0.5, 1.5, ...).
let viewCoord = vec2f(pos.x - 0.5, pos.y - 0.5);
@@ -76,6 +77,62 @@ fn fsMain(@builtin(position) pos: vec4f) -> @location(0) vec4f {
outColor = terrain;
}
// Borders (purely visual): render a stable-pixel-width line at ownership edges.
if (borderMode > 0.5 && altView <= 0.5 && owner != 0u) {
let fx = fract(mapCoord.x);
let fy = fract(mapCoord.y);
var dist = 1e9;
// Only border against other non-zero owners.
if (texCoord.x > 0) {
let o = textureLoad(stateTex, texCoord + vec2i(-1, 0), 0).x & 0xFFFu;
if (o != 0u && o != owner) {
dist = min(dist, fx);
}
}
if (texCoord.x + 1 < i32(mapRes.x)) {
let o = textureLoad(stateTex, texCoord + vec2i(1, 0), 0).x & 0xFFFu;
if (o != 0u && o != owner) {
dist = min(dist, 1.0 - fx);
}
}
if (texCoord.y > 0) {
let o = textureLoad(stateTex, texCoord + vec2i(0, -1), 0).x & 0xFFFu;
if (o != 0u && o != owner) {
dist = min(dist, fy);
}
}
if (texCoord.y + 1 < i32(mapRes.y)) {
let o = textureLoad(stateTex, texCoord + vec2i(0, 1), 0).x & 0xFFFu;
if (o != 0u && o != owner) {
dist = min(dist, 1.0 - fy);
}
}
if (dist < 1e8) {
let pxPerTile = max(viewScale, 0.001);
let aaTiles = 1.0 / pxPerTile;
let thicknessPx = select(1.0, 2.5, borderMode > 1.5);
let thicknessTiles = thicknessPx / pxPerTile;
let line = 1.0 - smoothstep(thicknessTiles, thicknessTiles + aaTiles, dist);
outColor = vec4f(
mix(outColor.rgb, vec3f(0.0, 0.0, 0.0), clamp(line * 0.9, 0.0, 0.9)),
outColor.a,
);
if (borderMode > 1.5) {
let glowTiles = (thicknessPx * 3.0) / pxPerTile;
let glow = 1.0 - smoothstep(glowTiles, glowTiles + aaTiles * 2.0, dist);
outColor = vec4f(
mix(outColor.rgb, vec3f(1.0, 1.0, 1.0), clamp(glow * 0.12, 0.0, 0.12)),
outColor.a,
);
}
}
}
// Apply hover highlight if needed
if (highlightId > 0.5) {
let alpha = select(0.65, 0.0, altView > 0.5);
+18
View File
@@ -123,6 +123,20 @@ export class UserSettings {
this.setCached(key, value.toString());
}
getInt(key: string, defaultValue: number): number {
const value = localStorage.getItem(key);
if (!value) return defaultValue;
const intValue = parseInt(value, 10);
if (!Number.isFinite(intValue)) return defaultValue;
return intValue;
}
setInt(key: string, value: number): void {
localStorage.setItem(key, Math.trunc(value).toString());
}
emojis() {
return this.getBool("settings.emojis", true);
}
@@ -174,6 +188,10 @@ export class UserSettings {
);
}
territoryBorderMode(): number {
return this.getInt("settings.territoryBorderMode", 1);
}
cursorCostLabel() {
const legacy = this.getBool("settings.ghostPricePill", true);
return this.getBool("settings.cursorCostLabel", legacy);