border test 9000

This commit is contained in:
scamiv
2026-01-18 01:10:36 +01:00
parent e37bbbb169
commit e5c7ba24bc
9 changed files with 237 additions and 4 deletions
+2
View File
@@ -466,6 +466,8 @@
"attack_ratio_desc": "What percentage of your troops to send in an attack (1100%)",
"territory_patterns_label": "🏳️ Territory Skins",
"territory_patterns_desc": "Choose whether to display territory skin designs in game",
"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",
+26
View File
@@ -5,6 +5,7 @@ import { UserSettings } from "../core/game/UserSettings";
import "./components/baseComponents/setting/SettingKeybind";
import { SettingKeybind } from "./components/baseComponents/setting/SettingKeybind";
import "./components/baseComponents/setting/SettingNumber";
import "./components/baseComponents/setting/SettingSelect";
import "./components/baseComponents/setting/SettingSlider";
import "./components/baseComponents/setting/SettingToggle";
import { BaseModal } from "./components/BaseModal";
@@ -370,6 +371,16 @@ export class UserSettingModal extends BaseModal {
console.log("🏳️ Territory Patterns:", enabled ? "ON" : "OFF");
}
private changeTerritoryBorderMode(e: CustomEvent<{ value: string }>) {
const value = e.detail?.value;
if (typeof value !== "string") return;
const mode = parseInt(value, 10);
if (!Number.isFinite(mode)) return;
this.userSettings.setInt("settings.territoryBorderMode", mode);
}
private togglePerformanceOverlay(e: CustomEvent<{ checked: boolean }>) {
const enabled = e.detail?.checked;
if (typeof enabled !== "boolean") return;
@@ -794,6 +805,21 @@ export class UserSettingModal extends BaseModal {
this.toggleDarkMode(e)}
></setting-toggle>
<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>
<!-- 😊 Emojis -->
<setting-toggle
label="${translateText("user_setting.emojis_label")}"
@@ -0,0 +1,62 @@
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";
export type SettingSelectOption = { value: string; label: string };
@customElement("setting-select")
export class SettingSelect extends LitElement {
@property() label = "Setting";
@property() description = "";
@property() id = "";
@property() value = "";
@property({ attribute: false }) options: SettingSelectOption[] = [];
@property({ type: Boolean }) easter = false;
createRenderRoot() {
return this;
}
private handleChange(e: Event) {
const select = e.target as HTMLSelectElement;
this.value = select.value;
this.dispatchEvent(
new CustomEvent("change", {
detail: { value: this.value },
bubbles: true,
composed: true,
}),
);
}
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-row items-center justify-between w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-4 ${rainbowClass}"
>
<div class="flex flex-col flex-1 min-w-0 mr-4">
<div class="text-white font-bold text-base block mb-1">
${this.label}
</div>
<div class="text-white/50 text-sm leading-snug">
${this.description}
</div>
</div>
<select
id=${this.id}
class="shrink-0 bg-black/60 border border-white/10 text-white/90 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600"
.value=${this.value}
@change=${this.handleChange}
>
${this.options.map(
(o) => html`<option value=${o.value}>${o.label}</option>`,
)}
</select>
</div>
`;
}
}
@@ -141,6 +141,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();
@@ -286,6 +297,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
@@ -33,6 +33,20 @@ export class UserSettings {
localStorage.setItem(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.get("settings.emojis", true);
}
@@ -73,6 +87,10 @@ export class UserSettings {
return this.get("settings.territoryPatterns", true);
}
territoryBorderMode(): number {
return this.getInt("settings.territoryBorderMode", 1);
}
cursorCostLabel() {
const legacy = this.get("settings.ghostPricePill", true);
return this.get("settings.cursorCostLabel", legacy);