Merge branch 'v26'
|
Before Width: | Height: | Size: 739 B After Width: | Height: | Size: 303 B |
|
Before Width: | Height: | Size: 585 B After Width: | Height: | Size: 305 B |
|
Before Width: | Height: | Size: 592 B After Width: | Height: | Size: 262 B |
|
Before Width: | Height: | Size: 801 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 537 B After Width: | Height: | Size: 227 B |
|
Before Width: | Height: | Size: 726 B After Width: | Height: | Size: 259 B |
@@ -144,7 +144,7 @@
|
||||
"instant_build": "Instant build",
|
||||
"infinite_gold": "Infinite gold",
|
||||
"infinite_troops": "Infinite troops",
|
||||
"compact_map": "Mini Map",
|
||||
"compact_map": "Compact Map",
|
||||
"max_timer": "Game length (minutes)",
|
||||
"disable_nukes": "Disable Nukes",
|
||||
"automatic_difficulty": "Automatic Difficulty",
|
||||
|
||||
|
Before Width: | Height: | Size: 316 B |
|
Before Width: | Height: | Size: 438 B |
|
Before Width: | Height: | Size: 246 B |
|
Before Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 204 B |
|
Before Width: | Height: | Size: 341 B |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 409 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -1,21 +1,15 @@
|
||||
import miniBigSmoke from "../../../resources/sprites/bigsmoke.png";
|
||||
import buildingExplosion from "../../../resources/sprites/buildingExplosion.png";
|
||||
import conquestSword from "../../../resources/sprites/conquestSword.png";
|
||||
import dust from "../../../resources/sprites/dust.png";
|
||||
import miniExplosion from "../../../resources/sprites/miniExplosion.png";
|
||||
import miniFire from "../../../resources/sprites/minifire.png";
|
||||
import nuke from "../../../resources/sprites/nukeExplosion.png";
|
||||
import SAMExplosion from "../../../resources/sprites/samExplosion.png";
|
||||
import sinkingShip from "../../../resources/sprites/sinkingShip.png";
|
||||
import miniSmoke from "../../../resources/sprites/smoke.png";
|
||||
import miniSmokeAndFire from "../../../resources/sprites/smokeAndFire.png";
|
||||
import unitExplosion from "../../../resources/sprites/unitExplosion.png";
|
||||
|
||||
import bats from "../../../resources/sprites/halloween/bats.png";
|
||||
import bubble from "../../../resources/sprites/halloween/bubble.png";
|
||||
import ghost from "../../../resources/sprites/halloween/ghost.png";
|
||||
import minifireGreen from "../../../resources/sprites/halloween/minifireGreen.png";
|
||||
import shark from "../../../resources/sprites/halloween/shark.png";
|
||||
import skull from "../../../resources/sprites/halloween/skull.png";
|
||||
import skullNuke from "../../../resources/sprites/halloween/skullNuke.png";
|
||||
import miniSmokeAndFireGreen from "../../../resources/sprites/halloween/smokeAndFireGreen.png";
|
||||
import tentacle from "../../../resources/sprites/halloween/tentacle.png";
|
||||
import tornado from "../../../resources/sprites/halloween/tornado.png";
|
||||
|
||||
import { Theme } from "../../core/configuration/Config";
|
||||
import { PlayerView } from "../../core/game/GameView";
|
||||
import { AnimatedSprite } from "./AnimatedSprite";
|
||||
@@ -34,7 +28,7 @@ type AnimatedSpriteConfig = {
|
||||
|
||||
const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
|
||||
[FxType.MiniFire]: {
|
||||
url: minifireGreen,
|
||||
url: miniFire,
|
||||
frameWidth: 7,
|
||||
frameCount: 6,
|
||||
frameDuration: 100,
|
||||
@@ -43,28 +37,28 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
|
||||
originY: 11,
|
||||
},
|
||||
[FxType.MiniSmoke]: {
|
||||
url: ghost,
|
||||
frameWidth: 10,
|
||||
frameCount: 5,
|
||||
frameDuration: 100,
|
||||
url: miniSmoke,
|
||||
frameWidth: 11,
|
||||
frameCount: 4,
|
||||
frameDuration: 120,
|
||||
looping: true,
|
||||
originX: 4,
|
||||
originX: 2,
|
||||
originY: 10,
|
||||
},
|
||||
[FxType.MiniBigSmoke]: {
|
||||
url: bats,
|
||||
frameWidth: 21,
|
||||
frameCount: 6,
|
||||
url: miniBigSmoke,
|
||||
frameWidth: 24,
|
||||
frameCount: 5,
|
||||
frameDuration: 120,
|
||||
looping: true,
|
||||
originX: 9,
|
||||
originY: 14,
|
||||
},
|
||||
[FxType.MiniSmokeAndFire]: {
|
||||
url: miniSmokeAndFireGreen,
|
||||
url: miniSmokeAndFire,
|
||||
frameWidth: 24,
|
||||
frameCount: 5,
|
||||
frameDuration: 90,
|
||||
frameDuration: 120,
|
||||
looping: true,
|
||||
originX: 9,
|
||||
originY: 14,
|
||||
@@ -96,15 +90,6 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
|
||||
originX: 9,
|
||||
originY: 9,
|
||||
},
|
||||
[FxType.SinkingShip]: {
|
||||
url: sinkingShip,
|
||||
frameWidth: 16,
|
||||
frameCount: 14,
|
||||
frameDuration: 90,
|
||||
looping: false,
|
||||
originX: 7,
|
||||
originY: 7,
|
||||
},
|
||||
[FxType.BuildingExplosion]: {
|
||||
url: buildingExplosion,
|
||||
frameWidth: 17,
|
||||
@@ -114,14 +99,23 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
|
||||
originX: 8,
|
||||
originY: 8,
|
||||
},
|
||||
[FxType.Nuke]: {
|
||||
url: skullNuke,
|
||||
frameWidth: 42,
|
||||
frameCount: 19,
|
||||
frameDuration: 50,
|
||||
[FxType.SinkingShip]: {
|
||||
url: sinkingShip,
|
||||
frameWidth: 16,
|
||||
frameCount: 14,
|
||||
frameDuration: 90,
|
||||
looping: false,
|
||||
originX: 20,
|
||||
originY: 21,
|
||||
originX: 7,
|
||||
originY: 7,
|
||||
},
|
||||
[FxType.Nuke]: {
|
||||
url: nuke,
|
||||
frameWidth: 60,
|
||||
frameCount: 9,
|
||||
frameDuration: 70,
|
||||
looping: false,
|
||||
originX: 30,
|
||||
originY: 30,
|
||||
},
|
||||
[FxType.SAMExplosion]: {
|
||||
url: SAMExplosion,
|
||||
@@ -133,51 +127,16 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
|
||||
originY: 19,
|
||||
},
|
||||
[FxType.Conquest]: {
|
||||
url: skull,
|
||||
frameWidth: 14,
|
||||
frameCount: 14,
|
||||
frameDuration: 90,
|
||||
looping: false,
|
||||
originX: 7,
|
||||
originY: 23,
|
||||
},
|
||||
[FxType.Tentacle]: {
|
||||
url: tentacle,
|
||||
frameWidth: 22,
|
||||
frameCount: 26,
|
||||
frameDuration: 90,
|
||||
looping: false,
|
||||
originX: 13,
|
||||
originY: 28,
|
||||
},
|
||||
[FxType.Shark]: {
|
||||
url: shark,
|
||||
frameWidth: 25,
|
||||
frameCount: 14,
|
||||
frameDuration: 90,
|
||||
looping: false,
|
||||
originX: 13,
|
||||
originY: 8,
|
||||
},
|
||||
[FxType.Bubble]: {
|
||||
url: bubble,
|
||||
frameWidth: 22,
|
||||
frameCount: 13,
|
||||
frameDuration: 80,
|
||||
looping: false,
|
||||
originX: 13,
|
||||
originY: 8,
|
||||
},
|
||||
[FxType.Tornado]: {
|
||||
url: tornado,
|
||||
frameWidth: 30,
|
||||
url: conquestSword,
|
||||
frameWidth: 21,
|
||||
frameCount: 10,
|
||||
frameDuration: 80,
|
||||
looping: true,
|
||||
originX: 11,
|
||||
originY: 22,
|
||||
frameDuration: 90,
|
||||
looping: false,
|
||||
originX: 10,
|
||||
originY: 16,
|
||||
},
|
||||
};
|
||||
|
||||
export class AnimatedSpriteLoader {
|
||||
private animatedSpriteImageMap: Map<FxType, HTMLCanvasElement> = new Map();
|
||||
// Do not color the same sprite twice
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Colord } from "colord";
|
||||
import miniPumpkin from "../../../resources/sprites/halloween/miniPumpkin.png";
|
||||
import pumpkin from "../../../resources/sprites/halloween/pumpkin.png";
|
||||
import atomBombSprite from "../../../resources/sprites/atombomb.png";
|
||||
import hydrogenBombSprite from "../../../resources/sprites/hydrogenbomb.png";
|
||||
import mirvSprite from "../../../resources/sprites/mirv2.png";
|
||||
import samMissileSprite from "../../../resources/sprites/samMissile.png";
|
||||
import tradeShipSprite from "../../../resources/sprites/tradeship.png";
|
||||
@@ -26,8 +26,8 @@ const SPRITE_CONFIG: Partial<Record<UnitType | TrainTypeSprite, string>> = {
|
||||
[UnitType.TransportShip]: transportShipSprite,
|
||||
[UnitType.Warship]: warshipSprite,
|
||||
[UnitType.SAMMissile]: samMissileSprite,
|
||||
[UnitType.AtomBomb]: miniPumpkin,
|
||||
[UnitType.HydrogenBomb]: pumpkin,
|
||||
[UnitType.AtomBomb]: atomBombSprite,
|
||||
[UnitType.HydrogenBomb]: hydrogenBombSprite,
|
||||
[UnitType.TradeShip]: tradeShipSprite,
|
||||
[UnitType.MIRV]: mirvSprite,
|
||||
[TrainTypeSprite.Engine]: trainEngineSprite,
|
||||
|
||||
@@ -26,6 +26,7 @@ export function conquestFxFactory(
|
||||
x,
|
||||
y,
|
||||
FxType.Conquest,
|
||||
2500,
|
||||
);
|
||||
const fadeAnimation = new FadeFx(swordAnimation, 0.1, 0.6);
|
||||
conquestFx.push(fadeAnimation);
|
||||
|
||||
@@ -16,8 +16,4 @@ export enum FxType {
|
||||
UnderConstruction = "UnderConstruction",
|
||||
Dust = "Dust",
|
||||
Conquest = "Conquest",
|
||||
Tentacle = "Tentacle",
|
||||
Shark = "Shark",
|
||||
Bubble = "Bubble",
|
||||
Tornado = "Tornado",
|
||||
}
|
||||
|
||||
@@ -19,35 +19,6 @@ function fadeInOut(
|
||||
return 1 - f * f;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Move a sprite around
|
||||
*/
|
||||
export class MoveSpriteFx implements Fx {
|
||||
private originX: number;
|
||||
private originY: number;
|
||||
constructor(
|
||||
private fxToMove: SpriteFx,
|
||||
private toX: number,
|
||||
private toY: number,
|
||||
private fadeIn: number = 0.1,
|
||||
private fadeOut: number = 0.9,
|
||||
) {
|
||||
this.originX = fxToMove.x;
|
||||
this.originY = fxToMove.y;
|
||||
}
|
||||
|
||||
renderTick(duration: number, ctx: CanvasRenderingContext2D): boolean {
|
||||
const t = this.fxToMove.getElapsedTime() / this.fxToMove.getDuration();
|
||||
this.fxToMove.x = Math.floor(this.originX * (1 - t) + this.toX * t);
|
||||
this.fxToMove.y = Math.floor(this.originY * (1 - t) + this.toY * t);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = fadeInOut(t, this.fadeIn, this.fadeOut);
|
||||
const result = this.fxToMove.renderTick(duration, ctx);
|
||||
ctx.restore();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fade in/out another FX
|
||||
*/
|
||||
@@ -78,8 +49,8 @@ export class SpriteFx implements Fx {
|
||||
protected waitToTheEnd = false;
|
||||
constructor(
|
||||
animatedSpriteLoader: AnimatedSpriteLoader,
|
||||
public x: number,
|
||||
public y: number,
|
||||
protected x: number,
|
||||
protected y: number,
|
||||
fxType: FxType,
|
||||
duration?: number,
|
||||
owner?: PlayerView,
|
||||
|
||||
@@ -14,7 +14,7 @@ import { conquestFxFactory } from "../fx/ConquestFx";
|
||||
import { Fx, FxType } from "../fx/Fx";
|
||||
import { NukeAreaFx } from "../fx/NukeAreaFx";
|
||||
import { nukeFxFactory, ShockwaveFx } from "../fx/NukeFx";
|
||||
import { FadeFx, MoveSpriteFx, SpriteFx } from "../fx/SpriteFx";
|
||||
import { SpriteFx } from "../fx/SpriteFx";
|
||||
import { TargetFx } from "../fx/TargetFx";
|
||||
import { TextFx } from "../fx/TextFx";
|
||||
import { UnitExplosionFx } from "../fx/UnitExplosionFx";
|
||||
@@ -22,8 +22,6 @@ import { Layer } from "./Layer";
|
||||
export class FxLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D;
|
||||
private lastRandomEvent: number = 0;
|
||||
private randomEventRate: number = 8;
|
||||
|
||||
private lastRefresh: number = 0;
|
||||
private refreshRate: number = 10;
|
||||
@@ -44,14 +42,6 @@ export class FxLayer implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this.game.config().userSettings()?.fxLayer()) {
|
||||
return;
|
||||
}
|
||||
this.lastRandomEvent += 1;
|
||||
if (this.lastRandomEvent > this.randomEventRate) {
|
||||
this.lastRandomEvent = 0;
|
||||
this.randomEvent();
|
||||
}
|
||||
this.manageBoatTargetFx();
|
||||
this.game
|
||||
.updatesSinceLastTick()
|
||||
@@ -154,72 +144,6 @@ export class FxLayer implements Layer {
|
||||
this.allFx.push(textFx);
|
||||
}
|
||||
|
||||
randomEvent() {
|
||||
const randX = Math.floor(Math.random() * this.game.width());
|
||||
const randY = Math.floor(Math.random() * this.game.height());
|
||||
const ref = this.game.ref(randX, randY);
|
||||
if (this.game.isOcean(ref) && !this.game.isShoreline(ref)) {
|
||||
const animation = Math.floor(Math.random() * 4);
|
||||
if (animation === 0) {
|
||||
const fx = new SpriteFx(
|
||||
this.animatedSpriteLoader,
|
||||
randX,
|
||||
randY,
|
||||
FxType.Shark,
|
||||
);
|
||||
this.allFx.push(fx);
|
||||
} else if (animation === 1) {
|
||||
const fx = new SpriteFx(
|
||||
this.animatedSpriteLoader,
|
||||
randX,
|
||||
randY,
|
||||
FxType.Bubble,
|
||||
);
|
||||
this.allFx.push(fx);
|
||||
} else if (animation === 2) {
|
||||
const fx = new MoveSpriteFx(
|
||||
new SpriteFx(
|
||||
this.animatedSpriteLoader,
|
||||
randX,
|
||||
randY,
|
||||
FxType.Tornado,
|
||||
6000,
|
||||
),
|
||||
randX - 40,
|
||||
randY,
|
||||
0.1,
|
||||
0.8,
|
||||
);
|
||||
this.allFx.push(fx);
|
||||
} else if (animation === 3) {
|
||||
const fx = new FadeFx(
|
||||
new SpriteFx(
|
||||
this.animatedSpriteLoader,
|
||||
randX,
|
||||
randY,
|
||||
FxType.Tentacle,
|
||||
),
|
||||
0.1,
|
||||
0.8,
|
||||
);
|
||||
this.allFx.push(fx);
|
||||
}
|
||||
} else {
|
||||
const ghost = new FadeFx(
|
||||
new SpriteFx(
|
||||
this.animatedSpriteLoader,
|
||||
randX,
|
||||
randY,
|
||||
FxType.MiniSmoke,
|
||||
4000,
|
||||
),
|
||||
0.1,
|
||||
0.8,
|
||||
);
|
||||
this.allFx.push(ghost);
|
||||
}
|
||||
}
|
||||
|
||||
onUnitEvent(unit: UnitView) {
|
||||
switch (unit.type()) {
|
||||
case UnitType.TransportShip: {
|
||||
|
||||
@@ -293,17 +293,17 @@ export function createRandomName(
|
||||
}
|
||||
|
||||
export const emojiTable = [
|
||||
["😀", "😊", "😇", "😎", "😈"],
|
||||
["😀", "😊", "🥰", "😇", "😎"],
|
||||
["😞", "🥺", "😭", "😱", "😡"],
|
||||
["⏳", "🥱", "🤦♂️", "🖕", "🤡"],
|
||||
["👋", "👏", "👻", "💪", "🎃"],
|
||||
["😈", "🤡", "🖕", "🥱", "🤦♂️"],
|
||||
["👋", "👏", "🤌", "💪", "🫡"],
|
||||
["👍", "👎", "❓", "🐔", "🐀"],
|
||||
["🆘", "🤝", "🕊️", "🏳️", "🛡️"],
|
||||
["🤝", "🆘", "🕊️", "🏳️", "⏳"],
|
||||
["🔥", "💥", "💀", "☢️", "⚠️"],
|
||||
["↖️", "⬆️", "↗️", "👑", "🥇"],
|
||||
["⬅️", "🎯", "➡️", "🥈", "🥉"],
|
||||
["↙️", "⬇️", "↘️", "❤️", "💔"],
|
||||
["💰", "🏭", "🚂", "⚓", "⛵"],
|
||||
["💰", "⚓", "⛵", "🏡", "🛡️"],
|
||||
] as const;
|
||||
// 2d to 1d array
|
||||
export const flattenedEmojiTable = emojiTable.flat();
|
||||
|
||||
@@ -136,6 +136,7 @@ export interface Config {
|
||||
deleteUnitCooldown(): Tick;
|
||||
defaultDonationAmount(sender: Player): number;
|
||||
unitInfo(type: UnitType): UnitInfo;
|
||||
tradeShipShortRangeDebuff(): number;
|
||||
tradeShipGold(dist: number, numPorts: number): Gold;
|
||||
tradeShipSpawnRate(
|
||||
numTradeShips: number,
|
||||
|
||||
@@ -173,8 +173,9 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
turnIntervalMs(): number {
|
||||
return 100;
|
||||
}
|
||||
|
||||
gameCreationRate(): number {
|
||||
return 60 * 1000;
|
||||
return 30 * 1000;
|
||||
}
|
||||
|
||||
lobbyMaxPlayers(
|
||||
@@ -378,9 +379,10 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
tradeShipGold(dist: number, numPorts: number): Gold {
|
||||
// Sigmoid: concave start, sharp S-curve middle, linear end - heavily punishes trades under 200
|
||||
// Sigmoid: concave start, sharp S-curve middle, linear end - heavily punishes trades under range debuff.
|
||||
const debuff = this.tradeShipShortRangeDebuff();
|
||||
const baseGold =
|
||||
100_000 / (1 + Math.exp(-0.03 * (dist - 200))) + 100 * dist;
|
||||
100_000 / (1 + Math.exp(-0.03 * (dist - debuff))) + 100 * dist;
|
||||
const numPortBonus = numPorts - 1;
|
||||
// Hyperbolic decay, midpoint at 5 ports, 3x bonus max.
|
||||
const bonus = 1 + 2 * (numPortBonus / (numPortBonus + 5));
|
||||
@@ -584,10 +586,11 @@ export class DefaultConfig implements Config {
|
||||
return 10 * 10;
|
||||
}
|
||||
deletionMarkDuration(): Tick {
|
||||
return 15 * 10;
|
||||
return 30 * 10;
|
||||
}
|
||||
|
||||
deleteUnitCooldown(): Tick {
|
||||
return 15 * 10;
|
||||
return 30 * 10;
|
||||
}
|
||||
emojiMessageDuration(): Tick {
|
||||
return 5 * 10;
|
||||
@@ -784,6 +787,10 @@ export class DefaultConfig implements Config {
|
||||
return 20;
|
||||
}
|
||||
|
||||
tradeShipShortRangeDebuff(): number {
|
||||
return 300;
|
||||
}
|
||||
|
||||
proximityBonusPortsNb(totalPorts: number) {
|
||||
return within(totalPorts / 3, 4, totalPorts);
|
||||
}
|
||||
|
||||
@@ -108,15 +108,15 @@ export class PastelTheme implements Theme {
|
||||
}
|
||||
case TerrainType.Plains:
|
||||
return colord({
|
||||
r: 216,
|
||||
g: 205 - 2 * mag,
|
||||
b: 127,
|
||||
r: 190,
|
||||
g: 220 - 2 * mag,
|
||||
b: 138,
|
||||
});
|
||||
case TerrainType.Highland:
|
||||
return colord({
|
||||
r: 223 + 2 * mag,
|
||||
g: 187 + 2 * mag,
|
||||
b: 132 + 2 * mag,
|
||||
r: 200 + 2 * mag,
|
||||
g: 183 + 2 * mag,
|
||||
b: 138 + 2 * mag,
|
||||
});
|
||||
case TerrainType.Mountain:
|
||||
return colord({
|
||||
|
||||
@@ -58,7 +58,7 @@ export class PortExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
const ports = this.player.tradingPorts(this.port);
|
||||
const ports = this.tradingPorts();
|
||||
|
||||
if (ports.length === 0) {
|
||||
return;
|
||||
@@ -103,4 +103,40 @@ export class PortExecution implements Execution {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It's a probability list, so if an element appears twice it's because it's
|
||||
// twice more likely to be picked later.
|
||||
tradingPorts(): Unit[] {
|
||||
const ports = this.mg
|
||||
.players()
|
||||
.filter((p) => p !== this.port!.owner() && p.canTrade(this.port!.owner()))
|
||||
.flatMap((p) => p.units(UnitType.Port))
|
||||
.sort((p1, p2) => {
|
||||
return (
|
||||
this.mg.manhattanDist(this.port!.tile(), p1.tile()) -
|
||||
this.mg.manhattanDist(this.port!.tile(), p2.tile())
|
||||
);
|
||||
});
|
||||
|
||||
const weightedPorts: Unit[] = [];
|
||||
|
||||
for (const [i, otherPort] of ports.entries()) {
|
||||
const expanded = new Array(otherPort.level()).fill(otherPort);
|
||||
weightedPorts.push(...expanded);
|
||||
const tooClose =
|
||||
this.mg.manhattanDist(this.port!.tile(), otherPort.tile()) <
|
||||
this.mg.config().tradeShipShortRangeDebuff();
|
||||
const closeBonus =
|
||||
i < this.mg.config().proximityBonusPortsNb(ports.length);
|
||||
if (!tooClose && closeBonus) {
|
||||
// If the port is close, but not too close, add it again
|
||||
// to increase the chances of trading with it.
|
||||
weightedPorts.push(...expanded);
|
||||
}
|
||||
if (!tooClose && this.port!.owner().isFriendly(otherPort.owner())) {
|
||||
weightedPorts.push(...expanded);
|
||||
}
|
||||
}
|
||||
return weightedPorts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ const EMOJI_ASSIST_ACCEPT = (["👍", "⛵", "🤝", "🎯"] as const).map(emoji
|
||||
const EMOJI_RELATION_TOO_LOW = (["🥱", "🤦♂️"] as const).map(emojiId);
|
||||
const EMOJI_TARGET_ME = (["🥺", "💀"] as const).map(emojiId);
|
||||
const EMOJI_TARGET_ALLY = (["🕊️", "👎"] as const).map(emojiId);
|
||||
export const EMOJI_HECKLE = (["👻", "🎃"] as const).map(emojiId);
|
||||
export const EMOJI_HECKLE = (["🤡", "😡"] as const).map(emojiId);
|
||||
|
||||
export class BotBehavior {
|
||||
private enemy: Player | null = null;
|
||||
|
||||
@@ -656,7 +656,6 @@ export interface Player {
|
||||
// Misc
|
||||
toUpdate(): PlayerUpdate;
|
||||
playerProfile(): PlayerProfile;
|
||||
tradingPorts(port: Unit): Unit[];
|
||||
// WARNING: this operation is expensive.
|
||||
bestTransportShipSpawn(tile: TileRef): TileRef | false;
|
||||
}
|
||||
|
||||
@@ -1222,34 +1222,4 @@ export class PlayerImpl implements Player {
|
||||
bestTransportShipSpawn(targetTile: TileRef): TileRef | false {
|
||||
return bestShoreDeploymentSource(this.mg, this, targetTile);
|
||||
}
|
||||
|
||||
// It's a probability list, so if an element appears twice it's because it's
|
||||
// twice more likely to be picked later.
|
||||
tradingPorts(port: Unit): Unit[] {
|
||||
const ports = this.mg
|
||||
.players()
|
||||
.filter((p) => p !== port.owner() && p.canTrade(port.owner()))
|
||||
.flatMap((p) => p.units(UnitType.Port))
|
||||
.sort((p1, p2) => {
|
||||
return (
|
||||
this.mg.manhattanDist(port.tile(), p1.tile()) -
|
||||
this.mg.manhattanDist(port.tile(), p2.tile())
|
||||
);
|
||||
});
|
||||
|
||||
const weightedPorts: Unit[] = [];
|
||||
|
||||
for (const [i, otherPort] of ports.entries()) {
|
||||
const expanded = new Array(otherPort.level()).fill(otherPort);
|
||||
weightedPorts.push(...expanded);
|
||||
if (i < this.mg.config().proximityBonusPortsNb(ports.length)) {
|
||||
weightedPorts.push(...expanded);
|
||||
}
|
||||
if (port.owner().isFriendly(otherPort.owner())) {
|
||||
weightedPorts.push(...expanded);
|
||||
}
|
||||
}
|
||||
|
||||
return weightedPorts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,9 @@ describe("DeleteUnitExecution Security Tests", () => {
|
||||
if (!tileOwner.isPlayer() || tileOwner.id() !== player.id()) {
|
||||
throw new Error("Unit is not on player's territory");
|
||||
}
|
||||
|
||||
game.config().deleteUnitCooldown = () => 10;
|
||||
game.config().deletionMarkDuration = () => 10;
|
||||
});
|
||||
|
||||
describe("Security Validations", () => {
|
||||
|
||||
@@ -83,22 +83,6 @@ describe("PlayerImpl", () => {
|
||||
expect(cityToUpgrade).toBe(false);
|
||||
});
|
||||
|
||||
test("Destination ports chances scale with level", () => {
|
||||
game.config().proximityBonusPortsNb = () => 0;
|
||||
|
||||
player.conquer(game.ref(10, 10));
|
||||
const playerPort = player.buildUnit(UnitType.Port, game.ref(10, 10), {});
|
||||
|
||||
other.conquer(game.ref(0, 0));
|
||||
const otherPort = other.buildUnit(UnitType.Port, game.ref(0, 0), {});
|
||||
otherPort.increaseLevel();
|
||||
otherPort.increaseLevel();
|
||||
|
||||
const ports = player.tradingPorts(playerPort);
|
||||
|
||||
expect(ports.length).toBe(3);
|
||||
});
|
||||
|
||||
test("Can't send alliance requests when dead", () => {
|
||||
// conquer other
|
||||
const otherTiles = other.tiles();
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { PortExecution } from "../src/core/execution/PortExecution";
|
||||
import {
|
||||
Game,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
UnitType,
|
||||
} from "../src/core/game/Game";
|
||||
import { setup } from "./util/Setup";
|
||||
|
||||
let game: Game;
|
||||
let player: Player;
|
||||
let other: Player;
|
||||
|
||||
describe("PortExecution", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"half_land_half_ocean",
|
||||
{
|
||||
instantBuild: true,
|
||||
},
|
||||
[
|
||||
new PlayerInfo("player", PlayerType.Human, null, "player_id"),
|
||||
new PlayerInfo("other", PlayerType.Human, null, "other_id"),
|
||||
],
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
player = game.player("player_id");
|
||||
player.addGold(BigInt(1000000));
|
||||
other = game.player("other_id");
|
||||
|
||||
game.config().structureMinDist = () => 10;
|
||||
});
|
||||
|
||||
test("Destination ports chances scale with level", () => {
|
||||
game.config().proximityBonusPortsNb = () => 0;
|
||||
game.config().tradeShipShortRangeDebuff = () => 0;
|
||||
|
||||
player.conquer(game.ref(7, 10));
|
||||
const execution = new PortExecution(player, game.ref(7, 10));
|
||||
execution.init(game, 0);
|
||||
execution.tick(0);
|
||||
|
||||
other.conquer(game.ref(0, 0));
|
||||
const otherPort = other.buildUnit(UnitType.Port, game.ref(0, 0), {});
|
||||
otherPort.increaseLevel();
|
||||
otherPort.increaseLevel();
|
||||
|
||||
const ports = execution.tradingPorts();
|
||||
|
||||
expect(ports.length).toBe(3);
|
||||
});
|
||||
|
||||
test("Trade ship proximity bonus", () => {
|
||||
game.config().proximityBonusPortsNb = () => 10;
|
||||
game.config().tradeShipShortRangeDebuff = () => 0;
|
||||
|
||||
player.conquer(game.ref(7, 10));
|
||||
const execution = new PortExecution(player, game.ref(7, 10));
|
||||
execution.init(game, 0);
|
||||
execution.tick(0);
|
||||
|
||||
other.conquer(game.ref(0, 0));
|
||||
other.buildUnit(UnitType.Port, game.ref(0, 0), {});
|
||||
|
||||
const ports = execution.tradingPorts();
|
||||
|
||||
expect(ports.length).toBe(2);
|
||||
});
|
||||
|
||||
test("Trade ship short range debuff", () => {
|
||||
game.config().proximityBonusPortsNb = () => 10;
|
||||
// Short range debuff cancels out the proximity bonus.
|
||||
game.config().tradeShipShortRangeDebuff = () => 100;
|
||||
|
||||
player.conquer(game.ref(7, 10));
|
||||
const execution = new PortExecution(player, game.ref(7, 10));
|
||||
execution.init(game, 0);
|
||||
execution.tick(0);
|
||||
|
||||
other.conquer(game.ref(0, 0));
|
||||
other.buildUnit(UnitType.Port, game.ref(0, 0), {});
|
||||
|
||||
const ports = execution.tradingPorts();
|
||||
|
||||
expect(ports.length).toBe(1);
|
||||
});
|
||||
});
|
||||