mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 07:06:09 +00:00
Patterned territory (#786)
## Description: This is meant to give players more customization options. Permission handling hasn’t really been implemented yet. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri
This commit is contained in:
@@ -45,6 +45,7 @@ import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
|
||||
|
||||
export interface LobbyConfig {
|
||||
serverConfig: ServerConfig;
|
||||
pattern: string | undefined;
|
||||
flag: string;
|
||||
playerName: string;
|
||||
clientID: ClientID;
|
||||
|
||||
@@ -21,6 +21,7 @@ import { NewsModal } from "./NewsModal";
|
||||
import "./PublicLobby";
|
||||
import { PublicLobby } from "./PublicLobby";
|
||||
import { SinglePlayerModal } from "./SinglePlayerModal";
|
||||
import { TerritoryPatternsModal } from "./TerritoryPatternsModal";
|
||||
import { UserSettingModal } from "./UserSettingModal";
|
||||
import "./UsernameInput";
|
||||
import { UsernameInput } from "./UsernameInput";
|
||||
@@ -178,6 +179,28 @@ class Client {
|
||||
hlpModal.open();
|
||||
});
|
||||
|
||||
const territoryModal = document.querySelector(
|
||||
"territory-patterns-modal",
|
||||
) as TerritoryPatternsModal;
|
||||
const tpButton = document.getElementById(
|
||||
"territory-patterns-input-preview-button",
|
||||
);
|
||||
territoryModal instanceof TerritoryPatternsModal;
|
||||
if (tpButton === null)
|
||||
throw new Error("territory-patterns-input-preview-button");
|
||||
territoryModal.previewButton = tpButton;
|
||||
territoryModal.updatePreview();
|
||||
territoryModal.resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.target.classList.contains("preview-container")) {
|
||||
territoryModal.buttonWidth = entry.contentRect.width;
|
||||
}
|
||||
}
|
||||
});
|
||||
tpButton.addEventListener("click", () => {
|
||||
territoryModal.open();
|
||||
});
|
||||
|
||||
if (isLoggedIn() === false) {
|
||||
// Not logged in
|
||||
loginDiscordButton.disable = false;
|
||||
@@ -212,6 +235,7 @@ class Client {
|
||||
loginDiscordButton.translationKey = "main.logged_in";
|
||||
loginDiscordButton.hidden = true;
|
||||
const { user, player } = userMeResponse;
|
||||
territoryModal.onUserMe(userMeResponse);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -316,6 +340,7 @@ class Client {
|
||||
{
|
||||
gameID: lobby.gameID,
|
||||
serverConfig: config,
|
||||
pattern: this.userSettings.getSelectedPattern(),
|
||||
flag:
|
||||
this.flagInput === null || this.flagInput.getCurrentFlag() === "xx"
|
||||
? ""
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
UnitType,
|
||||
mapCategories,
|
||||
} from "../core/game/Game";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import { generateID } from "../core/Util";
|
||||
import "./components/baseComponents/Button";
|
||||
import "./components/baseComponents/Modal";
|
||||
@@ -41,6 +42,8 @@ export class SinglePlayerModal extends LitElement {
|
||||
|
||||
@state() private disabledUnits: UnitType[] = [];
|
||||
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<o-modal title=${translateText("single_modal.title")}>
|
||||
@@ -410,6 +413,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
flagInput.getCurrentFlag() === "xx"
|
||||
? ""
|
||||
: flagInput.getCurrentFlag(),
|
||||
pattern: this.userSettings.getSelectedPattern(),
|
||||
},
|
||||
],
|
||||
config: {
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement, render } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
import { PatternDecoder, territoryPatterns } from "../core/Cosmetics";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import "./components/Difficulties";
|
||||
import "./components/Maps";
|
||||
import { translateText } from "./Utils";
|
||||
|
||||
@customElement("territory-patterns-modal")
|
||||
export class TerritoryPatternsModal extends LitElement {
|
||||
@query("o-modal") private modalEl!: HTMLElement & {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
public previewButton: HTMLElement | null = null;
|
||||
public buttonWidth: number = 100;
|
||||
|
||||
@state() private selectedPattern: string | undefined = undefined;
|
||||
|
||||
@state() private lockedPatterns: string[] = [];
|
||||
@state() private lockedReasons: Record<string, string> = {};
|
||||
@state() private hoveredPattern: string | null = null;
|
||||
@state() private hoverPosition = { x: 0, y: 0 };
|
||||
|
||||
@state() private keySequence: string[] = [];
|
||||
@state() private showChocoPattern = false;
|
||||
|
||||
@state() private roles: string[] = [];
|
||||
@state() private flares: string[] = [];
|
||||
|
||||
public resizeObserver: ResizeObserver;
|
||||
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const b64 = this.userSettings.getSelectedPattern();
|
||||
if (b64) {
|
||||
const found = Object.entries(territoryPatterns.pattern).find(
|
||||
([key, pattern]) => pattern.pattern === b64,
|
||||
);
|
||||
this.selectedPattern = found ? found[0] : "custom";
|
||||
} else {
|
||||
this.selectedPattern = undefined;
|
||||
}
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
this.updateComplete.then(() => {
|
||||
const containers = this.renderRoot.querySelectorAll(".preview-container");
|
||||
if (this.resizeObserver) {
|
||||
containers.forEach((container) =>
|
||||
this.resizeObserver.observe(container),
|
||||
);
|
||||
}
|
||||
this.updatePreview();
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
onUserMe(userMeResponse: UserMeResponse) {
|
||||
const { user, player } = userMeResponse;
|
||||
if (player) {
|
||||
const { publicId, roles, flares } = player;
|
||||
if (roles) {
|
||||
this.roles = roles;
|
||||
}
|
||||
if (flares) {
|
||||
this.flares = flares;
|
||||
}
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private checkPatternPermission(roles: string[]) {
|
||||
const patterns = territoryPatterns.pattern ?? {};
|
||||
|
||||
for (const key in patterns) {
|
||||
const patternData = patterns[key];
|
||||
const roleGroup: string[] | string | undefined = patternData.role_group;
|
||||
console.log(`pattern:${key}`);
|
||||
if (
|
||||
this.flares.includes("pattern:*") ||
|
||||
this.flares.includes(`pattern:${key}`)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!roleGroup || (Array.isArray(roleGroup) && roleGroup.length === 0)) {
|
||||
if (roles.length === 0) {
|
||||
const reason = translateText("territory_patterns.blocked.login");
|
||||
this.setLockedPatterns([key], reason);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const groupList = Array.isArray(roleGroup) ? roleGroup : [roleGroup];
|
||||
const isAllowed = groupList.some((required) => roles.includes(required));
|
||||
|
||||
if (!isAllowed) {
|
||||
const reason = translateText("territory_patterns.blocked.role", {
|
||||
role: groupList.join(", "),
|
||||
});
|
||||
this.setLockedPatterns([key], reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
const key = e.key.toLowerCase();
|
||||
const nextSequence = [...this.keySequence, key].slice(-5);
|
||||
this.keySequence = nextSequence;
|
||||
|
||||
if (nextSequence.join("") === "choco") {
|
||||
this.triggerChocoEasterEgg();
|
||||
this.keySequence = [];
|
||||
}
|
||||
};
|
||||
|
||||
private triggerChocoEasterEgg() {
|
||||
console.log("🍫 Choco pattern unlocked!");
|
||||
this.showChocoPattern = true;
|
||||
|
||||
const popup = document.createElement("div");
|
||||
popup.className = "easter-egg-popup";
|
||||
popup.textContent = "🎉 You unlocked the Choco pattern!";
|
||||
document.body.appendChild(popup);
|
||||
|
||||
setTimeout(() => {
|
||||
popup.remove();
|
||||
}, 5000);
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private renderTooltip(): TemplateResult | null {
|
||||
if (this.hoveredPattern && this.lockedReasons[this.hoveredPattern]) {
|
||||
return html`
|
||||
<div
|
||||
class="fixed z-[10000] px-3 py-2 rounded bg-black text-white text-sm pointer-events-none shadow-md"
|
||||
style="top: ${this.hoverPosition.y + 12}px; left: ${this.hoverPosition
|
||||
.x + 12}px;"
|
||||
>
|
||||
${this.lockedReasons[this.hoveredPattern]}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private renderPatternButton(
|
||||
key: string,
|
||||
pattern: (typeof territoryPatterns.pattern)[string],
|
||||
): TemplateResult {
|
||||
const isLocked = this.isPatternLocked(key);
|
||||
const isSelected =
|
||||
this.selectedPattern === key ||
|
||||
(key === "custom" && this.selectedPattern === "custom");
|
||||
let previewPattern = pattern;
|
||||
if (key === "custom") {
|
||||
const b64 = this.userSettings.getSelectedPattern();
|
||||
if (b64) {
|
||||
previewPattern = { pattern: b64 } as any;
|
||||
}
|
||||
}
|
||||
return html`
|
||||
<button
|
||||
class="border p-2 rounded-lg shadow text-black dark:text-white text-left
|
||||
${isSelected
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700"}
|
||||
${isLocked ? "opacity-50 cursor-not-allowed" : ""}"
|
||||
style="flex: 0 1 calc(25% - 1rem); max-width: calc(25% - 1rem);"
|
||||
@click=${() => !isLocked && this.selectPattern(key)}
|
||||
@mouseenter=${(e: MouseEvent) => this.handleMouseEnter(key, e)}
|
||||
@mousemove=${(e: MouseEvent) => this.handleMouseMove(e)}
|
||||
@mouseleave=${() => this.handleMouseLeave()}
|
||||
>
|
||||
<div class="text-sm font-bold mb-1">
|
||||
${key === "custom"
|
||||
? "Custom"
|
||||
: translateText(`territory_patterns.pattern.${key}`)}
|
||||
</div>
|
||||
<div
|
||||
class="preview-container"
|
||||
style="
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
"
|
||||
>
|
||||
${this.renderPatternPreview(
|
||||
previewPattern,
|
||||
this.buttonWidth,
|
||||
this.buttonWidth,
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPatternGrid(): TemplateResult {
|
||||
const patterns = territoryPatterns.pattern ?? {};
|
||||
|
||||
const buttons: TemplateResult[] = [];
|
||||
for (const key in patterns) {
|
||||
if (!this.showChocoPattern && key === "choco") continue;
|
||||
const result = this.renderPatternButton(key, patterns[key]);
|
||||
buttons.push(result);
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="flex flex-wrap gap-4 p-2"
|
||||
style="justify-content: center; align-items: flex-start;"
|
||||
>
|
||||
<button
|
||||
class="border p-2 rounded-lg shadow text-black dark:text-white text-left
|
||||
${this.selectedPattern === null
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700"}"
|
||||
style="flex: 0 1 calc(25% - 1rem); max-width: calc(25% - 1rem);"
|
||||
@click=${() => this.selectPattern(null)}
|
||||
>
|
||||
<div class="text-sm font-bold mb-1">
|
||||
${translateText("territory_patterns.pattern.default")}
|
||||
</div>
|
||||
<div
|
||||
class="preview-container"
|
||||
style="
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
"
|
||||
>
|
||||
${this.renderBlankPreview(this.buttonWidth, this.buttonWidth)}
|
||||
</div>
|
||||
</button>
|
||||
${buttons}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.resetLockedPatterns();
|
||||
this.checkPatternPermission(this.roles);
|
||||
return html`
|
||||
${this.renderTooltip()}
|
||||
<o-modal
|
||||
id="territoryPatternsModal"
|
||||
title="${translateText("territory_patterns.title")}"
|
||||
>
|
||||
${this.renderPatternGrid()}
|
||||
</o-modal>
|
||||
`;
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.modalEl?.open();
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.modalEl?.close();
|
||||
}
|
||||
|
||||
private selectPattern(patternKey: string | null) {
|
||||
if (patternKey) {
|
||||
const pattern = territoryPatterns.pattern[patternKey];
|
||||
if (pattern) {
|
||||
this.userSettings.setSelectedPattern(pattern.pattern);
|
||||
this.selectedPattern = patternKey;
|
||||
} else {
|
||||
this.userSettings.setSelectedPattern("");
|
||||
this.selectedPattern = undefined;
|
||||
}
|
||||
} else {
|
||||
this.userSettings.setSelectedPattern("");
|
||||
this.selectedPattern = undefined;
|
||||
}
|
||||
this.updatePreview();
|
||||
this.close();
|
||||
}
|
||||
|
||||
private renderPatternPreview(
|
||||
pattern: (typeof territoryPatterns.pattern)[string],
|
||||
width: number,
|
||||
height: number,
|
||||
): TemplateResult {
|
||||
const decoder = new PatternDecoder(pattern.pattern);
|
||||
const cellCountX = decoder.getTileWidth();
|
||||
const cellCountY = decoder.getTileHeight();
|
||||
|
||||
const cellSize =
|
||||
cellCountX > 0 && cellCountY > 0
|
||||
? Math.min(height / cellCountY, width / cellCountX)
|
||||
: 1;
|
||||
|
||||
return html`
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: ${height}px;
|
||||
width: ${width}px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
display: grid;
|
||||
grid-template-columns: repeat(${cellCountX}, ${cellSize}px);
|
||||
grid-template-rows: repeat(${cellCountY}, ${cellSize}px);
|
||||
background-color: #ccc;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
${(() => {
|
||||
const tiles: TemplateResult[] = [];
|
||||
for (let py = 0; py < cellCountY; py++) {
|
||||
for (let px = 0; px < cellCountX; px++) {
|
||||
const x = px << decoder.getScale();
|
||||
const y = py << decoder.getScale();
|
||||
const bit = decoder.isSet(x, y);
|
||||
tiles.push(html`
|
||||
<div
|
||||
style="
|
||||
background-color: ${bit ? "#000" : "transparent"};
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
width: ${cellSize}px;
|
||||
height: ${cellSize}px;
|
||||
border-radius: 1px;
|
||||
"
|
||||
></div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
return tiles;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderBlankPreview(width: number, height: number): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: ${height}px;
|
||||
width: ${width}px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 1px solid #ccc;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="display: grid; grid-template-columns: repeat(2, ${width /
|
||||
2}px); grid-template-rows: repeat(2, ${height / 2}px);"
|
||||
>
|
||||
<div
|
||||
style="background-color: #fff; border: 1px solid rgba(0, 0, 0, 0.1); width: ${width /
|
||||
2}px; height: ${height / 2}px;"
|
||||
></div>
|
||||
<div
|
||||
style="background-color: #fff; border: 1px solid rgba(0, 0, 0, 0.1); width: ${width /
|
||||
2}px; height: ${height / 2}px;"
|
||||
></div>
|
||||
<div
|
||||
style="background-color: #fff; border: 1px solid rgba(0, 0, 0, 0.1); width: ${width /
|
||||
2}px; height: ${height / 2}px;"
|
||||
></div>
|
||||
<div
|
||||
style="background-color: #fff; border: 1px solid rgba(0, 0, 0, 0.1); width: ${width /
|
||||
2}px; height: ${height / 2}px;"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public updatePreview() {
|
||||
if (!this.previewButton) return;
|
||||
|
||||
const patternKey = this.selectedPattern ?? "default";
|
||||
let pattern = territoryPatterns.pattern[patternKey];
|
||||
if (!pattern && patternKey === "custom") {
|
||||
// customパターンはbase64から生成
|
||||
const b64 = this.userSettings.getSelectedPattern();
|
||||
if (b64) {
|
||||
pattern = { pattern: b64 } as any;
|
||||
}
|
||||
}
|
||||
if (!pattern) {
|
||||
const blankPreview = this.renderBlankPreview(48, 48);
|
||||
render(blankPreview, this.previewButton);
|
||||
return;
|
||||
}
|
||||
const previewHTML = this.renderPatternPreview(pattern, 48, 48);
|
||||
render(previewHTML, this.previewButton);
|
||||
}
|
||||
|
||||
private setLockedPatterns(lockedPatterns: string[], reason: string) {
|
||||
this.lockedPatterns = [...this.lockedPatterns, ...lockedPatterns];
|
||||
this.lockedReasons = {
|
||||
...this.lockedReasons,
|
||||
...lockedPatterns.reduce(
|
||||
(acc, key) => {
|
||||
acc[key] = reason;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private resetLockedPatterns() {
|
||||
this.lockedPatterns = [];
|
||||
this.lockedReasons = {};
|
||||
}
|
||||
|
||||
private isPatternLocked(patternKey: string): boolean {
|
||||
return this.lockedPatterns.includes(patternKey);
|
||||
}
|
||||
|
||||
private handleMouseEnter(patternKey: string, event: MouseEvent) {
|
||||
if (this.isPatternLocked(patternKey)) {
|
||||
this.hoveredPattern = patternKey;
|
||||
this.hoverPosition = { x: event.clientX, y: event.clientY };
|
||||
}
|
||||
}
|
||||
|
||||
private handleMouseMove(event: MouseEvent) {
|
||||
if (this.hoveredPattern) {
|
||||
this.hoverPosition = { x: event.clientX, y: event.clientY };
|
||||
}
|
||||
}
|
||||
|
||||
private handleMouseLeave() {
|
||||
this.hoveredPattern = null;
|
||||
}
|
||||
}
|
||||
@@ -370,6 +370,7 @@ export class Transport {
|
||||
token: this.lobbyConfig.token,
|
||||
username: this.lobbyConfig.playerName,
|
||||
flag: this.lobbyConfig.flag,
|
||||
pattern: this.lobbyConfig.pattern,
|
||||
} satisfies ClientJoinMessage);
|
||||
}
|
||||
|
||||
@@ -423,6 +424,7 @@ export class Transport {
|
||||
type: "spawn",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
flag: this.lobbyConfig.flag,
|
||||
pattern: this.lobbyConfig.pattern,
|
||||
name: this.lobbyConfig.playerName,
|
||||
playerType: PlayerType.Human,
|
||||
x: event.cell.x,
|
||||
|
||||
@@ -150,6 +150,15 @@ export class UserSettingModal extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private toggleTerritoryPatterns(e: CustomEvent<{ checked: boolean }>) {
|
||||
const enabled = e.detail?.checked;
|
||||
if (typeof enabled !== "boolean") return;
|
||||
|
||||
this.userSettings.set("settings.territoryPatterns", enabled);
|
||||
|
||||
console.log("🏳️ Territory Patterns:", enabled ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
private handleKeybindChange(
|
||||
e: CustomEvent<{ action: string; value: string }>,
|
||||
) {
|
||||
@@ -262,6 +271,15 @@ export class UserSettingModal extends LitElement {
|
||||
@change=${this.toggleAnonymousNames}
|
||||
></setting-toggle>
|
||||
|
||||
<!-- 🏳️ Territory Patterns -->
|
||||
<setting-toggle
|
||||
label="${translateText("user_setting.territory_patterns_label")}"
|
||||
description="${translateText("user_setting.territory_patterns_desc")}"
|
||||
id="territory-patterns-toggle"
|
||||
.checked=${this.userSettings.territoryPatterns()}
|
||||
@change=${this.toggleTerritoryPatterns}
|
||||
></setting-toggle>
|
||||
|
||||
<!-- ⚔️ Attack Ratio -->
|
||||
<setting-slider
|
||||
label="${translateText("user_setting.attack_ratio_label")}"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EventBus } from "../../core/EventBus";
|
||||
import { GameView } from "../../core/game/GameView";
|
||||
import { UserSettings } from "../../core/game/UserSettings";
|
||||
import { GameStartingModal } from "../GameStartingModal";
|
||||
import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler";
|
||||
import { TransformHandler } from "./TransformHandler";
|
||||
@@ -42,6 +43,7 @@ export function createRenderer(
|
||||
eventBus: EventBus,
|
||||
): GameRenderer {
|
||||
const transformHandler = new TransformHandler(game, eventBus, canvas);
|
||||
const userSettings = new UserSettings();
|
||||
|
||||
const uiState = { attackRatio: 20 };
|
||||
|
||||
@@ -215,7 +217,7 @@ export function createRenderer(
|
||||
|
||||
const layers: Layer[] = [
|
||||
new TerrainLayer(game, transformHandler),
|
||||
new TerritoryLayer(game, eventBus, transformHandler),
|
||||
new TerritoryLayer(game, eventBus, transformHandler, userSettings),
|
||||
new RailroadLayer(game),
|
||||
structureLayer,
|
||||
new UnitLayer(game, eventBus, transformHandler),
|
||||
|
||||
@@ -124,6 +124,11 @@ export class OptionsMenu extends LitElement implements Layer {
|
||||
this.userSettings.toggleLeftClickOpenMenu();
|
||||
}
|
||||
|
||||
private onToggleTerritoryPatterns() {
|
||||
this.userSettings.toggleTerritoryPatterns();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
init() {
|
||||
console.log("init called from OptionsMenu");
|
||||
this.showPauseButton =
|
||||
@@ -207,6 +212,12 @@ export class OptionsMenu extends LitElement implements Layer {
|
||||
title: "Toggle Special effects",
|
||||
children: "💥: " + (this.userSettings.fxLayer() ? "On" : "Off"),
|
||||
})}
|
||||
${button({
|
||||
onClick: this.onToggleTerritoryPatterns,
|
||||
title: "Territory Patterns",
|
||||
children:
|
||||
"🏳️: " + (this.userSettings.territoryPatterns() ? "On" : "Off"),
|
||||
})}
|
||||
${button({
|
||||
onClick: this.onToggleDarkModeButtonClick,
|
||||
title: "Dark Mode",
|
||||
|
||||
@@ -6,16 +6,24 @@ import { Cell, PlayerType, UnitType } from "../../../core/game/Game";
|
||||
import { euclDistFN, TileRef } from "../../../core/game/GameMap";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { UserSettings } from "../../../core/game/UserSettings";
|
||||
import { PseudoRandom } from "../../../core/PseudoRandom";
|
||||
import { AlternateViewEvent, DragEvent } from "../../InputHandler";
|
||||
import {
|
||||
AlternateViewEvent,
|
||||
DragEvent,
|
||||
RefreshGraphicsEvent,
|
||||
} from "../../InputHandler";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
export class TerritoryLayer implements Layer {
|
||||
private userSettings: UserSettings;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private imageData: ImageData;
|
||||
|
||||
private cachedTerritoryPatternsEnabled: boolean | undefined;
|
||||
|
||||
private tileToRenderQueue: PriorityQueue<{
|
||||
tile: TileRef;
|
||||
lastUpdate: number;
|
||||
@@ -42,8 +50,11 @@ export class TerritoryLayer implements Layer {
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
private transformHandler: TransformHandler,
|
||||
userSettings: UserSettings,
|
||||
) {
|
||||
this.userSettings = userSettings;
|
||||
this.theme = game.config().theme();
|
||||
this.cachedTerritoryPatternsEnabled = undefined;
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
@@ -58,6 +69,11 @@ export class TerritoryLayer implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
const prev = this.cachedTerritoryPatternsEnabled;
|
||||
this.cachedTerritoryPatternsEnabled = this.userSettings.territoryPatterns();
|
||||
if (prev !== undefined && prev !== this.cachedTerritoryPatternsEnabled) {
|
||||
this.eventBus.emit(new RefreshGraphicsEvent());
|
||||
}
|
||||
this.game.recentlyUpdatedTiles().forEach((t) => this.enqueueTile(t));
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : [];
|
||||
@@ -291,7 +307,24 @@ export class TerritoryLayer implements Layer {
|
||||
this.paintTile(tile, useBorderColor, 255);
|
||||
}
|
||||
} else {
|
||||
this.paintTile(tile, this.theme.territoryColor(owner), 150);
|
||||
const pattern = owner.pattern();
|
||||
const patternsEnabled = this.cachedTerritoryPatternsEnabled ?? false;
|
||||
if (pattern === undefined || patternsEnabled === false) {
|
||||
this.paintTile(tile, this.theme.territoryColor(owner), 150);
|
||||
} else {
|
||||
const x = this.game.x(tile);
|
||||
const y = this.game.y(tile);
|
||||
const baseColor = this.theme.territoryColor(owner);
|
||||
|
||||
const decoder = owner.patternDecoder();
|
||||
if (decoder !== undefined) {
|
||||
const bit = decoder.isSet(x, y) ? 1 : 0;
|
||||
const colorToUse = bit ? baseColor.darken(0.2) : baseColor;
|
||||
this.paintTile(tile, colorToUse, 150);
|
||||
} else {
|
||||
this.paintTile(tile, baseColor, 150);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -184,6 +184,13 @@
|
||||
|
||||
<div class="container__row">
|
||||
<flag-input class="w-[20%] md:w-[15%]"></flag-input>
|
||||
<territory-patterns-modal class="w-[50%] md:w-[12%]">
|
||||
<button
|
||||
id="territory-patterns-input-preview-button"
|
||||
class="w-full border p-[4px] rounded-lg flex cursor-pointer border-black/30 dark:border-gray-300/60 bg-white/70 dark:bg-[rgba(55,65,81,0.7)]"
|
||||
title="Pick a pattern!"
|
||||
></button>
|
||||
</territory-patterns-modal>
|
||||
<username-input class="relative w-full"></username-input>
|
||||
<news-button
|
||||
class="w-[20%] md:w-[15%] component-hideable"
|
||||
|
||||
Reference in New Issue
Block a user