mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:20:47 +00:00
Restore troop/gold ratio slider and worker allocation
This commit is contained in:
@@ -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 (1–100%)",
|
||||
"troop_ratio_label": "Troop/Gold Ratio",
|
||||
"troop_ratio_desc": "Adjust the balance between troops (combat) and workers (gold production) (1–100%)",
|
||||
"territory_patterns_label": "🏳️ Territory Skins",
|
||||
"territory_patterns_desc": "Choose whether to display territory skin designs in game",
|
||||
"performance_overlay_label": "Performance Overlay",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user