mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 08:20:50 +00:00
@@ -10,10 +10,10 @@ import { SendSetTargetTroopRatioEvent } from '../../Transport';
|
||||
|
||||
@customElement('control-panel')
|
||||
export class ControlPanel extends LitElement implements Layer {
|
||||
private game: Game
|
||||
public clientID: ClientID
|
||||
public eventBus: EventBus
|
||||
public uiState: UIState
|
||||
private game: Game;
|
||||
public clientID: ClientID;
|
||||
public eventBus: EventBus;
|
||||
public uiState: UIState;
|
||||
|
||||
@state()
|
||||
private attackRatio: number = .2;
|
||||
@@ -21,6 +21,9 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
@state()
|
||||
private targetTroopRatio = 1;
|
||||
|
||||
@state()
|
||||
private currentTroopRatio = 1;
|
||||
|
||||
@state()
|
||||
private _population: number;
|
||||
|
||||
@@ -43,40 +46,42 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
private _manpower: number = 0;
|
||||
|
||||
@state()
|
||||
private _gold: number
|
||||
|
||||
private _gold: number;
|
||||
|
||||
@state()
|
||||
private _goldPerSecond: number
|
||||
private _goldPerSecond: number;
|
||||
|
||||
init(game: Game) {
|
||||
this.game = game;
|
||||
this.attackRatio = .20
|
||||
this.uiState.attackRatio = this.attackRatio
|
||||
this.attackRatio = .20;
|
||||
this.uiState.attackRatio = this.attackRatio;
|
||||
this.currentTroopRatio = this.targetTroopRatio;
|
||||
}
|
||||
|
||||
tick() {
|
||||
// Update game state based on numTroops value if needed
|
||||
if (!this._isVisible && !this.game.inSpawnPhase()) {
|
||||
this.toggleVisibility();
|
||||
this.setVisibile(true);
|
||||
}
|
||||
|
||||
const player = this.game.playerByClientID(this.clientID)
|
||||
if (player == null) {
|
||||
this._isVisible = false
|
||||
return
|
||||
const player = this.game.playerByClientID(this.clientID);
|
||||
if (player == null || !player.isAlive()) {
|
||||
this.setVisibile(false);
|
||||
return;
|
||||
}
|
||||
this._population = player.population()
|
||||
this._maxPopulation = this.game.config().maxPopulation(player)
|
||||
this._gold = player.gold()
|
||||
this._troops = player.troops()
|
||||
this._workers = player.workers()
|
||||
this.popRate = this.game.config().populationIncreaseRate(player) * 10
|
||||
this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10
|
||||
|
||||
this._population = player.population();
|
||||
this._maxPopulation = this.game.config().maxPopulation(player);
|
||||
this._gold = player.gold();
|
||||
this._troops = player.troops();
|
||||
this._workers = player.workers();
|
||||
this.popRate = this.game.config().populationIncreaseRate(player) * 10;
|
||||
this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10;
|
||||
|
||||
this.currentTroopRatio = player.troops() / player.population();
|
||||
}
|
||||
|
||||
onAttackRatioChange(newRatio: number) {
|
||||
this.uiState.attackRatio = newRatio
|
||||
this.uiState.attackRatio = newRatio;
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
@@ -87,32 +92,29 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
return false;
|
||||
}
|
||||
|
||||
toggleVisibility() {
|
||||
this._isVisible = !this._isVisible;
|
||||
setVisibile(visible: boolean) {
|
||||
this._isVisible = visible;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
targetTroops(): number {
|
||||
return this._manpower * this.targetTroopRatio
|
||||
return this._manpower * this.targetTroopRatio;
|
||||
}
|
||||
|
||||
onTroopChange(newRatio: number) {
|
||||
this.eventBus.emit(new SendSetTargetTroopRatioEvent(newRatio))
|
||||
this.eventBus.emit(new SendSetTargetTroopRatioEvent(newRatio));
|
||||
}
|
||||
|
||||
delta(): number {
|
||||
const d = this._population - this.targetTroops()
|
||||
// if (Math.abs(d) < this._manpower / 200) {
|
||||
// return 0
|
||||
// }
|
||||
return d
|
||||
const d = this._population - this.targetTroops();
|
||||
return d;
|
||||
}
|
||||
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
@@ -126,13 +128,62 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
backdrop-filter: blur(5px);
|
||||
transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.slider-track {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.slider-fill {
|
||||
position: absolute;
|
||||
height: 8px;
|
||||
background: rgba(0, 150, 255, 0.6);
|
||||
border-radius: 4px;
|
||||
top: 20px;
|
||||
transition: width 0.3s ease-out;
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: white;
|
||||
border: 2px solid rgb(0, 150, 255);
|
||||
border-radius: 50%;
|
||||
top: 16px;
|
||||
transform: translateX(-50%);
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.slider-thumb:hover {
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 12px;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.control-panel-info {
|
||||
color: white;
|
||||
margin-bottom: 15px;
|
||||
@@ -140,26 +191,77 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
color: white;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
input[type="range"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
color: white;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.attack-slider {
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.attack-slider .slider-track {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.attack-slider .slider-fill {
|
||||
position: absolute;
|
||||
height: 8px;
|
||||
background: rgba(255, 0, 0, 0.6);
|
||||
border-radius: 4px;
|
||||
top: 20px;
|
||||
transition: width 0.3s ease-out;
|
||||
}
|
||||
|
||||
.attack-slider .slider-thumb {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: white;
|
||||
border: 2px solid rgb(255, 0, 0);
|
||||
border-radius: 50%;
|
||||
top: 16px;
|
||||
transform: translateX(-50%);
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.attack-slider .slider-thumb:hover {
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
.attack-slider input[type="range"] {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 12px;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
@@ -183,21 +285,39 @@ export class ControlPanel extends LitElement implements Layer {
|
||||
<span>${renderNumber(this._goldPerSecond)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="slider-container">
|
||||
<label for="numTroops">Troops: ${renderTroops(this._troops)} | Workers: ${renderTroops(this._workers)}</label>
|
||||
<input type="range" id="numTroops" min="1" max="10" .value=${this.targetTroopRatio * 10}
|
||||
<label>Troops: ${renderTroops(this._troops)} | Workers: ${renderTroops(this._workers)}</label>
|
||||
<div class="slider-track"></div>
|
||||
<div class="slider-fill" style="width: ${this.currentTroopRatio * 100}%"></div>
|
||||
<div class="slider-thumb" style="left: ${this.targetTroopRatio * 100}%"></div>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${this.targetTroopRatio * 100}
|
||||
@input=${(e: Event) => {
|
||||
this.targetTroopRatio = parseInt((e.target as HTMLInputElement).value) / 10;
|
||||
this.targetTroopRatio = parseInt((e.target as HTMLInputElement).value) / 100;
|
||||
this.onTroopChange(this.targetTroopRatio);
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<label for="numTroops">Attack Ratio: ${this.attackRatio * 100}%</label>
|
||||
<input type="range" id="numTroops" min="1" max="10" value=${this.attackRatio * 10}
|
||||
|
||||
<div class="attack-slider">
|
||||
<label>Attack Ratio: ${(this.attackRatio * 100).toFixed(0)}%</label>
|
||||
<div class="slider-track"></div>
|
||||
<div class="slider-fill" style="width: ${this.attackRatio * 100}%"></div>
|
||||
<div class="slider-thumb" style="left: ${this.attackRatio * 100}%"></div>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="100"
|
||||
.value=${this.attackRatio * 100}
|
||||
@input=${(e: Event) => {
|
||||
this.attackRatio = parseInt((e.target as HTMLInputElement).value) / 10;
|
||||
this.attackRatio = parseInt((e.target as HTMLInputElement).value) / 100;
|
||||
this.onAttackRatioChange(this.attackRatio);
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { EventBus } from '../../../../core/EventBus';
|
||||
import { Cell, Game, Player } from '../../../../core/game/Game';
|
||||
import { Cell, Game, Item, Items, Player } from '../../../../core/game/Game';
|
||||
import { SendNukeIntentEvent } from '../../../Transport';
|
||||
import nukeIcon from '../../../../../resources/images/NukeIconWhite.svg';
|
||||
import goldCoinIcon from '../../../../../resources/images/GoldCoinIcon.svg';
|
||||
import { renderNumber } from '../../Utils';
|
||||
|
||||
interface BuildItem {
|
||||
id: string;
|
||||
name: string;
|
||||
item: Item
|
||||
icon: string;
|
||||
cost: number;
|
||||
}
|
||||
|
||||
const buildTable: BuildItem[][] = [
|
||||
[
|
||||
{ id: 'nuke', name: 'Nuke', icon: nukeIcon, cost: 1_000_000 },
|
||||
{ item: Items.Nuke, icon: nukeIcon },
|
||||
// { id: 'battleship', name: 'Battleship', icon: '🚢', cost: 500, buildTime: 20 }
|
||||
]
|
||||
];
|
||||
@@ -24,11 +22,10 @@ const buildTable: BuildItem[][] = [
|
||||
@customElement('build-menu')
|
||||
export class BuildMenu extends LitElement {
|
||||
public game: Game;
|
||||
public eventBus: EventBus
|
||||
public eventBus: EventBus;
|
||||
|
||||
|
||||
private myPlayer: Player
|
||||
private clickedCell: Cell
|
||||
private myPlayer: Player;
|
||||
private clickedCell: Cell;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
@@ -72,15 +69,27 @@ export class BuildMenu extends LitElement {
|
||||
margin: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
.build-button:hover {
|
||||
.build-button:not(:disabled):hover {
|
||||
background-color: #3A3A3A;
|
||||
transform: scale(1.05);
|
||||
border-color: #666;
|
||||
}
|
||||
.build-button:active {
|
||||
.build-button:not(:disabled):active {
|
||||
background-color: #4A4A4A;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.build-button:disabled {
|
||||
background-color: #1A1A1A;
|
||||
border-color: #333;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.build-button:disabled img {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.build-button:disabled .build-cost {
|
||||
color: #FF4444;
|
||||
}
|
||||
.build-icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 5px;
|
||||
@@ -128,6 +137,10 @@ export class BuildMenu extends LitElement {
|
||||
@state()
|
||||
private _hidden = true;
|
||||
|
||||
private canAfford(item: BuildItem): boolean {
|
||||
return this.myPlayer && this.myPlayer.gold() >= item.item.cost;
|
||||
}
|
||||
|
||||
public onBuildSelected: (item: BuildItem) => void = () => {
|
||||
this.eventBus.emit(new SendNukeIntentEvent(this.myPlayer, this.clickedCell, null))
|
||||
this.hideMenu()
|
||||
@@ -139,11 +152,16 @@ export class BuildMenu extends LitElement {
|
||||
${buildTable.map(row => html`
|
||||
<div class="build-row">
|
||||
${row.map(item => html`
|
||||
<button class="build-button" @click=${() => this.onBuildSelected(item)}>
|
||||
<img src=${item.icon} alt="${item.name}" width="40" height="40">
|
||||
<span class="build-name">${item.name}</span>
|
||||
<button
|
||||
class="build-button"
|
||||
@click=${() => this.onBuildSelected(item)}
|
||||
?disabled=${!this.canAfford(item)}
|
||||
title=${!this.canAfford(item) ? 'Not enough money' : ''}
|
||||
>
|
||||
<img src=${item.icon} alt="${item.item.name}" width="40" height="40">
|
||||
<span class="build-name">${item.item.name}</span>
|
||||
<span class="build-cost">
|
||||
${renderNumber(item.cost)}
|
||||
${renderNumber(item.item.cost)}
|
||||
<img src=${goldCoinIcon} alt="gold" width="12" height="12" style="vertical-align: middle;">
|
||||
</span>
|
||||
</button>
|
||||
@@ -160,8 +178,8 @@ export class BuildMenu extends LitElement {
|
||||
}
|
||||
|
||||
showMenu(player: Player, clickedCell: Cell) {
|
||||
this.myPlayer = player
|
||||
this.clickedCell = clickedCell
|
||||
this.myPlayer = player;
|
||||
this.clickedCell = clickedCell;
|
||||
this._hidden = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
troopAdjustmentRate(player: Player): number {
|
||||
const maxDiff = this.maxPopulation(player) / 300
|
||||
const maxDiff = this.maxPopulation(player) / 1000
|
||||
const target = player.population() * player.targetTroopRatio()
|
||||
const diff = target - player.troops()
|
||||
if (Math.abs(diff) < maxDiff) {
|
||||
|
||||
@@ -6,7 +6,7 @@ export const devConfig = new class extends DefaultConfig {
|
||||
return 95
|
||||
}
|
||||
numSpawnPhaseTurns(): number {
|
||||
return 40
|
||||
return 20
|
||||
}
|
||||
gameCreationRate(): number {
|
||||
return 20 * 1000
|
||||
|
||||
@@ -2,7 +2,6 @@ import { PriorityQueue } from "@datastructures-js/priority-queue";
|
||||
import { Cell, Execution, MutableGame, MutablePlayer, Player, PlayerID, TerrainType, TerraNullius, Tile } from "../game/Game";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { manhattanDist } from "../Util";
|
||||
import { Terrain } from "../game/TerrainMapLoader";
|
||||
|
||||
export class AttackExecution implements Execution {
|
||||
private breakAlliance = false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Cell, Execution, MutableGame, MutablePlayer, PlayerID, Tile } from "../game/Game";
|
||||
import { Cell, Execution, Items, MutableGame, MutablePlayer, PlayerID, Tile } from "../game/Game";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { bfs, dist, euclideanDist, manhattanDist } from "../Util";
|
||||
|
||||
@@ -26,6 +26,13 @@ export class NukeExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.sender.gold() < Items.Nuke.cost) {
|
||||
console.warn(`player ${this.sender} insufficient gold for nuke`)
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
this.sender.removeGold(Items.Nuke.cost)
|
||||
|
||||
const rand = new PseudoRandom(this.mg.ticks())
|
||||
const tile = this.mg.tile(this.cell)
|
||||
const toDestroy = bfs(tile, (n: Tile) => {
|
||||
|
||||
@@ -25,6 +25,14 @@ export enum GameMap {
|
||||
Mena
|
||||
}
|
||||
|
||||
export class Item {
|
||||
constructor(public readonly name: string, public readonly cost: Gold) { }
|
||||
}
|
||||
|
||||
export const Items = {
|
||||
Nuke: new Item("Nuke", 1_000_000),
|
||||
} as const;
|
||||
|
||||
export class Nation {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
|
||||
Reference in New Issue
Block a user