mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-04 22:52:00 +00:00
Merge branch 'main' into main
This commit is contained in:
@@ -47,4 +47,13 @@ export class PseudoRandom {
|
||||
chance(odds: number): boolean {
|
||||
return this.nextInt(0, odds) == 0;
|
||||
}
|
||||
|
||||
shuffleArray(array: any[]) {
|
||||
for (let i = array.length - 1; i >= 0; i--) {
|
||||
const j = Math.floor(this.nextInt(0, i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
+12
-11
@@ -133,11 +133,12 @@ const BaseIntentSchema = z.object({
|
||||
"build_unit",
|
||||
]),
|
||||
clientID: ID,
|
||||
playerID: ID,
|
||||
});
|
||||
|
||||
export const AttackIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("attack"),
|
||||
attackerID: ID,
|
||||
playerID: ID,
|
||||
targetID: ID.nullable(),
|
||||
troops: z.number().nullable(),
|
||||
});
|
||||
@@ -154,7 +155,7 @@ export const SpawnIntentSchema = BaseIntentSchema.extend({
|
||||
|
||||
export const BoatAttackIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("boat"),
|
||||
attackerID: ID,
|
||||
playerID: ID,
|
||||
targetID: ID.nullable(),
|
||||
troops: z.number().nullable(),
|
||||
x: z.number(),
|
||||
@@ -163,52 +164,52 @@ export const BoatAttackIntentSchema = BaseIntentSchema.extend({
|
||||
|
||||
export const AllianceRequestIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("allianceRequest"),
|
||||
requestor: ID,
|
||||
playerID: ID,
|
||||
recipient: ID,
|
||||
});
|
||||
|
||||
export const AllianceRequestReplyIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("allianceRequestReply"),
|
||||
requestor: ID, // The one who made the original alliance request
|
||||
recipient: ID,
|
||||
playerID: ID,
|
||||
accept: z.boolean(),
|
||||
});
|
||||
|
||||
export const BreakAllianceIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("breakAlliance"),
|
||||
requestor: ID, // The one who made the original alliance request
|
||||
playerID: ID,
|
||||
recipient: ID,
|
||||
});
|
||||
|
||||
export const TargetPlayerIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("targetPlayer"),
|
||||
requestor: ID,
|
||||
playerID: ID,
|
||||
target: ID,
|
||||
});
|
||||
|
||||
export const EmojiIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("emoji"),
|
||||
sender: ID,
|
||||
playerID: ID,
|
||||
recipient: z.union([ID, z.literal(AllPlayers)]),
|
||||
emoji: EmojiSchema,
|
||||
});
|
||||
|
||||
export const DonateIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("donate"),
|
||||
sender: ID,
|
||||
playerID: ID,
|
||||
recipient: ID,
|
||||
troops: z.number().nullable(),
|
||||
});
|
||||
|
||||
export const TargetTroopRatioIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("troop_ratio"),
|
||||
player: ID,
|
||||
playerID: ID,
|
||||
ratio: z.number().min(0).max(1),
|
||||
});
|
||||
|
||||
export const BuildUnitIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("build_unit"),
|
||||
player: ID,
|
||||
playerID: ID,
|
||||
unit: z.nativeEnum(UnitType),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
@@ -267,6 +268,7 @@ export const ServerMessageSchema = z.union([
|
||||
const ClientBaseMessageSchema = z.object({
|
||||
type: z.enum(["winner", "join", "intent", "ping", "log"]),
|
||||
clientID: ID,
|
||||
persistentID: SafeString.nullable(), // WARNING: persistent id is private.
|
||||
gameID: ID,
|
||||
});
|
||||
|
||||
@@ -294,7 +296,6 @@ export const ClientIntentMessageSchema = ClientBaseMessageSchema.extend({
|
||||
// WARNING: never send this message to clients.
|
||||
export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({
|
||||
type: z.literal("join"),
|
||||
persistentID: SafeString, // WARNING: persistent id is private.
|
||||
lastTurn: z.number(), // The last turn the client saw.
|
||||
username: SafeString,
|
||||
});
|
||||
|
||||
@@ -62,6 +62,17 @@ export class AttackExecution implements Execution {
|
||||
}
|
||||
this.mg = mg;
|
||||
|
||||
if (!mg.hasPlayer(this._ownerID)) {
|
||||
console.warn(`player ${this._ownerID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (this._targetID != null && !mg.hasPlayer(this._targetID)) {
|
||||
console.warn(`target ${this._targetID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._owner = mg.player(this._ownerID);
|
||||
this.target =
|
||||
this._targetID == this.mg.terraNullius().id()
|
||||
@@ -90,8 +101,8 @@ export class AttackExecution implements Execution {
|
||||
.config()
|
||||
.attackAmount(this._owner, this.target);
|
||||
}
|
||||
this.startTroops = Math.min(this._owner.troops(), this.startTroops);
|
||||
if (this.removeTroops) {
|
||||
this.startTroops = Math.min(this._owner.troops(), this.startTroops);
|
||||
this._owner.removeTroops(this.startTroops);
|
||||
}
|
||||
this.attack = this._owner.createAttack(
|
||||
|
||||
@@ -22,6 +22,11 @@ export class CityExecution implements Execution {
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
if (!mg.hasPlayer(this.ownerId)) {
|
||||
console.warn(`CityExecution: player ${this.ownerId} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.player = mg.player(this.ownerId);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ export class ConstructionExecution implements Execution {
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
if (!mg.hasPlayer(this.ownerId)) {
|
||||
console.warn(`ConstructionExecution: owner ${this.ownerId} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.player = mg.player(this.ownerId);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@ export class DefensePostExecution implements Execution {
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
if (!mg.hasPlayer(this.ownerId)) {
|
||||
console.warn(`DefensePostExectuion: owner ${this.ownerId} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.player = mg.player(this.ownerId);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,17 @@ export class DonateExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.senderID)) {
|
||||
console.warn(`DonateExecution: sender ${this.senderID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (!mg.hasPlayer(this.recipientID)) {
|
||||
console.warn(`DonateExecution recipient ${this.recipientID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.sender = mg.player(this.senderID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
if (this.troops == null) {
|
||||
|
||||
@@ -22,6 +22,17 @@ export class EmojiExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.senderID)) {
|
||||
console.warn(`EmojiExecution: sender ${this.senderID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (this.recipientID != AllPlayers && !mg.hasPlayer(this.recipientID)) {
|
||||
console.warn(`EmojiExecution: recipient ${this.recipientID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.requestor = mg.player(this.senderID);
|
||||
this.recipient =
|
||||
this.recipientID == AllPlayers ? AllPlayers : mg.player(this.recipientID);
|
||||
|
||||
@@ -33,6 +33,7 @@ import { DonateExecution } from "./DonateExecution";
|
||||
import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution";
|
||||
import { ConstructionExecution } from "./ConstructionExecution";
|
||||
import { fixProfaneUsername, isProfaneUsername } from "../validations/username";
|
||||
import { NoOpExecution } from "./NoOpExecution";
|
||||
|
||||
export class Executor {
|
||||
// private random = new PseudoRandom(999)
|
||||
@@ -52,11 +53,26 @@ export class Executor {
|
||||
}
|
||||
|
||||
createExec(intent: Intent): Execution {
|
||||
if (intent.type != "spawn") {
|
||||
if (!this.mg.hasPlayer(intent.playerID)) {
|
||||
console.warn(
|
||||
`player ${intent.playerID} not found on intent ${intent.type}`,
|
||||
);
|
||||
return new NoOpExecution();
|
||||
}
|
||||
const 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()}`,
|
||||
);
|
||||
return new NoOpExecution();
|
||||
}
|
||||
}
|
||||
switch (intent.type) {
|
||||
case "attack": {
|
||||
return new AttackExecution(
|
||||
intent.troops,
|
||||
intent.attackerID,
|
||||
intent.playerID,
|
||||
intent.targetID,
|
||||
null,
|
||||
);
|
||||
@@ -77,40 +93,40 @@ export class Executor {
|
||||
);
|
||||
case "boat":
|
||||
return new TransportShipExecution(
|
||||
intent.attackerID,
|
||||
intent.playerID,
|
||||
intent.targetID,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
intent.troops,
|
||||
);
|
||||
case "allianceRequest":
|
||||
return new AllianceRequestExecution(intent.requestor, intent.recipient);
|
||||
return new AllianceRequestExecution(intent.playerID, intent.recipient);
|
||||
case "allianceRequestReply":
|
||||
return new AllianceRequestReplyExecution(
|
||||
intent.requestor,
|
||||
intent.recipient,
|
||||
intent.playerID,
|
||||
intent.accept,
|
||||
);
|
||||
case "breakAlliance":
|
||||
return new BreakAllianceExecution(intent.requestor, intent.recipient);
|
||||
return new BreakAllianceExecution(intent.playerID, intent.recipient);
|
||||
case "targetPlayer":
|
||||
return new TargetPlayerExecution(intent.requestor, intent.target);
|
||||
return new TargetPlayerExecution(intent.playerID, intent.target);
|
||||
case "emoji":
|
||||
return new EmojiExecution(
|
||||
intent.sender,
|
||||
intent.playerID,
|
||||
intent.recipient,
|
||||
intent.emoji,
|
||||
);
|
||||
case "donate":
|
||||
return new DonateExecution(
|
||||
intent.sender,
|
||||
intent.playerID,
|
||||
intent.recipient,
|
||||
intent.troops,
|
||||
);
|
||||
case "troop_ratio":
|
||||
return new SetTargetTroopRatioExecution(intent.player, intent.ratio);
|
||||
return new SetTargetTroopRatioExecution(intent.playerID, intent.ratio);
|
||||
case "build_unit":
|
||||
return new ConstructionExecution(
|
||||
intent.player,
|
||||
intent.playerID,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
intent.unit,
|
||||
);
|
||||
|
||||
@@ -546,9 +546,6 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
sendAttack(toAttack: Player | TerraNullius) {
|
||||
console.log(
|
||||
`${this.player.name()} sending troops ${renderTroops(this.player.troops() / 5)}`,
|
||||
);
|
||||
this.mg.addExecution(
|
||||
new AttackExecution(
|
||||
this.player.troops() / 5,
|
||||
|
||||
@@ -43,6 +43,12 @@ export class MirvExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.senderID)) {
|
||||
console.warn(`MIRVExecution: player ${this.senderID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.random = new PseudoRandom(mg.ticks() + simpleHash(this.senderID));
|
||||
this.mg = mg;
|
||||
this.pathFinder = PathFinder.Mini(mg, 10_000, true);
|
||||
|
||||
@@ -22,6 +22,12 @@ export class MissileSiloExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this._owner)) {
|
||||
console.warn(`MissileSiloExecution: owner ${this._owner} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.mg = mg;
|
||||
this.player = mg.player(this._owner);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Execution, Game, Player } from "../game/Game";
|
||||
|
||||
export class NoOpExecution implements Execution {
|
||||
isActive(): boolean {
|
||||
return false;
|
||||
}
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
init(mg: Game, ticks: number): void {}
|
||||
tick(ticks: number): void {}
|
||||
owner(): Player {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,12 @@ export class NukeExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.senderID)) {
|
||||
console.warn(`NukeExecution: sender ${this.senderID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.mg = mg;
|
||||
this.player = mg.player(this.senderID);
|
||||
this.random = new PseudoRandom(ticks);
|
||||
@@ -120,18 +126,13 @@ export class NukeExecution implements Execution {
|
||||
return (d <= magnitude.inner || rand.chance(2)) && d <= magnitude.outer;
|
||||
});
|
||||
|
||||
const ratio = Object.fromEntries(
|
||||
this.mg
|
||||
.players()
|
||||
.map((p) => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()]),
|
||||
);
|
||||
const attacked = new Map<Player, number>();
|
||||
for (const tile of toDestroy) {
|
||||
const owner = this.mg.owner(tile);
|
||||
if (owner.isPlayer()) {
|
||||
const mp = this.mg.player(owner.id());
|
||||
mp.relinquish(tile);
|
||||
mp.removeTroops(2 * ratio[mp.id()]);
|
||||
mp.removeTroops((5 * mp.population()) / mp.numTilesOwned());
|
||||
if (!attacked.has(mp)) {
|
||||
attacked.set(mp, 0);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@ export class PlayerExecution implements Execution {
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number) {
|
||||
if (!mg.hasPlayer(this.playerID)) {
|
||||
console.warn(`PlayerExecution: player ${this.playerID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.mg = mg;
|
||||
this.config = mg.config();
|
||||
this.player = mg.player(this.playerID);
|
||||
|
||||
@@ -31,6 +31,11 @@ export class PortExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this._owner)) {
|
||||
console.warn(`PortExecution: player ${this._owner} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.mg = mg;
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ export class SetTargetTroopRatioExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.playerID)) {
|
||||
console.warn(
|
||||
`SetTargetTRoopRatioExecution: player ${this.playerID} not found`,
|
||||
);
|
||||
}
|
||||
this.player = mg.player(this.playerID);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export class SpawnExecution implements Execution {
|
||||
this.active = false;
|
||||
|
||||
if (!this.mg.inSpawnPhase()) {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,19 @@ export class TargetPlayerExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.requestorID)) {
|
||||
console.warn(
|
||||
`TargetPlayerExecution: requestor ${this.requestorID} not found`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (!mg.hasPlayer(this.targetID)) {
|
||||
console.warn(`TargetPlayerExecution: target ${this.targetID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.requestor = mg.player(this.requestorID);
|
||||
this.target = mg.player(this.targetID);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,19 @@ export class TransportShipExecution implements Execution {
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number) {
|
||||
if (!mg.hasPlayer(this.attackerID)) {
|
||||
console.warn(
|
||||
`TransportShipExecution: attacker ${this.attackerID} not found`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (this.targetID != null && !mg.hasPlayer(this.targetID)) {
|
||||
console.warn(`TransportShipExecution: target ${this.targetID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastMove = ticks;
|
||||
this.mg = mg;
|
||||
this.pathFinder = PathFinder.Mini(mg, 10_000, false, 2);
|
||||
|
||||
@@ -43,6 +43,11 @@ export class WarshipExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.playerID)) {
|
||||
console.log(`WarshipExecution: player ${this.playerID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.pathfinder = PathFinder.Mini(mg, 5000, false);
|
||||
this._owner = mg.player(this.playerID);
|
||||
this.mg = mg;
|
||||
|
||||
@@ -19,6 +19,21 @@ export class AllianceRequestExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.requestorID)) {
|
||||
console.warn(
|
||||
`AllianceRequestExecution requester ${this.requestorID} not found`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (!mg.hasPlayer(this.recipientID)) {
|
||||
console.warn(
|
||||
`AllianceRequestExecution recipient ${this.recipientID} not found`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.mg = mg;
|
||||
this.requestor = mg.player(this.requestorID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
|
||||
@@ -20,6 +20,20 @@ export class AllianceRequestReplyExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.requestorID)) {
|
||||
console.warn(
|
||||
`AllianceRequestReplyExecution requester ${this.requestorID} not found`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (!mg.hasPlayer(this.recipientID)) {
|
||||
console.warn(
|
||||
`AllianceRequestReplyExecution recipient ${this.recipientID} not found`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.mg = mg;
|
||||
this.requestor = mg.player(this.requestorID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
|
||||
@@ -19,6 +19,20 @@ export class BreakAllianceExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.requestorID)) {
|
||||
console.warn(
|
||||
`BreakAllianceExecution requester ${this.requestorID} not found`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (!mg.hasPlayer(this.recipientID)) {
|
||||
console.warn(
|
||||
`BreakAllianceExecution: recipient ${this.recipientID} not found`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.requestor = mg.player(this.requestorID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
this.mg = mg;
|
||||
|
||||
Reference in New Issue
Block a user