Restore troop/gold ratio slider and worker allocation

This commit is contained in:
scamiv
2026-03-01 22:06:03 +01:00
parent c911bfb2d8
commit d9ec9b0e40
14 changed files with 232 additions and 11 deletions
+2
View File
@@ -510,6 +510,8 @@
"left_click_menu": "Left Click Menu",
"attack_ratio_label": "⚔️ Attack Ratio",
"attack_ratio_desc": "What percentage of your troops to send in an attack (1100%)",
"troop_ratio_label": "Troop/Gold Ratio",
"troop_ratio_desc": "Adjust the balance between troops (combat) and workers (gold production) (1100%)",
"territory_patterns_label": "🏳️ Territory Skins",
"territory_patterns_desc": "Choose whether to display territory skin designs in game",
"performance_overlay_label": "Performance Overlay",
+14
View File
@@ -145,6 +145,10 @@ export class CancelBoatIntentEvent implements GameEvent {
constructor(public readonly unitID: number) {}
}
export class SendSetTargetTroopRatioEvent implements GameEvent {
constructor(public readonly ratio: number) {}
}
export class SendWinnerEvent implements GameEvent {
constructor(
public readonly winner: Winner,
@@ -235,6 +239,9 @@ export class Transport {
this.eventBus.on(SendEmbargoAllIntentEvent, (e) =>
this.onSendEmbargoAllIntent(e),
);
this.eventBus.on(SendSetTargetTroopRatioEvent, (e) =>
this.onSendSetTargetTroopRatioEvent(e),
);
this.eventBus.on(BuildUnitIntentEvent, (e) => this.onBuildUnitIntent(e));
this.eventBus.on(PauseGameIntentEvent, (e) => this.onPauseGameIntent(e));
@@ -552,6 +559,13 @@ export class Transport {
});
}
private onSendSetTargetTroopRatioEvent(event: SendSetTargetTroopRatioEvent) {
this.sendIntent({
type: "troop_ratio",
ratio: event.ratio,
});
}
private onBuildUnitIntent(event: BuildUnitIntentEvent) {
this.sendIntent({
type: "build_unit",
+20
View File
@@ -362,6 +362,16 @@ export class UserSettingModal extends BaseModal {
}
}
private sliderTroopRatio(e: CustomEvent<{ value: number }>) {
const value = e.detail?.value;
if (typeof value === "number") {
const ratio = value / 100;
localStorage.setItem("settings.troopRatio", ratio.toString());
} else {
console.warn("Slider event missing detail.value", e);
}
}
private changeAttackRatioIncrement(
e: CustomEvent<{ value: number | string }>,
) {
@@ -915,6 +925,16 @@ export class UserSettingModal extends BaseModal {
@change=${this.sliderAttackRatio}
></setting-slider>
<setting-slider
label="${translateText("user_setting.troop_ratio_label")}"
description="${translateText("user_setting.troop_ratio_desc")}"
min="1"
max="100"
.value=${Number(localStorage.getItem("settings.troopRatio") ?? "0.95") *
100}
@change=${this.sliderTroopRatio}
></setting-slider>
<!-- ⚔️ Attack Ratio Increment -->
<setting-select
label=${translateText("user_setting.attack_ratio_increment_label")}
@@ -5,6 +5,7 @@ import { Gold } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { ClientID } from "../../../core/Schemas";
import { AttackRatioEvent } from "../../InputHandler";
import { SendSetTargetTroopRatioEvent } from "../../Transport";
import { renderNumber, renderTroops } from "../../Utils";
import { UIState } from "../UIState";
import { Layer } from "./Layer";
@@ -28,9 +29,21 @@ export class ControlPanel extends LitElement implements Layer {
@state()
private troopRate: number;
@state()
private targetTroopRatio = 0.95;
@state()
private currentTroopRatio = 0.95;
@state()
private _population: number;
@state()
private _troops: number;
@state()
private _workers: number;
@state()
private _isVisible = false;
@@ -46,6 +59,7 @@ export class ControlPanel extends LitElement implements Layer {
private _troopRateIsIncreasing: boolean = true;
private _lastTroopIncreaseRate: number;
private initTroopRatio = false;
getTickIntervalMs() {
return 100;
@@ -55,6 +69,11 @@ export class ControlPanel extends LitElement implements Layer {
this.attackRatio = Number(
localStorage.getItem("settings.attackRatio") ?? "0.2",
);
this.targetTroopRatio = Number(
localStorage.getItem("settings.troopRatio") ?? "0.95",
);
this.currentTroopRatio = this.targetTroopRatio;
this.initTroopRatio = true;
this.uiState.attackRatio = this.attackRatio;
this.eventBus.on(AttackRatioEvent, (event) => {
let newAttackRatio = this.attackRatio + event.attackRatio / 100;
@@ -78,6 +97,13 @@ export class ControlPanel extends LitElement implements Layer {
}
tick() {
if (this.initTroopRatio) {
this.eventBus.emit(
new SendSetTargetTroopRatioEvent(this.targetTroopRatio),
);
this.initTroopRatio = false;
}
if (!this._isVisible && !this.game.inSpawnPhase()) {
this.setVisibile(true);
}
@@ -92,11 +118,14 @@ export class ControlPanel extends LitElement implements Layer {
this._maxTroops = this.game.config().maxTroops(player);
this._gold = player.gold();
this._population = player.population();
this._troops = player.troops();
this._workers = player.workers();
this._attackingTroops = player
.outgoingAttacks()
.map((a) => a.troops)
.reduce((a, b) => a + b, 0);
this.currentTroopRatio = this._troops / Math.max(this._population, 1);
this.troopRate = this.game.config().troopIncreaseRate(player) * 10;
this.requestUpdate();
}
@@ -193,6 +222,13 @@ export class ControlPanel extends LitElement implements Layer {
this.onAttackRatioChange(this.attackRatio);
}
private handleTroopRatioSliderInput(e: Event) {
const value = Number((e.target as HTMLInputElement).value);
this.targetTroopRatio = value / 100;
localStorage.setItem("settings.troopRatio", this.targetTroopRatio.toString());
this.eventBus.emit(new SendSetTargetTroopRatioEvent(this.targetTroopRatio));
}
private renderTroopBar() {
const base = Math.max(this._maxTroops, 1);
const greenPercentRaw = (this._troops / base) * 100;
@@ -342,6 +378,16 @@ export class ControlPanel extends LitElement implements Layer {
</div>
`
: ""}
<div class="mt-2 flex items-center justify-between text-[11px] text-white/80" translate="no">
<span>
${renderTroops(this._troops)} troops | ${renderTroops(this._workers)}
workers
</span>
<span>
${(this.currentTroopRatio * 100).toFixed(0)}% live /
${(this.targetTroopRatio * 100).toFixed(0)}% target
</span>
</div>
<!-- Attack ratio bar (desktop, always visible) -->
<div class="hidden lg:block mt-2">
<div
@@ -374,6 +420,26 @@ export class ControlPanel extends LitElement implements Layer {
class="w-full h-2 accent-red-500 cursor-pointer"
/>
</div>
<div class="mt-2">
<div
class="flex items-center justify-between text-xs lg:text-sm font-bold mb-1 text-white"
translate="no"
>
<span>Troop/Gold Ratio</span>
<span>
${(this.targetTroopRatio * 100).toFixed(0)}%
(${renderTroops(this._population * this.targetTroopRatio)} troops)
</span>
</div>
<input
type="range"
min="1"
max="100"
.value=${String(Math.round(this.targetTroopRatio * 100))}
@input=${(e: Event) => this.handleTroopRatioSliderInput(e)}
class="w-full h-2 accent-blue-500 cursor-pointer"
/>
</div>
</div>
`;
}
+10
View File
@@ -41,6 +41,7 @@ export type Intent =
| EmojiIntent
| DonateGoldIntent
| DonateTroopsIntent
| TargetTroopRatioIntent
| BuildUnitIntent
| EmbargoIntent
| QuickChatIntent
@@ -66,6 +67,9 @@ export type TargetPlayerIntent = z.infer<typeof TargetPlayerIntentSchema>;
export type EmojiIntent = z.infer<typeof EmojiIntentSchema>;
export type DonateGoldIntent = z.infer<typeof DonateGoldIntentSchema>;
export type DonateTroopsIntent = z.infer<typeof DonateTroopIntentSchema>;
export type TargetTroopRatioIntent = z.infer<
typeof TargetTroopRatioIntentSchema
>;
export type EmbargoIntent = z.infer<typeof EmbargoIntentSchema>;
export type BuildUnitIntent = z.infer<typeof BuildUnitIntentSchema>;
export type UpgradeStructureIntent = z.infer<
@@ -358,6 +362,11 @@ export const DonateTroopIntentSchema = z.object({
troops: z.number().nonnegative().nullable(),
});
export const TargetTroopRatioIntentSchema = z.object({
type: z.literal("troop_ratio"),
ratio: z.number().min(0).max(1),
});
export const BuildUnitIntentSchema = z.object({
type: z.literal("build_unit"),
unit: z.enum(UnitType),
@@ -434,6 +443,7 @@ const IntentSchema = z.discriminatedUnion("type", [
EmojiIntentSchema,
DonateGoldIntentSchema,
DonateTroopIntentSchema,
TargetTroopRatioIntentSchema,
BuildUnitIntentSchema,
UpgradeStructureIntentSchema,
EmbargoIntentSchema,
+1
View File
@@ -84,6 +84,7 @@ export interface Config {
startManpower(playerInfo: PlayerInfo): number;
troopIncreaseRate(player: Player | PlayerView): number;
goldAdditionRate(player: Player | PlayerView): Gold;
troopAdjustmentRate(player: Player): number;
attackTilesPerTick(
attckTroops: number,
attacker: Player,
+21 -8
View File
@@ -777,9 +777,9 @@ export class DefaultConfig implements Config {
troopIncreaseRate(player: Player): number {
const max = this.maxTroops(player);
let toAdd = 10 + Math.pow(player.troops(), 0.73) / 4;
let toAdd = 10 + Math.pow(player.population(), 0.73) / 4;
const ratio = 1 - player.troops() / max;
const ratio = 1 - player.population() / max;
toAdd *= ratio;
if (player.type() === PlayerType.Bot) {
@@ -805,18 +805,31 @@ export class DefaultConfig implements Config {
}
}
return Math.min(player.troops() + toAdd, max) - player.troops();
return Math.min(player.population() + toAdd, max) - player.population();
}
goldAdditionRate(player: Player): Gold {
const multiplier = this.goldMultiplier();
let baseRate: bigint;
let baseRate = 0.045 * player.workers() ** 0.7;
if (player.type() === PlayerType.Bot) {
baseRate = 50n;
} else {
baseRate = 100n;
baseRate *= 0.6;
}
return BigInt(Math.floor(Number(baseRate) * multiplier));
return BigInt(Math.floor(baseRate * multiplier));
}
troopAdjustmentRate(player: Player): number {
const maxDiff = this.maxTroops(player) / 1000;
const target = player.population() * player.targetTroopRatio();
const diff = target - player.troops();
if (Math.abs(diff) < maxDiff) {
return diff;
}
const adjustment = maxDiff * Math.sign(diff);
// Can ramp down troops much faster.
if (adjustment < 0) {
return adjustment * 5;
}
return adjustment;
}
nukeMagnitudes(unitType: UnitType): NukeMagnitude {
+3
View File
@@ -23,6 +23,7 @@ import { NoOpExecution } from "./NoOpExecution";
import { PauseExecution } from "./PauseExecution";
import { QuickChatExecution } from "./QuickChatExecution";
import { RetreatExecution } from "./RetreatExecution";
import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution";
import { SpawnExecution } from "./SpawnExecution";
import { TargetPlayerExecution } from "./TargetPlayerExecution";
import { TransportShipExecution } from "./TransportShipExecution";
@@ -91,6 +92,8 @@ export class Executor {
);
case "donate_gold":
return new DonateGoldExecution(player, intent.recipient, intent.gold);
case "troop_ratio":
return new SetTargetTroopRatioExecution(player, intent.ratio);
case "embargo":
return new EmbargoExecution(player, intent.targetID, intent.action);
case "embargo_all":
+7 -2
View File
@@ -73,14 +73,19 @@ export class PlayerExecution implements Execution {
return;
}
const troopInc = this.config.troopIncreaseRate(this.player);
this.player.addTroops(troopInc);
const popInc = this.config.troopIncreaseRate(this.player);
this.player.addWorkers(popInc * (1 - this.player.targetTroopRatio()));
this.player.addTroops(popInc * this.player.targetTroopRatio());
const goldFromWorkers = this.config.goldAdditionRate(this.player);
this.player.addGold(goldFromWorkers);
// Record stats
this.mg.stats().goldWork(this.player, goldFromWorkers);
const adjustRate = this.config.troopAdjustmentRate(this.player);
this.player.addTroops(adjustRate);
this.player.removeWorkers(adjustRate);
for (const alliance of this.player.alliances()) {
if (alliance.expiresAt() <= this.mg.ticks()) {
alliance.expire();
@@ -0,0 +1,31 @@
import { Execution, Game, Player } from "../game/Game";
export class SetTargetTroopRatioExecution implements Execution {
private active = true;
constructor(
private player: Player,
private targetTroopsRatio: number,
) {}
init(mg: Game, ticks: number): void {}
tick(ticks: number): void {
if (this.targetTroopsRatio < 0 || this.targetTroopsRatio > 1) {
console.warn(
`target troop ratio of ${this.targetTroopsRatio} for player ${this.player} invalid`,
);
} else {
this.player.setTargetTroopRatio(this.targetTroopsRatio);
}
this.active = false;
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+6
View File
@@ -627,7 +627,13 @@ export interface Player {
gold(): Gold;
addGold(toAdd: Gold, tile?: TileRef): void;
removeGold(toRemove: Gold): Gold;
population(): number;
workers(): number;
troops(): number;
targetTroopRatio(): number;
addWorkers(toAdd: number): void;
removeWorkers(toRemove: number): void;
setTargetTroopRatio(target: number): void;
setTroops(troops: number): void;
addTroops(troops: number): void;
removeTroops(troops: number): number;
+3
View File
@@ -173,7 +173,10 @@ export interface PlayerUpdate {
isDisconnected: boolean;
tilesOwned: number;
gold: Gold;
population: number;
workers: number;
troops: number;
targetTroopRatio: number;
allies: number[];
embargoes: Set<PlayerID>;
isTraitor: boolean;
+9
View File
@@ -502,6 +502,15 @@ export class PlayerView {
gold(): Gold {
return this.data.gold;
}
population(): number {
return this.data.population;
}
workers(): number {
return this.data.workers;
}
targetTroopRatio(): number {
return this.data.targetTroopRatio;
}
troops(): number {
return this.data.troops;
+39 -1
View File
@@ -4,6 +4,7 @@ import { ClientID } from "../Schemas";
import {
assertNever,
distSortUnit,
maxInt,
minInt,
simpleHash,
toInt,
@@ -72,6 +73,9 @@ export class PlayerImpl implements Player {
private _gold: bigint;
private _troops: bigint;
private _workers: bigint;
// Stored as percentage, 0 to 100.
private _targetTroopRatio: bigint;
markedTraitorTick = -1;
private _betrayalCount: number = 0;
@@ -115,7 +119,9 @@ export class PlayerImpl implements Player {
private readonly _team: Team | null,
) {
this._name = playerInfo.name;
this._targetTroopRatio = 95n;
this._troops = toInt(startTroops);
this._workers = 0n;
this._gold = mg.config().startingGold(playerInfo);
this._displayName = this._name;
this._pseudo_random = new PseudoRandom(simpleHash(this.playerInfo.id));
@@ -141,7 +147,10 @@ export class PlayerImpl implements Player {
isDisconnected: this.isDisconnected(),
tilesOwned: this.numTilesOwned(),
gold: this._gold,
population: this.population(),
workers: this.workers(),
troops: this.troops(),
targetTroopRatio: this.targetTroopRatio(),
allies: this.alliances().map((a) => a.other(this).smallID()),
embargoes: new Set([...this.embargoes.keys()].map((p) => p.toString())),
isTraitor: this.isTraitor(),
@@ -930,6 +939,35 @@ export class PlayerImpl implements Player {
return actualRemoved;
}
population(): number {
return Number(this._troops + this._workers);
}
workers(): number {
return Math.max(1, Number(this._workers));
}
addWorkers(toAdd: number): void {
this._workers += toInt(toAdd);
}
removeWorkers(toRemove: number): void {
this._workers = maxInt(1n, this._workers - toInt(toRemove));
}
targetTroopRatio(): number {
return Number(this._targetTroopRatio) / 100;
}
setTargetTroopRatio(target: number): void {
if (target < 0 || target > 1) {
throw new Error(
`invalid targetTroopRatio ${target} set on player ${PlayerImpl}`,
);
}
this._targetTroopRatio = toInt(target * 100);
}
troops(): number {
return Number(this._troops);
}
@@ -1276,7 +1314,7 @@ export class PlayerImpl implements Player {
hash(): number {
return (
simpleHash(this.id()) * (this.troops() + this.numTilesOwned()) +
simpleHash(this.id()) * (this.population() + this.numTilesOwned()) +
this._units.reduce((acc, unit) => acc + unit.hash(), 0)
);
}