Merge pull request #100 from d3n0x8/scrollAttackRatio

add shift + scroll command in HelpModal hotkeys table
This commit is contained in:
evanpelle
2025-03-02 07:46:00 -08:00
committed by ilan schemoul
15 changed files with 232 additions and 26 deletions
+6 -2
View File
@@ -1,5 +1,5 @@
import { LitElement, html, css } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import "./components/Difficulties";
import "./components/Maps";
@@ -236,6 +236,10 @@ export class HelpModal extends LitElement {
<td>1 / 2</td>
<td>Decrease/Increase attack ratio</td>
</tr>
<tr>
<td>Shift + scroll down / scroll up</td>
<td>Decrease/Increase attack ratio</td>
</tr>
<tr>
<td>ALT + R</td>
<td>Reset graphics</td>
+21
View File
@@ -100,6 +100,14 @@ export class SendDonateIntentEvent implements GameEvent {
) {}
}
export class SendEmbargoIntentEvent implements GameEvent {
constructor(
public readonly sender: PlayerView,
public readonly target: PlayerView,
public readonly action: "start" | "stop",
) {}
}
export class SendSetTargetTroopRatioEvent implements GameEvent {
constructor(public readonly ratio: number) {}
}
@@ -151,6 +159,9 @@ export class Transport {
);
this.eventBus.on(SendEmojiIntentEvent, (e) => this.onSendEmojiIntent(e));
this.eventBus.on(SendDonateIntentEvent, (e) => this.onSendDonateIntent(e));
this.eventBus.on(SendEmbargoIntentEvent, (e) =>
this.onSendEmbargoIntent(e),
);
this.eventBus.on(SendSetTargetTroopRatioEvent, (e) =>
this.onSendSetTargetTroopRatioEvent(e),
);
@@ -397,6 +408,16 @@ export class Transport {
});
}
private onSendEmbargoIntent(event: SendEmbargoIntentEvent) {
this.sendIntent({
type: "embargo",
clientID: this.lobbyConfig.clientID,
playerID: this.lobbyConfig.playerID,
targetID: event.target.id(),
action: event.action,
});
}
private onSendSetTargetTroopRatioEvent(event: SendSetTargetTroopRatioEvent) {
this.sendIntent({
type: "troop_ratio",
+52
View File
@@ -18,6 +18,7 @@ import {
SendDonateIntentEvent,
SendEmojiIntentEvent,
SendTargetPlayerIntentEvent,
SendEmbargoIntentEvent,
} from "../../Transport";
import { EmojiTable } from "./EmojiTable";
@@ -76,6 +77,26 @@ export class PlayerPanel extends LitElement implements Layer {
this.hide();
}
private handleEmbargoClick(
e: Event,
myPlayer: PlayerView,
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendEmbargoIntentEvent(myPlayer, other, "start"));
this.hide();
}
private handleStopEmbargoClick(
e: Event,
myPlayer: PlayerView,
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendEmbargoIntentEvent(myPlayer, other, "stop"));
this.hide();
}
private handleEmojiClick(e: Event, myPlayer: PlayerView, other: PlayerView) {
e.stopPropagation();
this.emojiTable.showTable((emoji: string) => {
@@ -131,6 +152,7 @@ export class PlayerPanel extends LitElement implements Layer {
: this.actions.interaction?.canSendEmoji;
const canBreakAlliance = this.actions.interaction?.canBreakAlliance;
const canTarget = this.actions.interaction?.canTarget;
const canEmbargo = this.actions.interaction?.canEmbargo;
return html`
<div
@@ -190,6 +212,15 @@ export class PlayerPanel extends LitElement implements Layer {
</div>
</div>
<div class="flex flex-col gap-1">
<div class="text-white text-opacity-80 text-sm px-2">
Embargo against you
</div>
<div class="bg-opacity-50 bg-gray-700 rounded p-2 text-white">
${other.hasEmbargoAgainst(myPlayer) ? "Yes" : "No"}
</div>
</div>
<!-- Action buttons -->
<div class="flex justify-center gap-2">
${canTarget
@@ -249,6 +280,27 @@ export class PlayerPanel extends LitElement implements Layer {
</button>`
: ""}
</div>
${canEmbargo && other != myPlayer
? html`<button
@click=${(e) => this.handleEmbargoClick(e, myPlayer, other)}
class="w-100 h-10 flex items-center justify-center
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
text-white rounded-lg transition-colors"
>
Start embargo
</button>`
: ""}
${!canEmbargo && other != myPlayer
? html`<button
@click=${(e) =>
this.handleStopEmbargoClick(e, myPlayer, other)}
class="w-100 h-10 flex items-center justify-center
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
text-white rounded-lg transition-colors"
>
Stop embargo
</button>`
: ""}
</div>
</div>
</div>
+1
View File
@@ -167,6 +167,7 @@ export class GameRunner {
canSendAllianceRequest: player.canSendAllianceRequest(other),
canBreakAlliance: player.isAlliedWith(other),
canDonate: player.canDonate(other),
canEmbargo: !player.hasEmbargoAgainst(other),
};
}
+12 -1
View File
@@ -22,7 +22,8 @@ export type Intent =
| EmojiIntent
| DonateIntent
| TargetTroopRatioIntent
| BuildUnitIntent;
| BuildUnitIntent
| EmbargoIntent;
export type AttackIntent = z.infer<typeof AttackIntentSchema>;
export type SpawnIntent = z.infer<typeof SpawnIntentSchema>;
@@ -35,6 +36,7 @@ export type BreakAllianceIntent = z.infer<typeof BreakAllianceIntentSchema>;
export type TargetPlayerIntent = z.infer<typeof TargetPlayerIntentSchema>;
export type EmojiIntent = z.infer<typeof EmojiIntentSchema>;
export type DonateIntent = z.infer<typeof DonateIntentSchema>;
export type EmbargoIntent = z.infer<typeof EmbargoIntentSchema>;
export type TargetTroopRatioIntent = z.infer<
typeof TargetTroopRatioIntentSchema
>;
@@ -133,6 +135,7 @@ const BaseIntentSchema = z.object({
"emoji",
"troop_ratio",
"build_unit",
"embargo",
]),
clientID: ID,
playerID: ID,
@@ -196,6 +199,13 @@ export const EmojiIntentSchema = BaseIntentSchema.extend({
emoji: EmojiSchema,
});
export const EmbargoIntentSchema = BaseIntentSchema.extend({
type: z.literal("embargo"),
playerID: ID,
targetID: ID,
action: z.union([z.literal("start"), z.literal("stop")]),
});
export const DonateIntentSchema = BaseIntentSchema.extend({
type: z.literal("donate"),
playerID: ID,
@@ -229,6 +239,7 @@ const IntentSchema = z.union([
DonateIntentSchema,
TargetTroopRatioIntentSchema,
BuildUnitIntentSchema,
EmbargoIntentSchema,
]);
export const TurnSchema = z.object({
+44
View File
@@ -0,0 +1,44 @@
import { consolex } from "../Consolex";
import { Execution, Game, Player, PlayerID } from "../game/Game";
export class EmbargoExecution implements Execution {
private active = true;
constructor(
private player: Player,
private targetID: PlayerID,
private readonly action: "start" | "stop",
) {}
init(mg: Game, _: number): void {
if (!mg.hasPlayer(this.player.id())) {
console.warn(`EmbargoExecution: sender ${this.player.id()} not found`);
this.active = false;
return;
}
if (!mg.hasPlayer(this.targetID)) {
console.warn(`EmbargoExecution recipient ${this.targetID} not found`);
this.active = false;
return;
}
}
tick(_: number): void {
if (this.action == "start") this.player.addEmbargo(this.targetID);
else this.player.stopEmbargo(this.targetID);
this.active = false;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+6 -1
View File
@@ -34,6 +34,7 @@ import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution";
import { ConstructionExecution } from "./ConstructionExecution";
import { fixProfaneUsername, isProfaneUsername } from "../validations/username";
import { NoOpExecution } from "./NoOpExecution";
import { EmbargoExecution } from "./EmbargoExecution";
export class Executor {
// private random = new PseudoRandom(999)
@@ -53,6 +54,7 @@ export class Executor {
}
createExec(intent: Intent): Execution {
let player: Player;
if (intent.type != "spawn") {
if (!this.mg.hasPlayer(intent.playerID)) {
console.warn(
@@ -60,7 +62,7 @@ export class Executor {
);
return new NoOpExecution();
}
const player = this.mg.player(intent.playerID);
player = this.mg.player(intent.playerID);
if (player.clientID() != intent.clientID) {
console.warn(
`intent ${intent.type} has incorrect clientID ${intent.clientID} for player ${player.name()} with clientID ${player.clientID()}`,
@@ -68,6 +70,7 @@ export class Executor {
return new NoOpExecution();
}
}
switch (intent.type) {
case "attack": {
return new AttackExecution(
@@ -124,6 +127,8 @@ export class Executor {
);
case "troop_ratio":
return new SetTargetTroopRatioExecution(intent.playerID, intent.ratio);
case "embargo":
return new EmbargoExecution(player, intent.targetID, intent.action);
case "build_unit":
return new ConstructionExecution(
intent.playerID,
+8 -10
View File
@@ -69,22 +69,20 @@ export class PortExecution implements Execution {
return;
}
const alliedPorts = this.player()
.alliances()
.map((a) => a.other(this.player()))
const tradingPartnersPorts = this.player()
.tradingPartners()
.flatMap((p) => p.units(UnitType.Port));
const alliedPortsSet = new Set(alliedPorts);
const tradingPartnersPortsSet = new Set(tradingPartnersPorts);
const allyConnections = new Set(
const tradingPartnersConnections = new Set(
Array.from(this.portPaths.keys()).map((p) => p.owner()),
);
allyConnections;
for (const port of alliedPorts) {
if (allyConnections.has(port.owner())) {
for (const port of tradingPartnersPorts) {
if (tradingPartnersConnections.has(port.owner())) {
continue;
}
allyConnections.add(port.owner());
tradingPartnersConnections.add(port.owner());
if (this.computingPaths.has(port)) {
const aStar = this.computingPaths.get(port);
switch (aStar.compute()) {
@@ -114,7 +112,7 @@ export class PortExecution implements Execution {
}
for (const port of this.portPaths.keys()) {
if (!port.isActive() || !alliedPortsSet.has(port)) {
if (!port.isActive() || !tradingPartnersPortsSet.has(port)) {
this.portPaths.delete(port);
this.computingPaths.delete(port);
}
+17 -8
View File
@@ -27,7 +27,7 @@ export class TradeShipExecution implements Execution {
constructor(
private _owner: PlayerID,
private srcPort: Unit,
private dstPort: Unit,
private _dstPort: Unit,
private pathFinder: PathFinder,
// don't modify
private path: TileRef[],
@@ -49,7 +49,12 @@ export class TradeShipExecution implements Execution {
this.active = false;
return;
}
this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, 0, spawn);
this.tradeShip = this.origOwner.buildUnit(
UnitType.TradeShip,
0,
spawn,
this._dstPort,
);
}
if (!this.tradeShip.isActive()) {
@@ -64,8 +69,8 @@ export class TradeShipExecution implements Execution {
if (
!this.wasCaptured &&
(!this.dstPort.isActive() ||
!this.tradeShip.owner().isAlliedWith(this.dstPort.owner()))
(!this._dstPort.isActive() ||
!this.tradeShip.owner().canTrade(this._dstPort.owner()))
) {
this.tradeShip.delete(false);
this.active = false;
@@ -122,17 +127,17 @@ export class TradeShipExecution implements Execution {
const gold = this.mg
.config()
.tradeShipGold(
this.mg.manhattanDist(this.srcPort.tile(), this.dstPort.tile()),
this.mg.manhattanDist(this.srcPort.tile(), this._dstPort.tile()),
);
this.srcPort.owner().addGold(gold);
this.dstPort.owner().addGold(gold);
this._dstPort.owner().addGold(gold);
this.mg.displayMessage(
`Received ${renderNumber(gold)} gold from trade with ${this.srcPort.owner().displayName()}`,
MessageType.SUCCESS,
this.dstPort.owner().id(),
this._dstPort.owner().id(),
);
this.mg.displayMessage(
`Received ${renderNumber(gold)} gold from trade with ${this.dstPort.owner().displayName()}`,
`Received ${renderNumber(gold)} gold from trade with ${this._dstPort.owner().displayName()}`,
MessageType.SUCCESS,
this.srcPort.owner().id(),
);
@@ -154,4 +159,8 @@ export class TradeShipExecution implements Execution {
activeDuringSpawnPhase(): boolean {
return false;
}
dstPort(): TileRef {
return this._dstPort.tile();
}
}
+5 -1
View File
@@ -78,7 +78,11 @@ export class WarshipExecution implements Execution {
.filter((u) => u.owner() != this.warship.owner())
.filter((u) => u != this.warship)
.filter((u) => !u.owner().isAlliedWith(this.warship.owner()))
.filter((u) => !this.alreadySentShell.has(u));
.filter((u) => !this.alreadySentShell.has(u))
.filter(
(u) =>
u.type() != UnitType.TradeShip || u.dstPort().owner() != this.owner(),
);
this.target =
ships.sort((a, b) => {
+18 -2
View File
@@ -214,6 +214,9 @@ export interface Unit {
// Updates
toUpdate(): UnitUpdate;
// Only for some types, otherwise return null
dstPort(): Unit;
}
export interface TerraNullius {
@@ -267,7 +270,12 @@ export interface Player {
units(...types: UnitType[]): Unit[];
unitsIncludingConstruction(type: UnitType): Unit[];
canBuild(type: UnitType, targetTile: TileRef): TileRef | false;
buildUnit(type: UnitType, troops: number, tile: TileRef): Unit;
buildUnit(
type: UnitType,
troops: number,
tile: TileRef,
dstPort?: Unit,
): Unit;
captureUnit(unit: Unit): void;
// Relations & Diplomacy
@@ -300,10 +308,17 @@ export interface Player {
outgoingEmojis(): EmojiMessage[];
sendEmoji(recipient: Player | typeof AllPlayers, emoji: string): void;
// Trading
// Donation
canDonate(recipient: Player): boolean;
donate(recipient: Player, troops: number): void;
// Embargo
hasEmbargoAgainst(other: Player): boolean;
tradingPartners(): Player[];
addEmbargo(other: PlayerID): void;
stopEmbargo(other: PlayerID): void;
canTrade(other: Player): boolean;
// Attacking.
canAttack(tile: TileRef): boolean;
createAttack(
@@ -392,6 +407,7 @@ export interface PlayerInteraction {
canBreakAlliance: boolean;
canTarget: boolean;
canDonate: boolean;
canEmbargo: boolean;
}
export interface EmojiMessage {
+1
View File
@@ -95,6 +95,7 @@ export interface PlayerUpdate {
troops: number;
targetTroopRatio: number;
allies: number[];
embargoes: Set<PlayerID>;
isTraitor: boolean;
targets: number[];
outgoingEmojis: EmojiMessage[];
+4
View File
@@ -191,6 +191,10 @@ export class PlayerView {
return this.data.outgoingAllianceRequests.some((id) => other.id() == id);
}
hasEmbargoAgainst(other: PlayerView): boolean {
return this.data.embargoes.has(other.id());
}
profile(): Promise<PlayerProfile> {
return this.game.worker.playerProfile(this.smallID());
}
+32 -1
View File
@@ -62,6 +62,8 @@ export class PlayerImpl implements Player {
isTraitor_ = false;
private embargoes: Set<PlayerID> = new Set();
public _borderTiles: Set<TileRef> = new Set();
public _units: UnitImpl[] = [];
@@ -123,6 +125,7 @@ export class PlayerImpl implements Player {
troops: this.troops(),
targetTroopRatio: this.targetTroopRatio(),
allies: this.alliances().map((a) => a.other(this).smallID()),
embargoes: this.embargoes,
isTraitor: this.isTraitor(),
targets: this.targets().map((p) => p.smallID()),
outgoingEmojis: this.outgoingEmojis(),
@@ -502,6 +505,28 @@ export class PlayerImpl implements Player {
);
}
hasEmbargoAgainst(other: Player): boolean {
return this.embargoes.has(other.id());
}
canTrade(other: Player): boolean {
return !other.hasEmbargoAgainst(this) && !this.hasEmbargoAgainst(other);
}
addEmbargo(other: PlayerID): void {
this.embargoes.add(other);
}
stopEmbargo(other: PlayerID): void {
this.embargoes.delete(other);
}
tradingPartners(): Player[] {
return this.mg
.players()
.filter((other) => other != this && this.canTrade(other));
}
gold(): Gold {
return this._gold;
}
@@ -588,7 +613,12 @@ export class PlayerImpl implements Player {
);
}
buildUnit(type: UnitType, troops: number, spawnTile: TileRef): UnitImpl {
buildUnit(
type: UnitType,
troops: number,
spawnTile: TileRef,
dstPort?: Unit,
): UnitImpl {
const cost = this.mg.unitInfo(type).cost(this);
const b = new UnitImpl(
type,
@@ -597,6 +627,7 @@ export class PlayerImpl implements Player {
troops,
this.mg.nextUnitID(),
this,
dstPort,
);
this._units.push(b);
this.removeGold(cost);
+5
View File
@@ -21,6 +21,7 @@ export class UnitImpl implements Unit {
private _troops: number,
private _id: number,
public _owner: PlayerImpl,
private _dstPort?: Unit,
) {
// default to 60% health (or 1.2 is no health specified)
this._health = (this.mg.unitInfo(_type).maxHealth ?? 2) * 0.6;
@@ -141,4 +142,8 @@ export class UnitImpl implements Unit {
toString(): string {
return `Unit:${this._type},owner:${this.owner().name()}`;
}
dstPort(): Unit {
return this._dstPort;
}
}