mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:00:44 +00:00
border test 9000
This commit is contained in:
@@ -466,6 +466,8 @@
|
||||
"attack_ratio_desc": "What percentage of your troops to send in an attack (1–100%)",
|
||||
"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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user