mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-11 21:49:55 +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:
@@ -43,6 +43,7 @@ export const UserMeResponseSchema = z.object({
|
||||
player: z.object({
|
||||
publicId: z.string(),
|
||||
roles: z.string().array().optional(),
|
||||
flares: z.string().array().optional(),
|
||||
}),
|
||||
});
|
||||
export type UserMeResponse = z.infer<typeof UserMeResponseSchema>;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Schema for resources/cosmetics/cosmetics.json
|
||||
export const CosmeticsSchema = z.object({
|
||||
role_group: z.record(z.string(), z.string().array()).optional(),
|
||||
pattern: z.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
pattern: z.string().base64(),
|
||||
role_group: z.string().array().optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type Cosmetics = z.infer<typeof CosmeticsSchema>;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { base64url } from "jose";
|
||||
import rawTerritoryPatterns from "../../resources/cosmetics/cosmetics.json" with { type: "json" };
|
||||
import { CosmeticsSchema } from "./CosmeticSchemas";
|
||||
|
||||
export const territoryPatterns = CosmeticsSchema.parse(rawTerritoryPatterns);
|
||||
|
||||
export class PatternDecoder {
|
||||
private bytes: Uint8Array;
|
||||
private tileWidth: number;
|
||||
private tileHeight: number;
|
||||
private scale: number;
|
||||
|
||||
constructor(base64: string) {
|
||||
this.bytes = base64url.decode(base64);
|
||||
|
||||
if (this.bytes.length < 3) {
|
||||
throw new Error(
|
||||
"Pattern data is too short to contain required metadata.",
|
||||
);
|
||||
}
|
||||
|
||||
const version = this.bytes[0];
|
||||
if (version !== 0) {
|
||||
throw new Error(`Unrecognized pattern version ${version}.`);
|
||||
}
|
||||
|
||||
const byte1 = this.bytes[1];
|
||||
const byte2 = this.bytes[2];
|
||||
this.scale = byte1 & 0x07;
|
||||
|
||||
this.tileWidth = (((byte2 & 0x03) << 5) | ((byte1 >> 3) & 0x1f)) + 2;
|
||||
this.tileHeight = ((byte2 >> 2) & 0x3f) + 2;
|
||||
|
||||
const expectedBits = this.tileWidth * this.tileHeight;
|
||||
const expectedBytes = (expectedBits + 7) >> 3; // Equivalent to: ceil(expectedBits / 8);
|
||||
if (this.bytes.length - 3 < expectedBytes) {
|
||||
throw new Error(
|
||||
"Pattern data is too short for the specified dimensions.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getTileWidth(): number {
|
||||
return this.tileWidth;
|
||||
}
|
||||
|
||||
getTileHeight(): number {
|
||||
return this.tileHeight;
|
||||
}
|
||||
|
||||
getScale(): number {
|
||||
return this.scale;
|
||||
}
|
||||
|
||||
isSet(x: number, y: number): boolean {
|
||||
const px = (x >> this.scale) % this.tileWidth;
|
||||
const py = (y >> this.scale) % this.tileHeight;
|
||||
const idx = py * this.tileWidth + px;
|
||||
const byteIndex = idx >> 3;
|
||||
const bitIndex = idx & 7;
|
||||
const byte = this.bytes[3 + byteIndex];
|
||||
if (byte === undefined) throw new Error("Invalid pattern");
|
||||
return (byte & (1 << bitIndex)) !== 0;
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ export async function createGameRunner(
|
||||
const humans = gameStart.players.map(
|
||||
(p) =>
|
||||
new PlayerInfo(
|
||||
p.pattern,
|
||||
p.flag,
|
||||
p.clientID === clientID
|
||||
? sanitize(p.username)
|
||||
@@ -60,6 +61,7 @@ export async function createGameRunner(
|
||||
new Cell(n.coordinates[0], n.coordinates[1]),
|
||||
n.strength,
|
||||
new PlayerInfo(
|
||||
undefined,
|
||||
n.flag || "",
|
||||
n.name,
|
||||
PlayerType.FakeHuman,
|
||||
|
||||
@@ -178,6 +178,7 @@ export const AllPlayersStatsSchema = z.record(ID, PlayerStatsSchema);
|
||||
|
||||
export const UsernameSchema = SafeString;
|
||||
export const FlagSchema = z.string().max(128).optional();
|
||||
export const PatternSchema = z.string().max(128).base64().optional();
|
||||
|
||||
export const QuickChatKeySchema = z.enum(
|
||||
Object.entries(quickChatData).flatMap(([category, entries]) =>
|
||||
@@ -203,6 +204,7 @@ export const SpawnIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("spawn"),
|
||||
name: UsernameSchema,
|
||||
flag: FlagSchema,
|
||||
pattern: PatternSchema,
|
||||
playerType: PlayerTypeSchema,
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
@@ -350,6 +352,7 @@ export const PlayerSchema = z.object({
|
||||
clientID: ID,
|
||||
username: UsernameSchema,
|
||||
flag: FlagSchema,
|
||||
pattern: PatternSchema,
|
||||
});
|
||||
|
||||
export const GameStartInfoSchema = z.object({
|
||||
@@ -454,6 +457,7 @@ export const ClientJoinMessageSchema = z.object({
|
||||
lastTurn: z.number(), // The last turn the client saw.
|
||||
username: UsernameSchema,
|
||||
flag: FlagSchema,
|
||||
pattern: PatternSchema,
|
||||
});
|
||||
|
||||
export const ClientMessageSchema = z.discriminatedUnion("type", [
|
||||
|
||||
@@ -46,7 +46,14 @@ export class BotSpawner {
|
||||
}
|
||||
}
|
||||
return new SpawnExecution(
|
||||
new PlayerInfo("", botName, PlayerType.Bot, null, this.random.nextID()),
|
||||
new PlayerInfo(
|
||||
undefined,
|
||||
"",
|
||||
botName,
|
||||
PlayerType.Bot,
|
||||
null,
|
||||
this.random.nextID(),
|
||||
),
|
||||
tile,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -349,6 +349,7 @@ export class PlayerInfo {
|
||||
public readonly clan: string | null;
|
||||
|
||||
constructor(
|
||||
public readonly pattern: string | undefined,
|
||||
public readonly flag: string | undefined,
|
||||
public readonly name: string,
|
||||
public readonly playerType: PlayerType,
|
||||
|
||||
@@ -133,6 +133,7 @@ export interface PlayerUpdate {
|
||||
type: GameUpdateType.Player;
|
||||
nameViewData?: NameViewData;
|
||||
clientID: ClientID | null;
|
||||
pattern: string | undefined;
|
||||
flag: string | undefined;
|
||||
name: string;
|
||||
displayName: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Config } from "../configuration/Config";
|
||||
import { PatternDecoder } from "../Cosmetics";
|
||||
import { ClientID, GameID } from "../Schemas";
|
||||
import { createRandomName } from "../Util";
|
||||
import { WorkerClient } from "../worker/WorkerClient";
|
||||
@@ -138,6 +139,7 @@ export class UnitView {
|
||||
|
||||
export class PlayerView {
|
||||
public anonymousName: string | null = null;
|
||||
private decoder?: PatternDecoder;
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
@@ -152,6 +154,12 @@ export class PlayerView {
|
||||
this.data.playerType,
|
||||
);
|
||||
}
|
||||
this.decoder =
|
||||
data.pattern === undefined ? undefined : new PatternDecoder(data.pattern);
|
||||
}
|
||||
|
||||
patternDecoder(): PatternDecoder | undefined {
|
||||
return this.decoder;
|
||||
}
|
||||
|
||||
async actions(tile: TileRef): Promise<PlayerActions> {
|
||||
@@ -197,6 +205,11 @@ export class PlayerView {
|
||||
flag(): string | undefined {
|
||||
return this.data.flag;
|
||||
}
|
||||
|
||||
pattern(): string | undefined {
|
||||
return this.data.pattern;
|
||||
}
|
||||
|
||||
name(): string {
|
||||
return this.anonymousName !== null && userSettings.anonymousNames()
|
||||
? this.anonymousName
|
||||
@@ -295,6 +308,7 @@ export class PlayerView {
|
||||
}
|
||||
info(): PlayerInfo {
|
||||
return new PlayerInfo(
|
||||
this.pattern(),
|
||||
this.flag(),
|
||||
this.name(),
|
||||
this.type(),
|
||||
|
||||
@@ -81,7 +81,6 @@ export class PlayerImpl implements Player {
|
||||
public _units: Unit[] = [];
|
||||
public _tiles: Set<TileRef> = new Set();
|
||||
|
||||
private _flag: string | undefined;
|
||||
private _name: string;
|
||||
private _displayName: string;
|
||||
|
||||
@@ -109,7 +108,6 @@ export class PlayerImpl implements Player {
|
||||
startTroops: number,
|
||||
private readonly _team: Team | null,
|
||||
) {
|
||||
this._flag = playerInfo.flag;
|
||||
this._name = sanitizeUsername(playerInfo.name);
|
||||
this._targetTroopRatio = 95n;
|
||||
this._troops = toInt(startTroops);
|
||||
@@ -130,6 +128,7 @@ export class PlayerImpl implements Player {
|
||||
return {
|
||||
type: GameUpdateType.Player,
|
||||
clientID: this.clientID(),
|
||||
pattern: this.pattern(),
|
||||
flag: this.flag(),
|
||||
name: this.name(),
|
||||
displayName: this.displayName(),
|
||||
@@ -178,8 +177,12 @@ export class PlayerImpl implements Player {
|
||||
return this._smallID;
|
||||
}
|
||||
|
||||
pattern(): string | undefined {
|
||||
return this.playerInfo.pattern;
|
||||
}
|
||||
|
||||
flag(): string | undefined {
|
||||
return this._flag;
|
||||
return this.playerInfo.flag;
|
||||
}
|
||||
|
||||
name(): string {
|
||||
|
||||
@@ -33,6 +33,10 @@ export class UserSettings {
|
||||
return this.get("settings.leftClickOpensMenu", false);
|
||||
}
|
||||
|
||||
territoryPatterns() {
|
||||
return this.get("settings.territoryPatterns", true);
|
||||
}
|
||||
|
||||
focusLocked() {
|
||||
return false;
|
||||
// TODO: renable when performance issues are fixed.
|
||||
@@ -59,6 +63,10 @@ export class UserSettings {
|
||||
this.set("settings.specialEffects", !this.fxLayer());
|
||||
}
|
||||
|
||||
toggleTerritoryPatterns() {
|
||||
this.set("settings.territoryPatterns", !this.territoryPatterns());
|
||||
}
|
||||
|
||||
toggleDarkMode() {
|
||||
this.set("settings.darkMode", !this.darkMode());
|
||||
if (this.darkMode()) {
|
||||
@@ -67,4 +75,14 @@ export class UserSettings {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
|
||||
private readonly PATTERN_KEY = "territoryPattern";
|
||||
|
||||
getSelectedPattern(): string | undefined {
|
||||
return localStorage.getItem(this.PATTERN_KEY) ?? undefined;
|
||||
}
|
||||
|
||||
setSelectedPattern(base64: string): void {
|
||||
localStorage.setItem(this.PATTERN_KEY, base64);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user