Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,20 @@
"disable_alliances": "Alliances Disabled",
"disable_alliances_label": "Alliances"
},
"modifier_description": {
"random_spawn": "Spawn location is chosen randomly",
"compact_map": "Map is 1/4 the size",
"crowded": "More players allowed on the map",
"hard_nations": "Stronger, more aggressive nation bots",
"starting_gold": "Start with bonus gold",
"gold_multiplier": "Gold income is multiplied",
"disable_alliances": "No alliances allowed",
"instant_build": "Buildings are placed instantly",
"infinite_gold": "Unlimited gold",
"infinite_troops": "Unlimited troops",
"donate_gold": "Send gold to other players",
"donate_troops": "Send troops to other players"
},
"select_lang": {
"title": "Select Language"
},
Expand Down
23 changes: 14 additions & 9 deletions src/client/GameModeSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import { terrainMapFileLoader } from "./TerrainMapFileLoader";
import { UsernameInput } from "./UsernameInput";
import {
calculateServerTimeOffset,
getActiveModifiers,
getMapName,
getModifierLabels,
getSecondsUntilServerTimestamp,
renderDuration,
translateText,
Expand Down Expand Up @@ -288,12 +288,16 @@ export class GameModeSelector extends LitElement {

const mapName = getMapName(lobby.gameConfig?.gameMap);

const modifierLabels = getModifierLabels(
const modifierInfos = getActiveModifiers(
lobby.gameConfig?.publicGameModifiers,
);
// Sort by length for visual consistency (shorter labels first)
if (modifierLabels.length > 1) {
modifierLabels.sort((a, b) => a.length - b.length);
// Sort by label length for visual consistency (shorter labels first)
if (modifierInfos.length > 1) {
modifierInfos.sort(
(a, b) =>
translateText(a.badgeKey, a.badgeParams).length -
translateText(b.badgeKey, b.badgeParams).length,
);
}

return html`
Expand All @@ -320,13 +324,14 @@ export class GameModeSelector extends LitElement {
<div
class="absolute inset-x-2 top-2 flex items-start justify-between gap-2"
>
${modifierLabels.length > 0
${modifierInfos.length > 0
? html`<div class="flex flex-col items-start gap-1 mt-[2px]">
${modifierLabels.map(
(label) =>
${modifierInfos.map(
(m) =>
html`<span
class="px-2 py-1 rounded text-xs font-bold uppercase tracking-widest bg-sky-600 text-white shadow-[0_0_6px_rgba(14,165,233,0.35)]"
>${label}</span
data-tooltip=${translateText(m.descriptionKey)}
>${translateText(m.badgeKey, m.badgeParams)}</span
>`,
)}
</div>`
Expand Down
10 changes: 10 additions & 0 deletions src/client/HostLobbyModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export class HostLobbyModal extends BaseModal {
></toggle-input-card>`,
html`<toggle-input-card
.labelKey=${"host_modal.gold_multiplier"}
.descriptionKey=${"modifier_description.gold_multiplier"}
.checked=${this.goldMultiplier}
.inputId=${"gold-multiplier-value"}
.inputMin=${0.1}
Expand All @@ -194,6 +195,7 @@ export class HostLobbyModal extends BaseModal {
></toggle-input-card>`,
html`<toggle-input-card
.labelKey=${"host_modal.starting_gold"}
.descriptionKey=${"modifier_description.starting_gold"}
.checked=${this.startingGold}
.inputId=${"starting-gold-value"}
.inputMin=${0.1}
Expand Down Expand Up @@ -270,34 +272,42 @@ export class HostLobbyModal extends BaseModal {
{
labelKey: "host_modal.instant_build",
checked: this.instantBuild,
descriptionKey: "modifier_description.instant_build",
},
{
labelKey: "host_modal.random_spawn",
checked: this.randomSpawn,
descriptionKey: "modifier_description.random_spawn",
},
{
labelKey: "host_modal.donate_gold",
checked: this.donateGold,
descriptionKey: "modifier_description.donate_gold",
},
{
labelKey: "host_modal.donate_troops",
checked: this.donateTroops,
descriptionKey: "modifier_description.donate_troops",
},
{
labelKey: "host_modal.infinite_gold",
checked: this.infiniteGold,
descriptionKey: "modifier_description.infinite_gold",
},
{
labelKey: "host_modal.infinite_troops",
checked: this.infiniteTroops,
descriptionKey: "modifier_description.infinite_troops",
},
{
labelKey: "host_modal.compact_map",
checked: this.compactMap,
descriptionKey: "modifier_description.compact_map",
},
{
labelKey: "host_modal.disable_alliances",
checked: this.disableAlliances,
descriptionKey: "modifier_description.disable_alliances",
},
],
inputCards,
Expand Down
1 change: 1 addition & 0 deletions src/client/JoinLobbyModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ export class JoinLobbyModal extends BaseModal {
(m.value !== undefined
? renderNumber(m.value)
: translateText("common.enabled"))}
.tooltip=${translateText(m.descriptionKey)}
></lobby-config-item>
`,
)}
Expand Down
8 changes: 8 additions & 0 deletions src/client/SinglePlayerModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export class SinglePlayerModal extends BaseModal {
></toggle-input-card>`,
html`<toggle-input-card
.labelKey=${"single_modal.gold_multiplier"}
.descriptionKey=${"modifier_description.gold_multiplier"}
.checked=${this.goldMultiplier}
.inputId=${"gold-multiplier-value"}
.inputMin=${0.1}
Expand All @@ -210,6 +211,7 @@ export class SinglePlayerModal extends BaseModal {
></toggle-input-card>`,
html`<toggle-input-card
.labelKey=${"single_modal.starting_gold"}
.descriptionKey=${"modifier_description.starting_gold"}
.checked=${this.startingGold}
.inputId=${"starting-gold-value"}
.inputMin=${0.1}
Expand Down Expand Up @@ -298,26 +300,32 @@ export class SinglePlayerModal extends BaseModal {
{
labelKey: "single_modal.instant_build",
checked: this.instantBuild,
descriptionKey: "modifier_description.instant_build",
},
{
labelKey: "single_modal.random_spawn",
checked: this.randomSpawn,
descriptionKey: "modifier_description.random_spawn",
},
{
labelKey: "single_modal.infinite_gold",
checked: this.infiniteGold,
descriptionKey: "modifier_description.infinite_gold",
},
{
labelKey: "single_modal.infinite_troops",
checked: this.infiniteTroops,
descriptionKey: "modifier_description.infinite_troops",
},
{
labelKey: "single_modal.compact_map",
checked: this.compactMap,
descriptionKey: "modifier_description.compact_map",
},
{
labelKey: "single_modal.disable_alliances",
checked: this.disableAlliances,
descriptionKey: "modifier_description.disable_alliances",
},
],
inputCards,
Expand Down
9 changes: 9 additions & 0 deletions src/client/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ export interface ModifierInfo {
labelKey: string;
/** Translation key for badge/short label (e.g. "public_game_modifier.random_spawn") */
badgeKey: string;
/** Translation key for tooltip description (e.g. "modifier_description.random_spawn") */
descriptionKey: string;
/** Parameters to pass to translateText for the badge key */
badgeParams?: Record<string, string | number>;
/** The raw value if applicable (e.g. startingGold amount) */
Expand All @@ -134,24 +136,28 @@ export function getActiveModifiers(
result.push({
labelKey: "host_modal.random_spawn",
badgeKey: "public_game_modifier.random_spawn",
descriptionKey: "modifier_description.random_spawn",
});
}
if (modifiers.isCompact) {
result.push({
labelKey: "host_modal.compact_map",
badgeKey: "public_game_modifier.compact_map",
descriptionKey: "modifier_description.compact_map",
});
}
if (modifiers.isCrowded) {
result.push({
labelKey: "host_modal.crowded",
badgeKey: "public_game_modifier.crowded",
descriptionKey: "modifier_description.crowded",
});
}
if (modifiers.isHardNations) {
result.push({
labelKey: "host_modal.hard_nations",
badgeKey: "public_game_modifier.hard_nations",
descriptionKey: "modifier_description.hard_nations",
});
}
if (modifiers.startingGold) {
Expand All @@ -161,6 +167,7 @@ export function getActiveModifiers(
result.push({
labelKey: "public_game_modifier.starting_gold_label",
badgeKey: "public_game_modifier.starting_gold",
descriptionKey: "modifier_description.starting_gold",
badgeParams: {
amount: millions,
},
Expand All @@ -172,6 +179,7 @@ export function getActiveModifiers(
result.push({
labelKey: "host_modal.gold_multiplier",
badgeKey: "public_game_modifier.gold_multiplier",
descriptionKey: "modifier_description.gold_multiplier",
badgeParams: {
amount: modifiers.goldMultiplier,
},
Expand All @@ -183,6 +191,7 @@ export function getActiveModifiers(
result.push({
labelKey: "public_game_modifier.disable_alliances_label",
badgeKey: "public_game_modifier.disable_alliances",
descriptionKey: "modifier_description.disable_alliances",
formattedValue: translateText("common.disabled"),
});
}
Expand Down
18 changes: 17 additions & 1 deletion src/client/components/GameConfigSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,20 @@ function stateTextClass(active: boolean): string {
return active ? "text-white" : "text-white/60";
}

/** Renders a styled toggle-style card button with an optional hover tooltip. */
function renderTextCardButton(
label: string,
active: boolean,
onClick: () => void,
cardExtraClass: string,
tooltip?: string,
): TemplateResult {
return html`
<button class="${cardClass(active, cardExtraClass)}" @click=${onClick}>
<button
class="${cardClass(active, cardExtraClass)}"
@click=${onClick}
data-tooltip=${tooltip ?? nothing}
>
<span class="${CARD_LABEL_CLASS} ${stateTextClass(active)}">
${label}
</span>
Expand Down Expand Up @@ -159,6 +165,8 @@ export interface ToggleOptionConfig {
labelKey: string;
checked: boolean;
hidden?: boolean;
/** Translation key for a tooltip description shown on hover */
descriptionKey?: string;
}

export interface GameConfigSettingsData {
Expand Down Expand Up @@ -265,11 +273,19 @@ export class GameConfigSettings extends LitElement {
private renderOptionToggle(toggle: ToggleOptionConfig): TemplateResult {
if (toggle.hidden) return html``;

const translatedTooltip = toggle.descriptionKey
? translateText(toggle.descriptionKey)
: undefined;
const tooltip =
translatedTooltip && translatedTooltip !== toggle.descriptionKey
? translatedTooltip
: undefined;
return renderTextCardButton(
translateText(toggle.labelKey),
toggle.checked,
() => this.handleOptionToggle(toggle),
"p-4 text-center",
tooltip,
);
}

Expand Down
5 changes: 4 additions & 1 deletion src/client/components/LobbyConfigItem.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { LitElement, TemplateResult, html } from "lit";
import { LitElement, TemplateResult, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";

@customElement("lobby-config-item")
export class LobbyConfigItem extends LitElement {
@property({ type: String }) label = "";
@property({ attribute: false }) value: string | TemplateResult = "";
/** Tooltip text shown on hover to describe the config item */
@property({ type: String }) tooltip?: string;

createRenderRoot() {
return this;
Expand All @@ -14,6 +16,7 @@ export class LobbyConfigItem extends LitElement {
return html`
<div
class="bg-white/5 border border-white/10 rounded-lg p-3 flex flex-col items-center justify-center gap-1 text-center min-w-[100px]"
data-tooltip=${this.tooltip ?? nothing}
>
<span
class="text-white/40 text-[10px] font-bold uppercase tracking-wider"
Expand Down
14 changes: 13 additions & 1 deletion src/client/components/ToggleInputCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ function cardClass(active: boolean, extra = ""): string {
@customElement("toggle-input-card")
export class ToggleInputCard extends LitElement {
@property({ attribute: false }) labelKey = "";
/** Translation key for a tooltip description shown on hover */
@property({ attribute: false }) descriptionKey?: string;
@property({ type: Boolean, attribute: false }) checked = false;
@property({ attribute: false }) inputId?: string;
@property({ attribute: false }) inputType = "number";
Expand Down Expand Up @@ -102,8 +104,18 @@ export class ToggleInputCard extends LitElement {
};

render() {
const translatedTooltip = this.descriptionKey
? translateText(this.descriptionKey)
: undefined;
const tooltip =
translatedTooltip && translatedTooltip !== this.descriptionKey
? translatedTooltip
: undefined;
return html`
<div class="${cardClass(this.checked, "relative overflow-hidden")}">
<div
class="${cardClass(this.checked, "relative overflow-hidden")}"
data-tooltip=${tooltip ?? nothing}
>
<button
type="button"
aria-pressed=${this.checked}
Expand Down
39 changes: 39 additions & 0 deletions src/client/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -635,3 +635,42 @@ news-button .active button::after {
.remove-player-btn:hover {
background: #ff6666;
}

/* Custom tooltip – replaces native title for instant display */
[data-tooltip] {
position: relative;
}

[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
left: 50%;
top: 100%;
transform: translateX(-50%);
margin-top: 6px;
padding: 6px 10px;
border-radius: 6px;
background: rgba(0, 0, 0, 0.9);
color: #fff;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
white-space: nowrap;
text-transform: none;
letter-spacing: normal;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s ease;
z-index: 50;
}

[data-tooltip]:hover::after {
opacity: 1;
}

/* Allow wrapping for longer tooltips */
[data-tooltip].tooltip-wrap::after {
white-space: normal;
max-width: 220px;
text-align: center;
}
Loading