Merge branch 'main' into ui/modal-overlay-improvements

This commit is contained in:
q8gazy
2025-02-12 06:58:36 +03:00
committed by GitHub
318 changed files with 30438 additions and 17781 deletions
+7
View File
@@ -48,6 +48,7 @@ import { GameView, PlayerView } from "../core/game/GameView";
import { GameUpdateViewData } from "../core/game/GameUpdates";
export interface LobbyConfig {
flag: () => string;
playerName: () => string;
clientID: ClientID;
playerID: PlayerID;
@@ -56,6 +57,9 @@ export interface LobbyConfig {
gameID: GameID;
map: GameMapType | null;
difficulty: Difficulty | null;
disableBots: boolean | null;
disableNPCs: boolean | null;
creativeMode: boolean | null;
}
export function joinLobby(
@@ -77,6 +81,9 @@ export function joinLobby(
gameType: GameType.Singleplayer,
gameMap: lobbyConfig.map,
difficulty: lobbyConfig.difficulty,
disableBots: lobbyConfig.disableBots,
disableNPCs: lobbyConfig.disableNPCs,
creativeMode: lobbyConfig.creativeMode,
};
}
+18
View File
@@ -475,6 +475,24 @@ export class HostLobbyModal extends LitElement {
this.putGameConfig();
}
private async handleDisableBotsChange(e: Event) {
this.disableBots = Boolean((e.target as HTMLInputElement).checked);
consolex.log(`updating disable bots to ${this.disableBots}`);
this.putGameConfig();
}
private async handleDisableNPCsChange(e: Event) {
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
consolex.log(`updating disable npcs to ${this.disableNPCs}`);
this.putGameConfig();
}
private async handleCreativeModeChange(e: Event) {
this.creativeMode = Boolean((e.target as HTMLInputElement).checked);
consolex.log(`updating creative mode to ${this.creativeMode}`);
this.putGameConfig();
}
private async putGameConfig() {
const response = await fetch(`/private_lobby/${this.lobbyId}`, {
method: "PUT",
+92 -32
View File
@@ -2,46 +2,31 @@ import { EventBus, GameEvent } from "../core/EventBus";
import { Game } from "../core/game/Game";
export class MouseUpEvent implements GameEvent {
constructor(
public readonly x: number,
public readonly y: number,
) {}
constructor(public readonly x: number, public readonly y: number) {}
}
export class MouseDownEvent implements GameEvent {
constructor(
public readonly x: number,
public readonly y: number,
) {}
constructor(public readonly x: number, public readonly y: number) {}
}
export class MouseMoveEvent implements GameEvent {
constructor(
public readonly x: number,
public readonly y: number,
) {}
constructor(public readonly x: number, public readonly y: number) {}
}
export class ContextMenuEvent implements GameEvent {
constructor(
public readonly x: number,
public readonly y: number,
) {}
constructor(public readonly x: number, public readonly y: number) {}
}
export class ZoomEvent implements GameEvent {
constructor(
public readonly x: number,
public readonly y: number,
public readonly delta: number,
public readonly delta: number
) {}
}
export class DragEvent implements GameEvent {
constructor(
public readonly deltaX: number,
public readonly deltaY: number,
) {}
constructor(public readonly deltaX: number, public readonly deltaY: number) {}
}
export class AlternateViewEvent implements GameEvent {
@@ -51,10 +36,7 @@ export class AlternateViewEvent implements GameEvent {
export class RefreshGraphicsEvent implements GameEvent {}
export class ShowBuildMenuEvent implements GameEvent {
constructor(
public readonly x: number,
public readonly y: number,
) {}
constructor(public readonly x: number, public readonly y: number) {}
}
export class InputHandler {
@@ -72,10 +54,13 @@ export class InputHandler {
private alternateView = false;
constructor(
private canvas: HTMLCanvasElement,
private eventBus: EventBus,
) {}
private moveInterval: any = null;
private activeKeys = new Set<string>();
private readonly PAN_SPEED = 5;
private readonly ZOOM_SPEED = 10;
constructor(private canvas: HTMLCanvasElement, private eventBus: EventBus) {}
initialize() {
this.canvas.addEventListener("pointerdown", (e) => this.onPointerDown(e));
@@ -95,14 +80,67 @@ export class InputHandler {
});
this.pointers.clear();
// Initialize the combined movement interval
this.moveInterval = setInterval(() => {
let deltaX = 0;
let deltaY = 0;
// Handle both WASD and arrow keys
if (this.activeKeys.has("KeyW") || this.activeKeys.has("ArrowUp"))
deltaY += this.PAN_SPEED;
if (this.activeKeys.has("KeyS") || this.activeKeys.has("ArrowDown"))
deltaY -= this.PAN_SPEED;
if (this.activeKeys.has("KeyA") || this.activeKeys.has("ArrowLeft"))
deltaX += this.PAN_SPEED;
if (this.activeKeys.has("KeyD") || this.activeKeys.has("ArrowRight"))
deltaX -= this.PAN_SPEED;
if (deltaX !== 0 || deltaY !== 0) {
this.eventBus.emit(new DragEvent(deltaX, deltaY));
}
// Handle zooming
const screenCenterX = window.innerWidth / 2;
const screenCenterY = window.innerHeight / 2;
if (this.activeKeys.has("Minus")) {
this.eventBus.emit(
new ZoomEvent(screenCenterX, screenCenterY, this.ZOOM_SPEED)
);
}
if (this.activeKeys.has("Equal")) {
this.eventBus.emit(
new ZoomEvent(screenCenterX, screenCenterY, -this.ZOOM_SPEED)
);
}
}, 1);
window.addEventListener("keydown", (e) => {
if (e.code === "Space") {
e.preventDefault(); // Prevent page scrolling
e.preventDefault();
if (!this.alternateView) {
this.alternateView = true;
this.eventBus.emit(new AlternateViewEvent(true));
}
}
// Add all movement keys to activeKeys
if (
[
"KeyW",
"KeyA",
"KeyS",
"KeyD",
"ArrowUp",
"ArrowLeft",
"ArrowDown",
"ArrowRight",
"Minus",
"Equal",
].includes(e.code)
) {
this.activeKeys.add(e.code);
}
});
window.addEventListener("keyup", (e) => {
@@ -115,6 +153,24 @@ export class InputHandler {
e.preventDefault();
this.eventBus.emit(new RefreshGraphicsEvent());
}
// Remove all movement keys from activeKeys
if (
[
"KeyW",
"KeyA",
"KeyS",
"KeyD",
"ArrowUp",
"ArrowLeft",
"ArrowDown",
"ArrowRight",
"Minus",
"Equal",
].includes(e.code)
) {
this.activeKeys.delete(e.code);
}
});
}
@@ -193,10 +249,9 @@ export class InputHandler {
const pinchDelta = currentPinchDistance - this.lastPinchDistance;
if (Math.abs(pinchDelta) > 1) {
// Threshold to avoid tiny zoom adjustments
const zoomCenter = this.getPinchCenter();
this.eventBus.emit(
new ZoomEvent(zoomCenter.x, zoomCenter.y, -pinchDelta * 2),
new ZoomEvent(zoomCenter.x, zoomCenter.y, -pinchDelta * 2)
);
this.lastPinchDistance = currentPinchDistance;
}
@@ -222,4 +277,9 @@ export class InputHandler {
y: (pointerEvents[0].clientY + pointerEvents[1].clientY) / 2,
};
}
destroy() {
clearInterval(this.moveInterval);
this.activeKeys.clear();
}
}
+150 -127
View File
@@ -1,156 +1,179 @@
import { ClientGameRunner, joinLobby } from "./ClientGameRunner";
import backgroundImage from "../../resources/images/EuropeBackground.svg";
import favicon from "../../resources/images/Favicon.svg";
import { ClientGameRunner, joinLobby } from './ClientGameRunner';
import backgroundImage from '../../resources/images/EuropeBackground.svg';
import favicon from '../../resources/images/Favicon.svg';
import "./PublicLobby";
import "./UsernameInput";
import "./styles.css";
import { UsernameInput } from "./UsernameInput";
import { SinglePlayerModal } from "./SinglePlayerModal";
import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
import { generateID } from "../core/Util";
import { generateCryptoRandomUUID } from "./Utils";
import { consolex } from "../core/Consolex";
import './PublicLobby';
import './UsernameInput';
import './styles.css';
import { UsernameInput } from './UsernameInput';
import { SinglePlayerModal } from './SinglePlayerModal';
import { HostLobbyModal as HostPrivateLobbyModal } from './HostLobbyModal';
import { JoinPrivateLobbyModal } from './JoinPrivateLobbyModal';
import { generateID } from '../core/Util';
import { generateCryptoRandomUUID } from './Utils';
import { consolex } from '../core/Consolex';
import './components/FlagInput';
import { FlagInput } from './components/FlagInput';
class Client {
private gameStop: () => void;
private gameStop: () => void;
private usernameInput: UsernameInput | null = null;
private usernameInput: UsernameInput | null = null;
private flagInput: FlagInput | null = null;
private joinModal: JoinPrivateLobbyModal;
constructor() {}
private joinModal: JoinPrivateLobbyModal;
constructor() {}
initialize(): void {
this.usernameInput = document.querySelector(
"username-input"
) as UsernameInput;
if (!this.usernameInput) {
consolex.warn("Username input element not found");
}
window.addEventListener("beforeunload", (event) => {
consolex.log("Browser is closing");
if (this.gameStop != null) {
this.gameStop();
}
});
initialize(): void {
this.flagInput = document.querySelector('flag-input') as FlagInput;
if (!this.flagInput) {
consolex.warn('Flag input element not found');
}
setFavicon();
document.addEventListener("join-lobby", this.handleJoinLobby.bind(this));
document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this));
document.addEventListener(
"single-player",
this.handleSinglePlayer.bind(this)
);
this.usernameInput = document.querySelector(
'username-input'
) as UsernameInput;
if (!this.usernameInput) {
consolex.warn('Username input element not found');
}
window.addEventListener('beforeunload', (event) => {
consolex.log('Browser is closing');
if (this.gameStop != null) {
this.gameStop();
}
});
const spModal = document.querySelector(
"single-player-modal"
) as SinglePlayerModal;
spModal instanceof SinglePlayerModal;
document.getElementById("single-player").addEventListener("click", () => {
if (this.usernameInput.isValid()) {
spModal.open();
}
});
setFavicon();
document.addEventListener(
'join-lobby',
this.handleJoinLobby.bind(this)
);
document.addEventListener(
'leave-lobby',
this.handleLeaveLobby.bind(this)
);
document.addEventListener(
'single-player',
this.handleSinglePlayer.bind(this)
);
const hostModal = document.querySelector(
"host-lobby-modal"
) as HostPrivateLobbyModal;
hostModal instanceof HostPrivateLobbyModal;
document
.getElementById("host-lobby-button")
.addEventListener("click", () => {
if (this.usernameInput.isValid()) {
hostModal.open();
}
});
const spModal = document.querySelector(
'single-player-modal'
) as SinglePlayerModal;
spModal instanceof SinglePlayerModal;
document
.getElementById('single-player')
.addEventListener('click', () => {
if (this.usernameInput.isValid()) {
spModal.open();
}
});
this.joinModal = document.querySelector(
"join-private-lobby-modal"
) as JoinPrivateLobbyModal;
this.joinModal instanceof JoinPrivateLobbyModal;
document
.getElementById("join-private-lobby-button")
.addEventListener("click", () => {
if (this.usernameInput.isValid()) {
this.joinModal.open();
}
});
}
const hostModal = document.querySelector(
'host-lobby-modal'
) as HostPrivateLobbyModal;
hostModal instanceof HostPrivateLobbyModal;
document
.getElementById('host-lobby-button')
.addEventListener('click', () => {
if (this.usernameInput.isValid()) {
hostModal.open();
}
});
private async handleJoinLobby(event: CustomEvent) {
const lobby = event.detail.lobby;
consolex.log(`joining lobby ${lobby.id}`);
if (this.gameStop != null) {
consolex.log("joining lobby, stopping existing game");
this.gameStop();
}
this.gameStop = joinLobby(
{
gameType: event.detail.gameType,
playerName: (): string => this.usernameInput.getCurrentUsername(),
gameID: lobby.id,
persistentID: getPersistentIDFromCookie(),
playerID: generateID(),
clientID: generateID(),
map: event.detail.map,
difficulty: event.detail.difficulty,
},
() => this.joinModal.close()
);
}
this.joinModal = document.querySelector(
'join-private-lobby-modal'
) as JoinPrivateLobbyModal;
this.joinModal instanceof JoinPrivateLobbyModal;
document
.getElementById('join-private-lobby-button')
.addEventListener('click', () => {
if (this.usernameInput.isValid()) {
this.joinModal.open();
}
});
}
private async handleLeaveLobby(event: CustomEvent) {
if (this.gameStop == null) {
return;
}
consolex.log("leaving lobby, cancelling game");
this.gameStop();
this.gameStop = null;
}
private async handleJoinLobby(event: CustomEvent) {
const lobby = event.detail.lobby;
consolex.log(`joining lobby ${lobby.id}`);
if (this.gameStop != null) {
consolex.log('joining lobby, stopping existing game');
this.gameStop();
}
this.gameStop = joinLobby(
{
gameType: event.detail.gameType,
flag: (): string => this.flagInput.getCurrentFlag(),
playerName: (): string =>
this.usernameInput.getCurrentUsername(),
gameID: lobby.id,
persistentID: getPersistentIDFromCookie(),
playerID: generateID(),
clientID: generateID(),
map: event.detail.map,
difficulty: event.detail.difficulty,
disableBots: event.detail.disableBots,
disableNPCs: event.detail.disableNPCs,
creativeMode: event.detail.creativeMode,
},
() => this.joinModal.close()
);
}
private async handleSinglePlayer(event: CustomEvent) {
alert("coming soon");
}
private async handleLeaveLobby(event: CustomEvent) {
if (this.gameStop == null) {
return;
}
consolex.log('leaving lobby, cancelling game');
this.gameStop();
this.gameStop = null;
}
private async handleSinglePlayer(event: CustomEvent) {
alert('coming soon');
}
}
// Initialize the client when the DOM is loaded
document.addEventListener("DOMContentLoaded", () => {
new Client().initialize();
document.addEventListener('DOMContentLoaded', () => {
new Client().initialize();
});
document.body.style.backgroundImage = `url(${backgroundImage})`;
function setFavicon(): void {
const link = document.createElement("link");
link.type = "image/x-icon";
link.rel = "shortcut icon";
link.href = favicon;
document.head.appendChild(link);
const link = document.createElement('link');
link.type = 'image/x-icon';
link.rel = 'shortcut icon';
link.href = favicon;
document.head.appendChild(link);
}
// WARNING: DO NOT EXPOSE THIS ID
export function getPersistentIDFromCookie(): string {
const COOKIE_NAME = "player_persistent_id";
const COOKIE_NAME = 'player_persistent_id';
// Try to get existing cookie
const cookies = document.cookie.split(";");
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie.split("=").map((c) => c.trim());
if (cookieName === COOKIE_NAME) {
return cookieValue;
}
}
// Try to get existing cookie
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie
.split('=')
.map((c) => c.trim());
if (cookieName === COOKIE_NAME) {
return cookieValue;
}
}
// If no cookie exists, create new ID and set cookie
const newID = generateCryptoRandomUUID();
document.cookie = [
`${COOKIE_NAME}=${newID}`,
`max-age=${5 * 365 * 24 * 60 * 60}`, // 5 years
"path=/",
"SameSite=Strict",
"Secure",
].join(";");
// If no cookie exists, create new ID and set cookie
const newID = generateCryptoRandomUUID();
document.cookie = [
`${COOKIE_NAME}=${newID}`,
`max-age=${5 * 365 * 24 * 60 * 60}`, // 5 years
'path=/',
'SameSite=Strict',
'Secure',
].join(';');
return newID;
return newID;
}
+34
View File
@@ -268,6 +268,31 @@ export class SinglePlayerModal extends LitElement {
/>
<label for="disable-npcs">Disable NPCs</label>
</div>
<div>
<input
type="checkbox"
id="disable-bots"
@change=${this.handleDisableBotsChange}
/>
<label for="disable-bots">Disable Bots</label>
</div>
<div>
<input
type="checkbox"
id="disable-npcs"
@change=${this.handleDisableNPCsChange}
/>
<label for="disable-npcs">Disable NPCs</label>
</div>
<div>
<input
type="checkbox"
id="creative-mode"
@change=${this.handleCreativeModeChange}
/>
<label for="creative-mode">Creative mode</label>
</div>
<div>
<input
@@ -308,6 +333,15 @@ export class SinglePlayerModal extends LitElement {
private handleCreativeModeChange(e: Event) {
this.creativeMode = Boolean((e.target as HTMLInputElement).checked);
}
private handleDisableBotsChange(e: Event) {
this.disableBots = Boolean((e.target as HTMLInputElement).checked);
}
private handleDisableNPCsChange(e: Event) {
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
}
private handleCreativeModeChange(e: Event) {
this.creativeMode = Boolean((e.target as HTMLInputElement).checked);
}
private startGame() {
consolex.log(
`Starting single player game with map: ${GameMapType[this.selectedMap]}`
+1
View File
@@ -340,6 +340,7 @@ export class Transport {
type: "spawn",
clientID: this.lobbyConfig.clientID,
playerID: this.lobbyConfig.playerID,
flag: this.lobbyConfig.flag(),
name: this.lobbyConfig.playerName(),
playerType: PlayerType.Human,
x: event.cell.x,
+3 -2
View File
@@ -3,6 +3,7 @@ export function renderTroops(troops: number): string {
}
export function renderNumber(num: number) {
num = Math.max(num, 0);
let numStr = "";
if (num >= 10_000_000) {
numStr = (num / 1000000).toFixed(1) + "M";
@@ -51,7 +52,7 @@ export function generateCryptoRandomUUID(): string {
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16),
).toString(16)
);
}
@@ -63,6 +64,6 @@ export function generateCryptoRandomUUID(): string {
const r: number = (Math.random() * 16) | 0;
const v: number = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
},
}
);
}
+230
View File
@@ -0,0 +1,230 @@
import { LitElement, html, css } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import Countries from "../data/countries.json";
const flagKey: string = "flag";
@customElement("flag-input")
export class FlagInput extends LitElement {
@state() private flag: string = "";
@state() private search: string = "";
@state() private showModal: boolean = false;
static styles = css`
.hidden {
display: none;
}
.flag-container {
display: flex;
}
.no-selected-flag {
position: absolute;
left: 8px;
top: 8px;
height: 50px;
border-radius: 0.75rem;
border: none;
background: none;
font-size: 1rem;
cursor: pointer;
}
.selected-flag {
width: 48px;
cursor: pointer;
position: absolute;
left: 24px;
top: 14px;
border: 1px solid black;
}
.flag-modal {
display: flex;
flex-direction: column;
gap: 0.5rem;
position: absolute;
top: 60px;
left: 0;
width: 560px;
height: 500px;
background-color: rgb(35 35 35 / 0.8);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
padding: 10px;
border-radius: 8px;
}
.flag-search {
height: 2rem;
border-radius: 8px;
border: none;
text-align: center;
font-size: 1.3rem;
}
.flag-dropdown {
overflow-y: auto;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
}
.dropdown-item {
opacity: 0.7;
width: calc(100% / 4 - 15px);
text-align: center;
color: white;
cursor: pointer;
border: none;
background: none;
}
.dropdown-item:hover {
opacity: 1;
}
.country-flag {
width: 100%;
height: auto;
}
@media (max-width: 768px) {
.flag-modal {
left: 0px;
width: calc(100% - 16px);
height: 50vh;
}
.dropdown-item {
width: calc(100% / 3 - 15px);
}
}
`;
private handleSearch(e: Event) {
this.search = String((e.target as HTMLInputElement).value);
}
private setFlag(flag: string) {
this.flag = flag;
this.showModal = false;
this.storeFlag(flag);
}
public getCurrentFlag(): string {
return this.flag;
}
private getStoredFlag(): string {
const storedFlag = localStorage.getItem(flagKey);
if (storedFlag) {
return storedFlag;
}
return "";
}
private storeFlag(flag: string) {
if (flag) {
localStorage.setItem(flagKey, flag);
} else if (flag === "") {
localStorage.removeItem(flagKey);
}
}
private dispatchFlagEvent() {
this.dispatchEvent(
new CustomEvent("flag-change", {
detail: { flag: this.flag },
bubbles: true,
composed: true,
})
);
}
connectedCallback() {
super.connectedCallback();
this.flag = this.getStoredFlag();
this.dispatchFlagEvent();
}
render() {
return html`
<div class="flag-container">
${this.flag === ""
? html` <button
class="no-selected-flag"
@click=${() => (this.showModal = true)}
>
Flags
</button>`
: html`<img
class="selected-flag"
src="flags/${this.flag}.svg"
@click=${() => (this.showModal = true)}
/>`}
${this.showModal
? html`
<div
class="flag-modal ${this.showModal
? ""
: "hidden"}"
>
<input
class="flag-search"
type="text"
placeholder="Search..."
@change=${this.handleSearch}
@keyup=${this.handleSearch}
/>
<div class="flag-dropdown">
<!-- Show each flag as button -->
<button
@click=${() => this.setFlag("")}
class="dropdown-item"
>
<img
class="country-flag"
src="flags/none.svg"
/>
<span class="country-name">None</span>
</button>
${Countries.filter(
(country) =>
country.name
.toLowerCase()
.includes(
this.search.toLowerCase()
) ||
country.code
.toLowerCase()
.includes(
this.search.toLowerCase()
)
).map(
(country) => html`
<button
@click=${() =>
this.setFlag(country.code)}
class="dropdown-item"
>
<img
class="country-flag"
src="flags/${country.code}.svg"
/>
<span class="country-name"
>${country.name}</span
>
</button>
`
)}
</div>
</div>
`
: ""}
</div>
`;
}
}
+974
View File
@@ -0,0 +1,974 @@
[
{
"name": "Afghanistan",
"code": "af"
},
{
"name": "Åland Islands",
"code": "ax"
},
{
"name": "Albania",
"code": "al"
},
{
"name": "Algeria",
"code": "dz"
},
{
"name": "American Samoa",
"code": "as"
},
{
"name": "AndorrA",
"code": "ad"
},
{
"name": "Angola",
"code": "ao"
},
{
"name": "Anguilla",
"code": "ai"
},
{
"name": "Antarctica",
"code": "aq"
},
{
"name": "Antigua and Barbuda",
"code": "ag"
},
{
"name": "Argentina",
"code": "ar"
},
{
"name": "Armenia",
"code": "am"
},
{
"name": "Aruba",
"code": "aw"
},
{
"name": "Australia",
"code": "au"
},
{
"name": "Austria",
"code": "at"
},
{
"name": "Azerbaijan",
"code": "az"
},
{
"name": "Bahamas",
"code": "bs"
},
{
"name": "Bahrain",
"code": "bh"
},
{
"name": "Bangladesh",
"code": "bd"
},
{
"name": "Barbados",
"code": "bb"
},
{
"name": "Belarus",
"code": "by"
},
{
"name": "Belgium",
"code": "be"
},
{
"name": "Belize",
"code": "bz"
},
{
"name": "Benin",
"code": "bj"
},
{
"name": "Bermuda",
"code": "bm"
},
{
"name": "Bhutan",
"code": "bt"
},
{
"name": "Bolivia",
"code": "bo"
},
{
"name": "Bosnia and Herzegovina",
"code": "ba"
},
{
"name": "Botswana",
"code": "bw"
},
{
"name": "Bouvet Island",
"code": "bv"
},
{
"name": "Brazil",
"code": "br"
},
{
"name": "British Indian Ocean Territory",
"code": "io"
},
{
"name": "Brunei Darussalam",
"code": "bn"
},
{
"name": "Bulgaria",
"code": "bg"
},
{
"name": "Burkina Faso",
"code": "bf"
},
{
"name": "Burundi",
"code": "bi"
},
{
"name": "Cambodia",
"code": "kh"
},
{
"name": "Cameroon",
"code": "cm"
},
{
"name": "Canada",
"code": "ca"
},
{
"name": "Cape Verde",
"code": "cv"
},
{
"name": "Cayman Islands",
"code": "ky"
},
{
"name": "Central African Republic",
"code": "cf"
},
{
"name": "Chad",
"code": "td"
},
{
"name": "Chile",
"code": "cl"
},
{
"name": "China",
"code": "cn"
},
{
"name": "Christmas Island",
"code": "cx"
},
{
"name": "Cocos (Keeling) Islands",
"code": "cc"
},
{
"name": "Colombia",
"code": "co"
},
{
"name": "Comoros",
"code": "km"
},
{
"name": "Congo",
"code": "cg"
},
{
"name": "Congo, The Democratic Republic of the",
"code": "cd"
},
{
"name": "Cook Islands",
"code": "ck"
},
{
"name": "Costa Rica",
"code": "cr"
},
{
"name": "Cote D'Ivoire",
"code": "ci"
},
{
"name": "Croatia",
"code": "hr"
},
{
"name": "Cuba",
"code": "cu"
},
{
"name": "Cyprus",
"code": "cy"
},
{
"name": "Czech Republic",
"code": "cz"
},
{
"name": "Denmark",
"code": "dk"
},
{
"name": "Djibouti",
"code": "dj"
},
{
"name": "Dominica",
"code": "dm"
},
{
"name": "Dominican Republic",
"code": "do"
},
{
"name": "Ecuador",
"code": "ec"
},
{
"name": "Egypt",
"code": "eg"
},
{
"name": "El Salvador",
"code": "sv"
},
{
"name": "Equatorial Guinea",
"code": "gq"
},
{
"name": "Eritrea",
"code": "er"
},
{
"name": "Estonia",
"code": "ee"
},
{
"name": "Ethiopia",
"code": "et"
},
{
"name": "Falkland Islands (Malvinas)",
"code": "fk"
},
{
"name": "Faroe Islands",
"code": "fo"
},
{
"name": "Fiji",
"code": "fj"
},
{
"name": "Finland",
"code": "fi"
},
{
"name": "France",
"code": "fr"
},
{
"name": "French Guiana",
"code": "gf"
},
{
"name": "French Polynesia",
"code": "pf"
},
{
"name": "French Southern Territories",
"code": "tf"
},
{
"name": "Gabon",
"code": "ga"
},
{
"name": "Gambia",
"code": "gm"
},
{
"name": "Georgia",
"code": "ge"
},
{
"name": "Germany",
"code": "de"
},
{
"name": "Ghana",
"code": "gh"
},
{
"name": "Gibraltar",
"code": "gi"
},
{
"name": "Greece",
"code": "gr"
},
{
"name": "Greenland",
"code": "gl"
},
{
"name": "Grenada",
"code": "gd"
},
{
"name": "Guadeloupe",
"code": "gp"
},
{
"name": "Guam",
"code": "gu"
},
{
"name": "Guatemala",
"code": "gt"
},
{
"name": "Guernsey",
"code": "gg"
},
{
"name": "Guinea",
"code": "gn"
},
{
"name": "Guinea-Bissau",
"code": "gw"
},
{
"name": "Guyana",
"code": "gy"
},
{
"name": "Haiti",
"code": "ht"
},
{
"name": "Heard Island and Mcdonald Islands",
"code": "hm"
},
{
"name": "Holy See (Vatican City State)",
"code": "va"
},
{
"name": "Honduras",
"code": "hn"
},
{
"name": "Hong Kong",
"code": "hk"
},
{
"name": "Hungary",
"code": "hu"
},
{
"name": "Iceland",
"code": "is"
},
{
"name": "India",
"code": "in"
},
{
"name": "Indonesia",
"code": "id"
},
{
"name": "Iran, Islamic Republic Of",
"code": "ir"
},
{
"name": "Iraq",
"code": "iq"
},
{
"name": "Ireland",
"code": "ie"
},
{
"name": "Isle of Man",
"code": "im"
},
{
"name": "Israel",
"code": "il"
},
{
"name": "Italy",
"code": "it"
},
{
"name": "Jamaica",
"code": "jm"
},
{
"name": "Japan",
"code": "jp"
},
{
"name": "Jersey",
"code": "je"
},
{
"name": "Jordan",
"code": "jo"
},
{
"name": "Kazakhstan",
"code": "kz"
},
{
"name": "Kenya",
"code": "ke"
},
{
"name": "Kiribati",
"code": "ki"
},
{
"name": "Korea, Democratic People'S Republic of",
"code": "kp"
},
{
"name": "Korea, Republic of",
"code": "kr"
},
{
"name": "Kuwait",
"code": "kw"
},
{
"name": "Kyrgyzstan",
"code": "kg"
},
{
"name": "Lao People'S Democratic Republic",
"code": "la"
},
{
"name": "Latvia",
"code": "lv"
},
{
"name": "Lebanon",
"code": "lb"
},
{
"name": "Lesotho",
"code": "ls"
},
{
"name": "Liberia",
"code": "lr"
},
{
"name": "Libyan Arab Jamahiriya",
"code": "ly"
},
{
"name": "Liechtenstein",
"code": "li"
},
{
"name": "Lithuania",
"code": "lt"
},
{
"name": "Luxembourg",
"code": "lu"
},
{
"name": "Macao",
"code": "mo"
},
{
"name": "Macedonia, The Former Yugoslav Republic of",
"code": "mk"
},
{
"name": "Madagascar",
"code": "mg"
},
{
"name": "Malawi",
"code": "mw"
},
{
"name": "Malaysia",
"code": "my"
},
{
"name": "Maldives",
"code": "mv"
},
{
"name": "Mali",
"code": "ml"
},
{
"name": "Malta",
"code": "mt"
},
{
"name": "Marshall Islands",
"code": "mh"
},
{
"name": "Martinique",
"code": "mq"
},
{
"name": "Mauritania",
"code": "mr"
},
{
"name": "Mauritius",
"code": "mu"
},
{
"name": "Mayotte",
"code": "yt"
},
{
"name": "Mexico",
"code": "mx"
},
{
"name": "Micronesia, Federated States of",
"code": "fm"
},
{
"name": "Moldova, Republic of",
"code": "md"
},
{
"name": "Monaco",
"code": "mc"
},
{
"name": "Mongolia",
"code": "mn"
},
{
"name": "Montserrat",
"code": "ms"
},
{
"name": "Morocco",
"code": "ma"
},
{
"name": "Mozambique",
"code": "mz"
},
{
"name": "Myanmar",
"code": "mm"
},
{
"name": "Namibia",
"code": "na"
},
{
"name": "Nauru",
"code": "nr"
},
{
"name": "Nepal",
"code": "np"
},
{
"name": "Netherlands",
"code": "nl"
},
{
"name": "Netherlands Antilles",
"code": "an"
},
{
"name": "New Caledonia",
"code": "nc"
},
{
"name": "New Zealand",
"code": "nz"
},
{
"name": "Nicaragua",
"code": "ni"
},
{
"name": "Niger",
"code": "ne"
},
{
"name": "Nigeria",
"code": "ng"
},
{
"name": "Niue",
"code": "nu"
},
{
"name": "Norfolk Island",
"code": "nf"
},
{
"name": "Northern Mariana Islands",
"code": "mp"
},
{
"name": "Norway",
"code": "no"
},
{
"name": "Oman",
"code": "om"
},
{
"name": "Pakistan",
"code": "pk"
},
{
"name": "Palau",
"code": "pw"
},
{
"name": "Palestinian Territory, Occupied",
"code": "ps"
},
{
"name": "Panama",
"code": "pa"
},
{
"name": "Papua New Guinea",
"code": "pg"
},
{
"name": "Paraguay",
"code": "py"
},
{
"name": "Peru",
"code": "pe"
},
{
"name": "Philippines",
"code": "ph"
},
{
"name": "Pitcairn",
"code": "pn"
},
{
"name": "Poland",
"code": "pl"
},
{
"name": "Portugal",
"code": "pt"
},
{
"name": "Puerto Rico",
"code": "pr"
},
{
"name": "Qatar",
"code": "qa"
},
{
"name": "Reunion",
"code": "re"
},
{
"name": "Romania",
"code": "ro"
},
{
"name": "Russian Federation",
"code": "ru"
},
{
"name": "RWANDA",
"code": "rw"
},
{
"name": "Saint Helena",
"code": "sh"
},
{
"name": "Saint Kitts and Nevis",
"code": "kn"
},
{
"name": "Saint Lucia",
"code": "lc"
},
{
"name": "Saint Pierre and Miquelon",
"code": "pm"
},
{
"name": "Saint Vincent and the Grenadines",
"code": "vc"
},
{
"name": "Samoa",
"code": "ws"
},
{
"name": "San Marino",
"code": "sm"
},
{
"name": "Sao Tome and Principe",
"code": "st"
},
{
"name": "Saudi Arabia",
"code": "sa"
},
{
"name": "Senegal",
"code": "sn"
},
{
"name": "Serbia and Montenegro",
"code": "cs"
},
{
"name": "Seychelles",
"code": "sc"
},
{
"name": "Sierra Leone",
"code": "sl"
},
{
"name": "Singapore",
"code": "sg"
},
{
"name": "Slovakia",
"code": "sk"
},
{
"name": "Slovenia",
"code": "si"
},
{
"name": "Solomon Islands",
"code": "sb"
},
{
"name": "Somalia",
"code": "so"
},
{
"name": "South Africa",
"code": "za"
},
{
"name": "South Georgia and the South Sandwich Islands",
"code": "gs"
},
{
"name": "Spain",
"code": "es"
},
{
"name": "Sri Lanka",
"code": "lk"
},
{
"name": "Sudan",
"code": "sd"
},
{
"name": "Suriname",
"code": "sr"
},
{
"name": "Svalbard and Jan Mayen",
"code": "sj"
},
{
"name": "Swaziland",
"code": "sz"
},
{
"name": "Sweden",
"code": "se"
},
{
"name": "Switzerland",
"code": "ch"
},
{
"name": "Syrian Arab Republic",
"code": "sy"
},
{
"name": "Taiwan, Province of China",
"code": "tw"
},
{
"name": "Tajikistan",
"code": "tj"
},
{
"name": "Tanzania, United Republic of",
"code": "tz"
},
{
"name": "Thailand",
"code": "th"
},
{
"name": "Timor-Leste",
"code": "tl"
},
{
"name": "Togo",
"code": "tg"
},
{
"name": "Tokelau",
"code": "tk"
},
{
"name": "Tonga",
"code": "to"
},
{
"name": "Trinidad and Tobago",
"code": "tt"
},
{
"name": "Tunisia",
"code": "tn"
},
{
"name": "Turkey",
"code": "tr"
},
{
"name": "Turkmenistan",
"code": "tm"
},
{
"name": "Turks and Caicos Islands",
"code": "tc"
},
{
"name": "Tuvalu",
"code": "tv"
},
{
"name": "Uganda",
"code": "ug"
},
{
"name": "Ukraine",
"code": "ua"
},
{
"name": "United Arab Emirates",
"code": "ae"
},
{
"name": "United Kingdom",
"code": "gb"
},
{
"name": "United States",
"code": "us"
},
{
"name": "United States Minor Outlying Islands",
"code": "um"
},
{
"name": "Uruguay",
"code": "uy"
},
{
"name": "Uzbekistan",
"code": "uz"
},
{
"name": "Vanuatu",
"code": "vu"
},
{
"name": "Venezuela",
"code": "ve"
},
{
"name": "Viet Nam",
"code": "vn"
},
{
"name": "Virgin Islands, British",
"code": "vg"
},
{
"name": "Virgin Islands, U.S.",
"code": "vi"
},
{
"name": "Wallis and Futuna",
"code": "wf"
},
{
"name": "Western Sahara",
"code": "eh"
},
{
"name": "Yemen",
"code": "ye"
},
{
"name": "Zambia",
"code": "zm"
},
{
"name": "Zimbabwe",
"code": "zw"
}
]
+3 -1
View File
@@ -15,6 +15,7 @@ import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg";
import missileSiloIcon from "../../../../resources/images/MissileSiloIconWhite.svg";
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
import portIcon from "../../../../resources/images/PortIcon.svg";
import mirvIcon from "../../../../resources/images/MIRVIcon.svg";
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
import shieldIcon from "../../../../resources/images/ShieldIconWhite.svg";
import { renderNumber } from "../../Utils";
@@ -28,7 +29,8 @@ interface BuildItemDisplay {
const buildTable: BuildItemDisplay[][] = [
[
{ unitType: UnitType.AtomBomb, icon: atomBombIcon },
{ unitType: UnitType.MIRV, icon: hydrogenBombIcon },
{ unitType: UnitType.MIRV, icon: mirvIcon },
{ unitType: UnitType.HydrogenBomb, icon: hydrogenBombIcon },
{ unitType: UnitType.Warship, icon: warshipIcon },
{ unitType: UnitType.Port, icon: portIcon },
{ unitType: UnitType.MissileSilo, icon: missileSiloIcon },
+2 -2
View File
@@ -195,7 +195,7 @@ export class ControlPanel extends LitElement implements Layer {
type="range"
min="1"
max="100"
.value=${this.targetTroopRatio * 100}
.value=${(this.targetTroopRatio * 100).toString()}
@input=${(e: Event) => {
this.targetTroopRatio =
parseInt((e.target as HTMLInputElement).value) / 100;
@@ -225,7 +225,7 @@ export class ControlPanel extends LitElement implements Layer {
type="range"
min="1"
max="100"
.value=${this.attackRatio * 100}
.value=${(this.attackRatio * 100).toString()}
@input=${(e: Event) => {
this.attackRatio =
parseInt((e.target as HTMLInputElement).value) / 100;
+104 -16
View File
@@ -1,8 +1,11 @@
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { AllPlayers, MessageType } from "../../../core/game/Game";
import { DisplayMessageUpdate } from "../../../core/game/GameUpdates";
import { AllPlayers, MessageType, PlayerType } from "../../../core/game/Game";
import {
AttackUpdate,
DisplayMessageUpdate,
} from "../../../core/game/GameUpdates";
import { EmojiUpdate } from "../../../core/game/GameUpdates";
import { TargetPlayerUpdate } from "../../../core/game/GameUpdates";
import { AllianceExpiredUpdate } from "../../../core/game/GameUpdates";
@@ -16,6 +19,7 @@ import { SendAllianceReplyIntentEvent } from "../../Transport";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { onlyImages, sanitize } from "../../../core/Util";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { renderTroops } from "../../Utils";
interface Event {
description: string;
@@ -38,6 +42,8 @@ export class EventsDisplay extends LitElement implements Layer {
public clientID: ClientID;
private events: Event[] = [];
@state() private incomingAttacks: AttackUpdate[] = [];
@state() private outgoingAttacks: AttackUpdate[] = [];
private updateMap = new Map([
[GameUpdateType.DisplayEvent, (u) => this.onDisplayMessageEvent(u)],
@@ -54,6 +60,8 @@ export class EventsDisplay extends LitElement implements Layer {
constructor() {
super();
this.events = [];
this.incomingAttacks = [];
this.outgoingAttacks = [];
}
init() {}
@@ -80,6 +88,31 @@ export class EventsDisplay extends LitElement implements Layer {
this.events = remainingEvents;
this.requestUpdate();
}
const myPlayer = this.game.myPlayer();
if (!myPlayer) {
return;
}
myPlayer.incomingAttacks().forEach((a) => {
console.log(
`got type: ${(
this.game.playerBySmallID(a.attackerID) as PlayerView
).type()}`
);
});
// Update attacks
this.incomingAttacks = myPlayer.incomingAttacks().filter((a) => {
const t = (this.game.playerBySmallID(a.attackerID) as PlayerView).type();
return t != PlayerType.Bot;
});
this.outgoingAttacks = myPlayer
.outgoingAttacks()
.filter((a) => a.targetID != 0);
this.requestUpdate();
}
private addEvent(event: Event) {
@@ -125,10 +158,10 @@ export class EventsDisplay extends LitElement implements Layer {
}
const requestor = this.game.playerBySmallID(
update.requestorID,
update.requestorID
) as PlayerView;
const recipient = this.game.playerBySmallID(
update.recipientID,
update.recipientID
) as PlayerView;
this.addEvent({
@@ -139,7 +172,7 @@ export class EventsDisplay extends LitElement implements Layer {
className: "btn",
action: () =>
this.eventBus.emit(
new SendAllianceReplyIntentEvent(requestor, recipient, true),
new SendAllianceReplyIntentEvent(requestor, recipient, true)
),
},
{
@@ -147,7 +180,7 @@ export class EventsDisplay extends LitElement implements Layer {
className: "btn-info",
action: () =>
this.eventBus.emit(
new SendAllianceReplyIntentEvent(requestor, recipient, false),
new SendAllianceReplyIntentEvent(requestor, recipient, false)
),
},
],
@@ -156,7 +189,7 @@ export class EventsDisplay extends LitElement implements Layer {
createdAt: this.game.ticks(),
onDelete: () =>
this.eventBus.emit(
new SendAllianceReplyIntentEvent(requestor, recipient, false),
new SendAllianceReplyIntentEvent(requestor, recipient, false)
),
});
}
@@ -168,7 +201,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
const recipient = this.game.playerBySmallID(
update.request.recipientID,
update.request.recipientID
) as PlayerView;
this.addEvent({
@@ -213,8 +246,8 @@ export class EventsDisplay extends LitElement implements Layer {
update.player1ID === myPlayer.smallID()
? update.player2ID
: update.player2ID === myPlayer.smallID()
? update.player1ID
: null;
? update.player1ID
: null;
const other = this.game.playerBySmallID(otherID) as PlayerView;
if (!other || !myPlayer.isAlive() || !other.isAlive()) return;
@@ -250,7 +283,7 @@ export class EventsDisplay extends LitElement implements Layer {
? AllPlayers
: this.game.playerBySmallID(update.emoji.recipientID);
const sender = this.game.playerBySmallID(
update.emoji.senderID,
update.emoji.senderID
) as PlayerView;
if (recipient == myPlayer) {
@@ -289,8 +322,62 @@ export class EventsDisplay extends LitElement implements Layer {
}
}
private renderAttacks() {
if (
this.incomingAttacks.length === 0 &&
this.outgoingAttacks.length === 0
) {
return html``;
}
return html`
${this.incomingAttacks.length > 0
? html`
<tr class="border-t border-gray-700">
<td class="lg:p-3 p-1 text-left text-red-400">
${this.incomingAttacks.map(
(attack) => html`
<div class="ml-2">
${renderTroops(attack.troops)}
${(
this.game.playerBySmallID(
attack.attackerID
) as PlayerView
)?.name()}
</div>
`
)}
</td>
</tr>
`
: ""}
${this.outgoingAttacks.length > 0
? html`
<tr class="border-t border-gray-700">
<td class="lg:p-3 p-1 text-left text-blue-400">
${this.outgoingAttacks.map(
(attack) => html`
<div class="ml-2">
${renderTroops(attack.troops)}
${(
this.game.playerBySmallID(attack.targetID) as PlayerView
)?.name()}
</div>
`
)}
</td>
</tr>
`
: ""}
`;
}
render() {
if (this.events.length === 0) {
if (
this.events.length === 0 &&
this.incomingAttacks.length === 0 &&
this.outgoingAttacks.length === 0
) {
return html``;
}
@@ -306,7 +393,7 @@ export class EventsDisplay extends LitElement implements Layer {
(event, index) => html`
<tr
class="border-b border-opacity-0 ${this.getMessageTypeClasses(
event.type,
event.type
)}"
>
<td class="lg:p-3 p-1 text-left">
@@ -331,15 +418,16 @@ export class EventsDisplay extends LitElement implements Layer {
>
${btn.text}
</button>
`,
`
)}
</div>
`
: ""}
</td>
</tr>
`,
`
)}
${this.renderAttacks()}
</tbody>
</table>
</div>
@@ -347,6 +435,6 @@ export class EventsDisplay extends LitElement implements Layer {
}
createRenderRoot() {
return this; // Required for Tailwind classes to work with Lit
return this;
}
}
+28 -10
View File
@@ -16,6 +16,7 @@ import targetIcon from "../../../../resources/images/TargetIcon.svg";
import { ClientID } from "../../../core/Schemas";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { createCanvas, renderTroops } from "../../Utils";
import { sanitize } from "../../../core/Util";
class RenderInfo {
public icons: Map<string, HTMLImageElement> = new Map(); // Track icon elements
@@ -142,33 +143,50 @@ export class NameLayer implements Layer {
element.style.alignItems = "center";
element.style.gap = "0px";
if (player.flag()) {
const flagImg = document.createElement("img");
flagImg.classList.add('player-flag');
flagImg.style.marginBottom = "-5%";
flagImg.style.opacity = '0.8';
flagImg.src = 'flags/' + sanitize(player.flag()) + '.svg';
flagImg.style.zIndex = "1";
flagImg.style.width = "40%";
flagImg.style.aspectRatio = "3/4";
element.appendChild(flagImg);
}
const nameDiv = document.createElement("div");
nameDiv.innerHTML = player.name();
nameDiv.classList.add('player-name');
nameDiv.innerHTML = (player.type() !== PlayerType.Human ? "🤖 " : '') + player.name();
nameDiv.style.color = this.theme.playerInfoColor(player.id()).toHex();
nameDiv.style.fontFamily = this.theme.font();
nameDiv.style.whiteSpace = "nowrap";
nameDiv.style.overflow = "hidden";
nameDiv.style.textOverflow = "ellipsis";
nameDiv.style.zIndex = "2";
nameDiv.style.zIndex = "3";
element.appendChild(nameDiv);
const troopsDiv = document.createElement("div");
troopsDiv.classList.add('player-troops');
troopsDiv.textContent = renderTroops(player.troops());
troopsDiv.style.color = this.theme.playerInfoColor(player.id()).toHex();
troopsDiv.style.fontFamily = this.theme.font();
troopsDiv.style.fontWeight = "bold";
troopsDiv.style.zIndex = "2";
troopsDiv.style.zIndex = "3";
element.appendChild(troopsDiv);
const iconsDiv = document.createElement("div");
iconsDiv.classList.add('player-icons');
iconsDiv.style.display = "flex";
iconsDiv.style.gap = "4px";
iconsDiv.style.justifyContent = "center";
iconsDiv.style.alignItems = "center";
iconsDiv.style.position = "absolute"; // Add this
iconsDiv.style.zIndex = "1"; // Add this
iconsDiv.style.width = "100%"; // Add this
iconsDiv.style.height = "100%"; // Add this
iconsDiv.style.position = "absolute";
iconsDiv.style.zIndex = "2";
iconsDiv.style.width = "100%";
iconsDiv.style.height = "100%";
element.appendChild(iconsDiv);
this.container.appendChild(element);
@@ -208,14 +226,14 @@ export class NameLayer implements Layer {
render.lastRenderCalc = now + this.rand.nextInt(0, 100);
// Update text sizes
const nameDiv = render.element.children[0] as HTMLDivElement;
const troopsDiv = render.element.children[1] as HTMLDivElement;
const nameDiv = render.element.querySelector(".player-name") as HTMLDivElement;
const troopsDiv = render.element.querySelector(".player-troops") as HTMLDivElement;
nameDiv.style.fontSize = `${render.fontSize}px`;
troopsDiv.style.fontSize = `${render.fontSize}px`;
troopsDiv.textContent = renderTroops(render.player.troops());
// Handle icons
const iconsDiv = render.element.children[2] as HTMLDivElement;
const iconsDiv = render.element.querySelector(".player-icons") as HTMLDivElement;
const iconSize = Math.min(render.fontSize * 1.5, 48);
const myPlayer = this.getPlayer();
+4 -2
View File
@@ -26,16 +26,19 @@ export class TopBar extends LitElement implements Layer {
if (!this.isVisible) {
return html``;
}
const myPlayer = this.game?.myPlayer();
if (!myPlayer?.isAlive() || this.game?.inSpawnPhase()) {
return html``;
}
const popRate = this.game.config().populationIncreaseRate(myPlayer) * 10;
const maxPop = this.game.config().maxPopulation(myPlayer);
const goldPerSecond = this.game.config().goldAdditionRate(myPlayer) * 10;
return html`
<div
class="fixed top-0 z-50 bg-black/90 text-white text-sm p-1 rounded grid grid-cols-1 sm:grid-cols-2 w-1/2 sm:w-2/3 md:w-1/2 lg:hidden"
class="fixed top-0 z-50 bg-gray-800/70 text-white text-sm p-1 rounded grid grid-cols-1 sm:grid-cols-2 w-1/2 sm:w-2/3 md:w-1/2 lg:hidden backdrop-blur"
>
<!-- Pop section (takes 2 columns on desktop) -->
<div
@@ -48,7 +51,6 @@ export class TopBar extends LitElement implements Layer {
>
<span>(+${renderTroops(popRate)})</span>
</div>
<!-- Gold section (takes 1 column on desktop) -->
<div
class="flex items-center space-x-2 overflow-x-auto whitespace-nowrap"
+6 -3
View File
@@ -80,7 +80,8 @@
v0.15.0
</div>
<div class="max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2 pb-4">
<div class="relative items-center max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2 pb-4">
<flag-input></flag-input>
<username-input class="w-full"></username-input>
</div>
@@ -146,8 +147,10 @@
<div class="flex sm:flex-row flex-col sm:gap-8 gap-2">
<a href="https://youtu.be/jvHEvbko3uw?si=znspkP84P76B1w5I"
class="text-white/70 hover:text-white transition-colors duration-300" target="_blank">How to Play</a>
<a href="https://discord.gg/k22YrnAzGp" class="text-white/70 hover:text-white transition-colors duration-300"
target="_blank">Discord</a>
<a href="https://discord.gg/k22YrnAzGp" class="text-white/70 hover:text-white transition-colors duration-300"
target="_blank">Discord</a>
<a href="https://openfront.fandom.com/wiki/Openfront_Wiki" class="text-white/70 hover:text-white transition-colors duration-300"
target="_blank">Wiki</a>
</div>
<div class="text-white/70">
© 2025