stable
This commit is contained in:
parent
872a70ad8a
commit
8f06f025e8
2 changed files with 190 additions and 39 deletions
187
engine.js
187
engine.js
|
|
@ -1806,6 +1806,7 @@ this.cityById.set(city.id, city);
|
||||||
const bucketKey = this.cityBucketKey(city.x, city.y);
|
const bucketKey = this.cityBucketKey(city.x, city.y);
|
||||||
if (!this.cityBuckets.has(bucketKey)) this.cityBuckets.set(bucketKey, []);
|
if (!this.cityBuckets.has(bucketKey)) this.cityBuckets.set(bucketKey, []);
|
||||||
this.cityBuckets.get(bucketKey).push(city);
|
this.cityBuckets.get(bucketKey).push(city);
|
||||||
|
this.assignNewCityToTerritoryOwner(city, i);
|
||||||
foundedThisTick++;
|
foundedThisTick++;
|
||||||
}
|
}
|
||||||
if (city) {
|
if (city) {
|
||||||
|
|
@ -1872,6 +1873,42 @@ w.mineral[tile] * 0.9 -
|
||||||
w.move[tile] * 1.1 -
|
w.move[tile] * 1.1 -
|
||||||
nearbyPenalty;
|
nearbyPenalty;
|
||||||
}
|
}
|
||||||
|
assignNewCityToTerritoryOwner(city, tile) {
|
||||||
|
const w = this.world;
|
||||||
|
const ownerId = w.polity?.[tile] ?? -1;
|
||||||
|
if (!city || ownerId < 0) return false;
|
||||||
|
const polity = this.getPolityById(ownerId);
|
||||||
|
if (!polity) return false;
|
||||||
|
if (city.polityId != null && city.polityId !== ownerId) return false;
|
||||||
|
const control = clamp(w.control?.[tile] || 0, 0, 1);
|
||||||
|
const contested = !!w.contested?.[tile];
|
||||||
|
if (contested && control < 0.35) return false;
|
||||||
|
const cityEthnicity = this.dominantCityEthnicity(city);
|
||||||
|
const nearest = this.nearestPolityCityToTile(polity, tile);
|
||||||
|
const core = nearest || this.getCityById(polity.centerCityId);
|
||||||
|
const coreEthnicity = this.dominantCityEthnicity(core);
|
||||||
|
const sameEthnicity = cityEthnicity !== null && coreEthnicity !== null && cityEthnicity === coreEthnicity;
|
||||||
|
const differentEthnicity = cityEthnicity !== null && coreEthnicity !== null && cityEthnicity !== coreEthnicity;
|
||||||
|
const distance = nearest ? Math.abs(nearest.x - city.x) + Math.abs(nearest.y - city.y) : 24;
|
||||||
|
let loyalty = 0.38 + control * 0.34 - (contested ? 0.18 : 0) + (sameEthnicity ? 0.10 : 0) - (differentEthnicity ? 0.10 : 0) - clamp((distance - 18) * 0.004, 0, 0.10);
|
||||||
|
this.addCityToPolity(city, polity, clamp(loyalty, 0.18, 0.82));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
nearestPolityCityToTile(polity, tile) {
|
||||||
|
const w = this.world;
|
||||||
|
const x = tile % w.size;
|
||||||
|
const y = Math.floor(tile / w.size);
|
||||||
|
let best = null;
|
||||||
|
let bestDistance = Infinity;
|
||||||
|
for (const city of this.getPolityCities(polity)) {
|
||||||
|
const distance = Math.abs(city.x - x) + Math.abs(city.y - y);
|
||||||
|
if (distance < bestDistance) {
|
||||||
|
best = city;
|
||||||
|
bestDistance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
drawPopulationFromTilesForCity(city, group, amount) {
|
drawPopulationFromTilesForCity(city, group, amount) {
|
||||||
const w = this.world;
|
const w = this.world;
|
||||||
let remaining = Math.max(0, amount);
|
let remaining = Math.max(0, amount);
|
||||||
|
|
@ -2159,8 +2196,10 @@ const capitalDistance = center ? this.effectiveDistance(center, city) : 0;
|
||||||
const capitalFactor = city.id === polity.centerCityId ? 1.28 : clamp(1 - capitalDistance / 96, 0.58, 1);
|
const capitalFactor = city.id === polity.centerCityId ? 1.28 : clamp(1 - capitalDistance / 96, 0.58, 1);
|
||||||
const loyaltyFactor = 0.62 + clamp(city.loyalty ?? 0.5, 0, 1) * 0.58;
|
const loyaltyFactor = 0.62 + clamp(city.loyalty ?? 0.5, 0, 1) * 0.58;
|
||||||
const tradeFactor = 1 + clamp((city.tradeReach || 0) / 60, 0, 0.42) + clamp((city.tradeValue || 0) * 0.10, 0, 0.22);
|
const tradeFactor = 1 + clamp((city.tradeReach || 0) / 60, 0, 0.42) + clamp((city.tradeValue || 0) * 0.10, 0, 0.22);
|
||||||
const baseInfluence = Math.max(1, this.cityInfluence(city)) * capitalFactor * loyaltyFactor * tradeFactor;
|
const popScale = clamp(Math.log1p(city.population || 1) / Math.log(1200), 0, 1.6);
|
||||||
const radius = clamp(Math.ceil(maxRange * (0.62 + clamp(Math.sqrt(city.population || 1) / 30, 0, 0.45))), 6, maxRange);
|
const populationRadiusFactor = clamp(0.55 + popScale * 0.75, 0.55, 1.45);
|
||||||
|
const baseInfluence = Math.max(1, this.cityInfluence(city)) * capitalFactor * loyaltyFactor * tradeFactor * (1 + popScale * 0.32);
|
||||||
|
const radius = clamp(Math.ceil(maxRange * populationRadiusFactor + (city.id === polity.centerCityId ? 2 : 0)), 5, 48);
|
||||||
for (let dy = -radius; dy <= radius; dy++) {
|
for (let dy = -radius; dy <= radius; dy++) {
|
||||||
for (let dx = -radius; dx <= radius; dx++) {
|
for (let dx = -radius; dx <= radius; dx++) {
|
||||||
const manhattan = Math.abs(dx) + Math.abs(dy);
|
const manhattan = Math.abs(dx) + Math.abs(dy);
|
||||||
|
|
@ -2171,15 +2210,16 @@ if (x < 0 || y < 0 || x >= w.size || y >= w.size) continue;
|
||||||
const tile = w.idx(x, y);
|
const tile = w.idx(x, y);
|
||||||
const isWater = w.terrain[tile] === Terrain.WATER;
|
const isWater = w.terrain[tile] === Terrain.WATER;
|
||||||
if (isWater && tile !== cityTile && !w.tradeRoute[tile] && !this.territoryWaterAnchor(tile)) continue;
|
if (isWater && tile !== cityTile && !w.tradeRoute[tile] && !this.territoryWaterAnchor(tile)) continue;
|
||||||
const routeBoost = w.tradeRoute[tile] ? 1.28 : 1;
|
const routeBoost = w.tradeRoute[tile] ? 1.34 + clamp(city.tradeValue || 0, 0, 2) * 0.08 : 1;
|
||||||
const terrainDrag = isWater ? 2.35 : w.move[tile] * (w.terrain[tile] === Terrain.MOUNTAIN ? 1.18 : 1);
|
const terrainDrag = isWater ? 2.35 : w.move[tile] * (w.terrain[tile] === Terrain.MOUNTAIN ? 1.18 : 1);
|
||||||
const effectiveDistance = (manhattan + terrainDrag * 1.4) / routeBoost;
|
const effectiveDistance = (manhattan + terrainDrag * 1.4) / routeBoost;
|
||||||
const distanceFalloff = clamp(1 - effectiveDistance / (radius + 4), 0, 1);
|
const distanceFalloff = clamp(1 - effectiveDistance / (radius + 4), 0, 1);
|
||||||
if (distanceFalloff <= 0) continue;
|
if (distanceFalloff <= 0) continue;
|
||||||
const localEthnicity = w.dominantEthnicity[tile];
|
const localEthnicity = this.dominantTileEthnicity(tile);
|
||||||
const ethnicityFactor = cityEthnicity !== null && localEthnicity >= 0 && cityEthnicity !== localEthnicity ? 0.82 : 1.06;
|
const ethnicityFactor = cityEthnicity !== null && localEthnicity >= 0 && cityEthnicity !== localEthnicity ? 0.88 : 1.07;
|
||||||
const populationAnchor = clamp(Math.sqrt(Math.max(0, w.population[tile])) / 20, 0, 0.28);
|
const populationAnchor = clamp(Math.sqrt(Math.max(0, w.population[tile])) / 20, 0, 0.28);
|
||||||
const score = baseInfluence * distanceFalloff * distanceFalloff * routeBoost * ethnicityFactor * (1 + populationAnchor) / (1 + terrainDrag * 0.18);
|
const coreBoost = manhattan <= Math.max(2, Math.floor(2 + popScale * 3)) ? 1.25 + popScale * 0.22 : 1;
|
||||||
|
const score = baseInfluence * distanceFalloff * distanceFalloff * routeBoost * ethnicityFactor * coreBoost * (1 + populationAnchor) / (1 + terrainDrag * 0.16);
|
||||||
if (score <= 0.8) continue;
|
if (score <= 0.8) continue;
|
||||||
if (score > best[tile]) {
|
if (score > best[tile]) {
|
||||||
if (owner[tile] !== polity.id) second[tile] = best[tile];
|
if (owner[tile] !== polity.id) second[tile] = best[tile];
|
||||||
|
|
@ -2197,7 +2237,7 @@ for (let i = 0; i < w.count; i++) {
|
||||||
if (owner[i] < 0 || best[i] < threshold) continue;
|
if (owner[i] < 0 || best[i] < threshold) continue;
|
||||||
w.polity[i] = owner[i];
|
w.polity[i] = owner[i];
|
||||||
w.claim[i] = clamp(best[i] / 22, 0, 1);
|
w.claim[i] = clamp(best[i] / 22, 0, 1);
|
||||||
w.control[i] = clamp((best[i] - threshold) / 18, 0.12, 1);
|
w.control[i] = clamp((best[i] - threshold) / 15, 0.12, 1);
|
||||||
if (second[i] > best[i] * 0.72) w.contested[i] = 1;
|
if (second[i] > best[i] * 0.72) w.contested[i] = 1;
|
||||||
}
|
}
|
||||||
this.applyStoredTerritorialClaims();
|
this.applyStoredTerritorialClaims();
|
||||||
|
|
@ -2265,14 +2305,15 @@ return stats;
|
||||||
campaignPressureOnTerritories() {
|
campaignPressureOnTerritories() {
|
||||||
const w = this.world;
|
const w = this.world;
|
||||||
const cfg = SimConfig.campaign || {};
|
const cfg = SimConfig.campaign || {};
|
||||||
const radius = cfg.claimRadius ?? 8;
|
|
||||||
for (const campaign of this.campaigns || []) {
|
for (const campaign of this.campaigns || []) {
|
||||||
if (campaign.status !== "active" || campaign.targetTile == null) continue;
|
if (campaign.status !== "active" || campaign.targetTile == null) continue;
|
||||||
const attacker = this.getPolityById(campaign.attackerPolityId);
|
const attacker = this.getPolityById(campaign.attackerPolityId);
|
||||||
if (!attacker) continue;
|
if (!attacker) continue;
|
||||||
|
const radius = campaign.type === "border_war" ? Math.max(5, Math.floor((cfg.claimRadius ?? 8) * 1.25)) : cfg.claimRadius ?? 8;
|
||||||
|
if (!campaign.affectedTiles?.length) campaign.affectedTiles = this.campaignAffectedTiles(campaign.targetTile, radius);
|
||||||
const cx = campaign.targetTile % w.size;
|
const cx = campaign.targetTile % w.size;
|
||||||
const cy = Math.floor(campaign.targetTile / w.size);
|
const cy = Math.floor(campaign.targetTile / w.size);
|
||||||
const pressure = clamp(0.10 + campaign.progress * 0.28, 0.08, 0.42);
|
const pressure = clamp(0.10 + campaign.progress * (campaign.type === "border_war" ? 0.38 : 0.28), 0.08, campaign.type === "border_war" ? 0.52 : 0.42);
|
||||||
for (let dy = -radius; dy <= radius; dy++) {
|
for (let dy = -radius; dy <= radius; dy++) {
|
||||||
for (let dx = -radius; dx <= radius; dx++) {
|
for (let dx = -radius; dx <= radius; dx++) {
|
||||||
const distance = Math.abs(dx) + Math.abs(dy);
|
const distance = Math.abs(dx) + Math.abs(dy);
|
||||||
|
|
@ -2295,6 +2336,25 @@ w.contested[tile] = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
campaignAffectedTiles(centerTile, radius) {
|
||||||
|
const w = this.world;
|
||||||
|
const tiles = [];
|
||||||
|
if (centerTile == null) return tiles;
|
||||||
|
const cx = centerTile % w.size;
|
||||||
|
const cy = Math.floor(centerTile / w.size);
|
||||||
|
for (let dy = -radius; dy <= radius; dy++) {
|
||||||
|
for (let dx = -radius; dx <= radius; dx++) {
|
||||||
|
const distance = Math.abs(dx) + Math.abs(dy);
|
||||||
|
if (distance > radius) continue;
|
||||||
|
const x = cx + dx;
|
||||||
|
const y = cy + dy;
|
||||||
|
if (x < 0 || y < 0 || x >= w.size || y >= w.size) continue;
|
||||||
|
const tile = w.idx(x, y);
|
||||||
|
if (w.terrain[tile] !== Terrain.WATER) tiles.push(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
getCityById(id) {
|
getCityById(id) {
|
||||||
const cached = this.cityById?.get(id);
|
const cached = this.cityById?.get(id);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
@ -2325,6 +2385,14 @@ dominantCityEthnicity(city) {
|
||||||
if (!city || !city.ethnicityComposition || !city.ethnicityComposition.size) return null;
|
if (!city || !city.ethnicityComposition || !city.ethnicityComposition.size) return null;
|
||||||
return dominantComposition(city.ethnicityComposition) || null;
|
return dominantComposition(city.ethnicityComposition) || null;
|
||||||
}
|
}
|
||||||
|
dominantTileEthnicity(tile) {
|
||||||
|
const mix = this.tileEthnicMix?.get(tile);
|
||||||
|
if (mix?.size) {
|
||||||
|
const dominant = dominantComposition(mix);
|
||||||
|
if (dominant != null) return dominant;
|
||||||
|
}
|
||||||
|
return this.world.dominantEthnicity?.[tile] ?? -1;
|
||||||
|
}
|
||||||
sameDominantEthnicity(cityA, cityB) {
|
sameDominantEthnicity(cityA, cityB) {
|
||||||
const a = this.dominantCityEthnicity(cityA);
|
const a = this.dominantCityEthnicity(cityA);
|
||||||
const b = this.dominantCityEthnicity(cityB);
|
const b = this.dominantCityEthnicity(cityB);
|
||||||
|
|
@ -3101,7 +3169,7 @@ return this.sampleTileCampaignTargets(polity, (tile, connection) => {
|
||||||
const w = this.world;
|
const w = this.world;
|
||||||
if (w.polity[tile] >= 0 || w.city[tile] >= 0 || w.terrain[tile] === Terrain.WATER) return null;
|
if (w.polity[tile] >= 0 || w.city[tile] >= 0 || w.terrain[tile] === Terrain.WATER) return null;
|
||||||
const population = w.population?.[tile] || w.pressure[tile] || 0;
|
const population = w.population?.[tile] || w.pressure[tile] || 0;
|
||||||
const ethnicity = w.dominantEthnicity[tile];
|
const ethnicity = this.dominantTileEthnicity(tile);
|
||||||
if (population < 2 && ethnicity < 0) return null;
|
if (population < 2 && ethnicity < 0) return null;
|
||||||
const diversity = w.cultureDiversity[tile] || 0;
|
const diversity = w.cultureDiversity[tile] || 0;
|
||||||
const sourceEthnicity = this.dominantCityEthnicity(connection.source);
|
const sourceEthnicity = this.dominantCityEthnicity(connection.source);
|
||||||
|
|
@ -3141,20 +3209,21 @@ const targets = [];
|
||||||
for (const other of this.polities) {
|
for (const other of this.polities) {
|
||||||
if (other.id === polity.id || !this.canBeWarTarget(other.id)) continue;
|
if (other.id === polity.id || !this.canBeWarTarget(other.id)) continue;
|
||||||
const distance = this.polityDistance(polity, other);
|
const distance = this.polityDistance(polity, other);
|
||||||
if (!Number.isFinite(distance) || distance > (cfg.maxTargetDistance ?? 48)) continue;
|
if (!Number.isFinite(distance) || distance > (cfg.maxTargetDistance ?? 48) * 1.25) continue;
|
||||||
const border = this.nearestBorderCampaignTile(polity, other);
|
const border = this.nearestBorderCampaignTile(polity, other);
|
||||||
const city = this.borderWarTargetCity(polity, other);
|
const city = this.borderWarTargetCity(polity, other);
|
||||||
const defenderPower = Math.max(1, this.polityPower(other));
|
const defenderPower = Math.max(1, this.polityPower(other));
|
||||||
const targetCityValue = city ? Math.sqrt(city.population || 1) * 0.18 + (city.tradeValue || 0) * 0.5 : 0.25;
|
const targetCityValue = city ? Math.sqrt(city.population || 1) * 0.32 + (city.tradeValue || 0) * 0.85 + (city.id === other.centerCityId ? 2.2 : 0) : 0.25;
|
||||||
const contested = border.tile != null && this.world.contested[border.tile] ? 0.75 : 0;
|
const contested = border.tile != null && this.world.contested[border.tile] ? 0.75 : 0;
|
||||||
const borderValue = this.borderFriction(polity, other, distance) + contested + targetCityValue;
|
const borderValue = this.borderFriction(polity, other, distance) + contested + targetCityValue;
|
||||||
|
const defenderWeakness = clamp(1 - (other.cohesion ?? 0.6), 0, 0.8) + clamp((other.crisis || 0) * 0.25, 0, 0.5);
|
||||||
const riskAppetite = this.rng.next() < 0.10 ? this.rng.range(0.4, 1.1) : 0;
|
const riskAppetite = this.rng.next() < 0.10 ? this.rng.range(0.4, 1.1) : 0;
|
||||||
const score = borderValue - defenderPower / 90 - distance * 0.018 + this.polityPower(polity) / 120 + riskAppetite;
|
const score = borderValue + defenderWeakness - defenderPower / 110 - distance * 0.014 + this.polityPower(polity) / 120 + riskAppetite;
|
||||||
targets.push({
|
targets.push({
|
||||||
targetTile: city ? this.world.idx(city.x, city.y) : border.tile,
|
targetTile: city ? this.world.idx(city.x, city.y) : border.tile,
|
||||||
targetCityId: city?.id ?? null,
|
targetCityId: city?.id ?? null,
|
||||||
defenderPolityId: other.id,
|
defenderPolityId: other.id,
|
||||||
weight: this.noisyTargetWeight(score)
|
weight: this.noisyTargetWeight(score) * (city ? 2.35 : 0.75)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return targets;
|
return targets;
|
||||||
|
|
@ -3215,14 +3284,42 @@ if (connection.distance < best.distance) best = { tile: i, distance: connection.
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
borderWarTargetCity(attacker, defender) {
|
borderWarTargetCity(attacker, defender) {
|
||||||
const maxDistance = SimConfig.campaign?.maxTargetDistance ?? 48;
|
const maxDistance = (SimConfig.campaign?.maxTargetDistance ?? 48) * 1.25;
|
||||||
let best = null;
|
let best = null;
|
||||||
let bestDistance = Infinity;
|
let bestScore = -Infinity;
|
||||||
for (const city of this.getPolityCities(defender)) {
|
for (const city of this.getPolityCities(defender)) {
|
||||||
const connection = this.polityConnectionToCity(attacker, city);
|
const connection = this.polityConnectionToCity(attacker, city);
|
||||||
if (connection.connected && connection.distance < bestDistance && connection.distance <= maxDistance) {
|
const direct = connection.source ? connection.distance : this.nearestPolityTileConnection(attacker, this.world.idx(city.x, city.y)).distance;
|
||||||
|
const routeFactor = connection.source && this.hasDirectTradeConnection(connection.source, city) ? 0.62 : 1;
|
||||||
|
const effectiveDistance = direct * routeFactor;
|
||||||
|
if (!Number.isFinite(effectiveDistance) || effectiveDistance > maxDistance) continue;
|
||||||
|
const contested = this.world.contested[this.world.idx(city.x, city.y)] ? 0.8 : 0;
|
||||||
|
const value = Math.sqrt(city.population || 1) * 0.28 + (city.tradeValue || 0) * 0.9 + (city.id === defender.centerCityId ? 2.4 : 0) + contested - effectiveDistance * 0.045;
|
||||||
|
if (value > bestScore) {
|
||||||
best = city;
|
best = city;
|
||||||
bestDistance = connection.distance;
|
bestScore = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
nearestEnemyCityNearTile(attacker, defender, tile, radius) {
|
||||||
|
const w = this.world;
|
||||||
|
const x = tile % w.size;
|
||||||
|
const y = Math.floor(tile / w.size);
|
||||||
|
let best = null;
|
||||||
|
let bestScore = -Infinity;
|
||||||
|
for (const city of this.getPolityCities(defender)) {
|
||||||
|
const distance = Math.abs(city.x - x) + Math.abs(city.y - y);
|
||||||
|
if (distance > radius) continue;
|
||||||
|
const connection = this.polityConnectionToCity(attacker, city);
|
||||||
|
const routeFactor = connection.connected && connection.source && this.hasDirectTradeConnection(connection.source, city) ? 0.70 : 1;
|
||||||
|
const effectiveDistance = Math.min(distance, connection.distance * routeFactor);
|
||||||
|
const cityTile = w.idx(city.x, city.y);
|
||||||
|
const value = Math.sqrt(city.population || 1) * 0.30 + (city.tradeValue || 0) * 0.75 + (city.id === defender.centerCityId ? 2.0 : 0) + (w.contested[cityTile] ? 0.85 : 0);
|
||||||
|
const score = value - effectiveDistance * 0.055;
|
||||||
|
if (score > bestScore) {
|
||||||
|
best = city;
|
||||||
|
bestScore = score;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return best;
|
return best;
|
||||||
|
|
@ -3245,8 +3342,17 @@ expires: this.year + duration,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
strength: 0,
|
strength: 0,
|
||||||
resistance: 0,
|
resistance: 0,
|
||||||
status: "active"
|
status: "active",
|
||||||
|
affectedTiles: [],
|
||||||
|
frontLineTiles: []
|
||||||
};
|
};
|
||||||
|
const visualRadius = campaign.type === "border_war"
|
||||||
|
? Math.max(5, Math.floor((cfg.claimRadius ?? 8) * 1.25))
|
||||||
|
: Math.max(3, Math.floor((cfg.claimRadius ?? 8) * 0.75));
|
||||||
|
campaign.affectedTiles = this.campaignAffectedTiles(campaign.targetTile, visualRadius);
|
||||||
|
campaign.frontLineTiles = campaign.type === "border_war"
|
||||||
|
? campaign.affectedTiles.filter(tile => this.isBorderTile(tile) || this.world.contested[tile])
|
||||||
|
: [];
|
||||||
this.campaigns.push(campaign);
|
this.campaigns.push(campaign);
|
||||||
polity.lastCampaignYear = this.year;
|
polity.lastCampaignYear = this.year;
|
||||||
polity.nextCampaignAllowedYear = this.year + years(this.rng.range(cfg.minCooldownYears ?? 12, cfg.maxCooldownYears ?? 45));
|
polity.nextCampaignAllowedYear = this.year + years(this.rng.range(cfg.minCooldownYears ?? 12, cfg.maxCooldownYears ?? 45));
|
||||||
|
|
@ -3311,13 +3417,13 @@ const defender = campaign.defenderPolityId != null ? this.getPolityById(campaign
|
||||||
const connection = this.nearestPolityTileConnection(attacker, tile);
|
const connection = this.nearestPolityTileConnection(attacker, tile);
|
||||||
const terrain = w.move[tile] * (w.terrain[tile] === Terrain.MOUNTAIN ? 1.45 : 1);
|
const terrain = w.move[tile] * (w.terrain[tile] === Terrain.MOUNTAIN ? 1.45 : 1);
|
||||||
const population = targetCity?.population || w.population[tile] || w.pressure[tile] || 0;
|
const population = targetCity?.population || w.population[tile] || w.pressure[tile] || 0;
|
||||||
const defenderPower = defender ? this.polityPower(defender) * 0.55 : 0;
|
const defenderPower = defender ? this.polityPower(defender) * (campaign.type === "border_war" && targetCity ? 0.42 : 0.55) : 0;
|
||||||
const attackerEthnicity = this.dominantCityEthnicity(connection.source);
|
const attackerEthnicity = this.dominantCityEthnicity(connection.source);
|
||||||
const localEthnicity = targetCity ? this.dominantCityEthnicity(targetCity) : w.dominantEthnicity[tile];
|
const localEthnicity = targetCity ? this.dominantCityEthnicity(targetCity) : this.dominantTileEthnicity(tile);
|
||||||
const mismatch = attackerEthnicity !== null && localEthnicity !== null && localEthnicity >= 0 && attackerEthnicity !== localEthnicity ? 1.22 : 0.92;
|
const mismatch = attackerEthnicity !== null && localEthnicity !== null && localEthnicity >= 0 && attackerEthnicity !== localEthnicity ? 1.22 : 0.92;
|
||||||
const foreignControl = w.polity[tile] >= 0 && w.polity[tile] !== attacker.id ? 1.35 : 1;
|
const foreignControl = w.polity[tile] >= 0 && w.polity[tile] !== attacker.id ? 1.35 : 1;
|
||||||
return Math.max(0.2,
|
return Math.max(0.2,
|
||||||
(Math.sqrt(Math.max(1, population)) * 3.2 + terrain * 8 + connection.distance * 0.32 + defenderPower) *
|
(Math.sqrt(Math.max(1, population)) * (campaign.type === "border_war" && targetCity ? 2.75 : 3.2) + terrain * 8 + connection.distance * 0.32 + defenderPower) *
|
||||||
mismatch *
|
mismatch *
|
||||||
foreignControl *
|
foreignControl *
|
||||||
(1 + (w.cultureDiversity[tile] || 0) * 0.35)
|
(1 + (w.cultureDiversity[tile] || 0) * 0.35)
|
||||||
|
|
@ -3338,15 +3444,35 @@ this.addPolityEvent(attacker.id, "annexation", this.year, this.campaignEventData
|
||||||
} else if (campaign.type === "border_war") {
|
} else if (campaign.type === "border_war") {
|
||||||
const defender = this.getPolityById(campaign.defenderPolityId);
|
const defender = this.getPolityById(campaign.defenderPolityId);
|
||||||
const city = this.getCityById(campaign.targetCityId);
|
const city = this.getCityById(campaign.targetCityId);
|
||||||
|
let capturedCity = false;
|
||||||
if (city && defender && city.polityId === defender.id) {
|
if (city && defender && city.polityId === defender.id) {
|
||||||
|
capturedCity = true;
|
||||||
this.removeCityFromPolity(city);
|
this.removeCityFromPolity(city);
|
||||||
this.addCityToPolity(city, attacker, 0.22);
|
this.addCityToPolity(city, attacker, 0.24);
|
||||||
|
const capturedTile = this.world.idx(city.x, city.y);
|
||||||
|
this.applyCampaignTerritory(attacker, capturedTile, Math.max(8, (SimConfig.campaign?.claimRadius ?? 8) + 4), 0.68);
|
||||||
|
for (const nearby of this.getCitiesNear(city.x, city.y, 14)) {
|
||||||
|
if (nearby.polityId === defender.id) nearby.loyalty = clamp((nearby.loyalty ?? 0.5) - 0.09, 0, 1);
|
||||||
|
}
|
||||||
|
defender.legitimacy = clamp((defender.legitimacy ?? 0.7) - 0.045, 0, 1);
|
||||||
|
this.addPolityEvent(attacker.id, "annexation", this.year, this.campaignEventData(campaign, "capturedCity"), 3);
|
||||||
} else {
|
} else {
|
||||||
this.applyCampaignTerritory(attacker, campaign.targetTile, SimConfig.campaign?.claimRadius ?? 8, 0.48);
|
this.applyCampaignTerritory(attacker, campaign.targetTile, Math.max(6, SimConfig.campaign?.claimRadius ?? 8), 0.58);
|
||||||
|
if (defender) {
|
||||||
|
const followUp = this.nearestEnemyCityNearTile(attacker, defender, campaign.targetTile, SimConfig.campaign?.maxTargetDistance ?? 48);
|
||||||
|
if (followUp && campaign.progress > 0.85 && this.rng.next() < 0.55) {
|
||||||
|
this.startCampaign(attacker, {
|
||||||
|
type: "border_war",
|
||||||
|
defenderPolityId: defender.id,
|
||||||
|
targetCityId: followUp.id,
|
||||||
|
targetTile: this.world.idx(followUp.x, followUp.y)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (defender) {
|
if (defender) {
|
||||||
defender.cohesion = clamp((defender.cohesion ?? 0.6) - 0.04, 0, 1);
|
defender.cohesion = clamp((defender.cohesion ?? 0.6) - (capturedCity ? 0.075 : 0.045), 0, 1);
|
||||||
defender.treasury = Math.max(0, (defender.treasury || 0) - campaign.strength * 0.04);
|
defender.treasury = Math.max(0, (defender.treasury || 0) - campaign.strength * (capturedCity ? 0.065 : 0.045));
|
||||||
}
|
}
|
||||||
this.addPolityEvent(attacker.id, "borderWar", this.year, this.campaignEventData(campaign, "succeeded"), 2);
|
this.addPolityEvent(attacker.id, "borderWar", this.year, this.campaignEventData(campaign, "succeeded"), 2);
|
||||||
} else if (campaign.type === "nonstate_subjugation") {
|
} else if (campaign.type === "nonstate_subjugation") {
|
||||||
|
|
@ -3388,7 +3514,7 @@ campaignEthnicSimilarity(polity, campaign) {
|
||||||
const source = this.nearestPolityTileConnection(polity, campaign.targetTile).source;
|
const source = this.nearestPolityTileConnection(polity, campaign.targetTile).source;
|
||||||
const targetCity = this.getCityById(campaign.targetCityId);
|
const targetCity = this.getCityById(campaign.targetCityId);
|
||||||
const a = this.dominantCityEthnicity(source);
|
const a = this.dominantCityEthnicity(source);
|
||||||
const b = targetCity ? this.dominantCityEthnicity(targetCity) : this.world.dominantEthnicity[campaign.targetTile];
|
const b = targetCity ? this.dominantCityEthnicity(targetCity) : this.dominantTileEthnicity(campaign.targetTile);
|
||||||
if (a === null || b === null || b < 0) return 0.45;
|
if (a === null || b === null || b < 0) return 0.45;
|
||||||
return a === b ? 1 : 0.25;
|
return a === b ? 1 : 0.25;
|
||||||
}
|
}
|
||||||
|
|
@ -3420,7 +3546,7 @@ contested: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applySubjugationEffects(polity, campaign) {
|
applySubjugationEffects(polity, campaign) {
|
||||||
const localEthnicity = campaign.targetEthnicity ?? this.world.dominantEthnicity[campaign.targetTile];
|
const localEthnicity = campaign.targetEthnicity ?? this.dominantTileEthnicity(campaign.targetTile);
|
||||||
if (localEthnicity == null || localEthnicity < 0) return;
|
if (localEthnicity == null || localEthnicity < 0) return;
|
||||||
const sourceEthnicity = this.dominantCityEthnicity(this.nearestPolityTileConnection(polity, campaign.targetTile).source);
|
const sourceEthnicity = this.dominantCityEthnicity(this.nearestPolityTileConnection(polity, campaign.targetTile).source);
|
||||||
if (sourceEthnicity === localEthnicity) return;
|
if (sourceEthnicity === localEthnicity) return;
|
||||||
|
|
@ -3432,7 +3558,7 @@ if (city.polityId === polity.id) city.loyalty = clamp((city.loyalty ?? 0.5) - 0.
|
||||||
}
|
}
|
||||||
seedFrontierPopulation(polity, tile) {
|
seedFrontierPopulation(polity, tile) {
|
||||||
const source = this.nearestPolityTileConnection(polity, tile).source;
|
const source = this.nearestPolityTileConnection(polity, tile).source;
|
||||||
const ethnicity = this.dominantCityEthnicity(source) || this.world.dominantEthnicity[tile];
|
const ethnicity = this.dominantCityEthnicity(source) ?? this.dominantTileEthnicity(tile);
|
||||||
if (!source || ethnicity == null || ethnicity < 0) return;
|
if (!source || ethnicity == null || ethnicity < 0) return;
|
||||||
const migrants = Math.max(1, Math.floor(Math.min(source.population * 0.018, 14)));
|
const migrants = Math.max(1, Math.floor(Math.min(source.population * 0.018, 14)));
|
||||||
source.population = Math.max(1, source.population - migrants);
|
source.population = Math.max(1, source.population - migrants);
|
||||||
|
|
@ -3499,7 +3625,7 @@ if (share > 0) composition.set(id, Math.max(1, Math.round(basePopulation * share
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!composition.size) {
|
if (!composition.size) {
|
||||||
const regionalEthnicity = this.world.dominantEthnicity[targetTile];
|
const regionalEthnicity = this.dominantTileEthnicity(targetTile);
|
||||||
const sourceEthnicity = this.dominantCityEthnicity(source) || (regionalEthnicity >= 0 ? regionalEthnicity : 1);
|
const sourceEthnicity = this.dominantCityEthnicity(source) || (regionalEthnicity >= 0 ? regionalEthnicity : 1);
|
||||||
composition.set(sourceEthnicity, isFrontier ? 12 : 18);
|
composition.set(sourceEthnicity, isFrontier ? 12 : 18);
|
||||||
}
|
}
|
||||||
|
|
@ -3536,6 +3662,7 @@ this.cityBuckets.get(bucketKey).push(city);
|
||||||
this.addCityToPolity(city, polity, city.loyalty);
|
this.addCityToPolity(city, polity, city.loyalty);
|
||||||
w.city[targetTile] = city.id;
|
w.city[targetTile] = city.id;
|
||||||
this.removeEthnicPopulationProportionally(targetTile, Math.min(seedPopulation, (w.population[targetTile] || 0) * 0.65));
|
this.removeEthnicPopulationProportionally(targetTile, Math.min(seedPopulation, (w.population[targetTile] || 0) * 0.65));
|
||||||
|
this.assignNewCityToTerritoryOwner(city, targetTile);
|
||||||
this.projectCityPopulationToTiles(city);
|
this.projectCityPopulationToTiles(city);
|
||||||
this.syncTilePopulationCulture();
|
this.syncTilePopulationCulture();
|
||||||
return city;
|
return city;
|
||||||
|
|
|
||||||
42
render.js
42
render.js
|
|
@ -36,12 +36,19 @@ const v = clamp(w.resource[i] / 30, 0, 1);
|
||||||
color = mix([28, 36, 40], [107, 188, 85], v);
|
color = mix([28, 36, 40], [107, 188, 85], v);
|
||||||
} else if (mode === "ethnicity") {
|
} else if (mode === "ethnicity") {
|
||||||
color = terrainInfo[w.terrain[i]].color;
|
color = terrainInfo[w.terrain[i]].color;
|
||||||
const dominant = w.dominantEthnicity?.[i] ?? -1;
|
const ethnicMix = sim.tileEthnicMix?.get(i);
|
||||||
const ethnicityColor = dominant >= 0 ? sim.ethnicities.get(dominant)?.color : null;
|
const population = w.population?.[i] || 0;
|
||||||
if (ethnicityColor) {
|
if (ethnicMix && ethnicMix.size && population >= 0.25) {
|
||||||
const populationStrength = clamp(Math.sqrt(Math.max(0, w.population?.[i] || w.pressure[i] || 0)) / 16, 0.18, 0.68);
|
const dominant = dominantComposition(ethnicMix);
|
||||||
const diversityDimming = clamp(w.cultureDiversity?.[i] || 0, 0, 1) * 0.18;
|
const ethnicityColor = dominant != null ? sim.ethnicities.get(dominant)?.color : null;
|
||||||
color = mix(color, ethnicityColor, clamp(populationStrength - diversityDimming, 0.12, 0.64));
|
const total = compositionTotal(ethnicMix);
|
||||||
|
if (ethnicityColor && Number.isFinite(total) && total > 0) {
|
||||||
|
const dominantShare = (ethnicMix.get(dominant) || 0) / total;
|
||||||
|
const popStrength = clamp(Math.log1p(population) / Math.log(80), 0.05, 0.72);
|
||||||
|
const purity = clamp(dominantShare, 0.25, 1);
|
||||||
|
const strength = clamp(popStrength * (0.45 + purity * 0.55), 0.04, 0.72);
|
||||||
|
color = mix(color, ethnicityColor, strength);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (w.city[i] >= 0) {
|
if (w.city[i] >= 0) {
|
||||||
const city = sim.getCityById(w.city[i]);
|
const city = sim.getCityById(w.city[i]);
|
||||||
|
|
@ -175,10 +182,23 @@ const y = Math.floor(campaign.targetTile / w.size);
|
||||||
const age = Math.max(0, sim.year - campaign.started) / Math.max(1, campaign.expires - campaign.started);
|
const age = Math.max(0, sim.year - campaign.started) / Math.max(1, campaign.expires - campaign.started);
|
||||||
const pulse = 0.5 + Math.sin(age * Math.PI * 8) * 0.5;
|
const pulse = 0.5 + Math.sin(age * Math.PI * 8) * 0.5;
|
||||||
const radius = 2.5 + clamp(campaign.progress, 0, 1) * 4 + pulse * 1.5;
|
const radius = 2.5 + clamp(campaign.progress, 0, 1) * 4 + pulse * 1.5;
|
||||||
|
if (campaign.type === "border_war" && campaign.affectedTiles?.length) {
|
||||||
|
ctx.globalAlpha = 0.10 + clamp(campaign.progress, 0, 1) * 0.10;
|
||||||
|
ctx.fillStyle = "rgba(164, 76, 58, 0.9)";
|
||||||
|
for (const tile of campaign.affectedTiles) {
|
||||||
|
ctx.fillRect(tile % w.size, Math.floor(tile / w.size), 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
ctx.globalAlpha = 0.55;
|
ctx.globalAlpha = 0.55;
|
||||||
ctx.strokeStyle = campaign.type === "border_war" ? "rgba(236, 126, 111, 0.95)" : "rgba(239, 211, 119, 0.95)";
|
ctx.strokeStyle = campaign.type === "border_war" ? "rgba(236, 126, 111, 0.95)" : "rgba(239, 211, 119, 0.95)";
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
strokeCircle(ctx, x + 0.5, y + 0.5, radius);
|
strokeCircle(ctx, x + 0.5, y + 0.5, campaign.type === "border_war" ? radius + 2.5 : radius);
|
||||||
|
if (campaign.type === "border_war" && campaign.targetCityId != null) {
|
||||||
|
ctx.globalAlpha = 0.82;
|
||||||
|
ctx.lineWidth = 1.4;
|
||||||
|
ctx.strokeStyle = "rgba(255, 195, 121, 0.95)";
|
||||||
|
strokeCircle(ctx, x + 0.5, y + 0.5, radius + 5 + pulse * 2);
|
||||||
|
}
|
||||||
ctx.globalAlpha = 0.22;
|
ctx.globalAlpha = 0.22;
|
||||||
ctx.fillStyle = campaign.type === "nonstate_subjugation" ? "rgba(180, 111, 73, 0.9)" : "rgba(239, 211, 119, 0.9)";
|
ctx.fillStyle = campaign.type === "nonstate_subjugation" ? "rgba(180, 111, 73, 0.9)" : "rgba(239, 211, 119, 0.9)";
|
||||||
fillSquare(ctx, x, y, 1);
|
fillSquare(ctx, x, y, 1);
|
||||||
|
|
@ -319,7 +339,9 @@ const mismatch = agent ? sim.climateMismatch(agent.ethnicity, i) : 0;
|
||||||
const polity = cityPolity(city);
|
const polity = cityPolity(city);
|
||||||
const territoryId = w.polity?.[i] ?? -1;
|
const territoryId = w.polity?.[i] ?? -1;
|
||||||
const territoryPolity = territoryId >= 0 ? sim.getPolityById(territoryId) : null;
|
const territoryPolity = territoryId >= 0 ? sim.getPolityById(territoryId) : null;
|
||||||
const regionalEthnicity = w.dominantEthnicity[i] >= 0 ? `E${w.dominantEthnicity[i]}` : "-";
|
const ethnicMix = sim.tileEthnicMix?.get(i);
|
||||||
|
const tileDominantEthnicity = ethnicMix?.size ? dominantComposition(ethnicMix) : w.dominantEthnicity[i];
|
||||||
|
const regionalEthnicity = tileDominantEthnicity != null && tileDominantEthnicity >= 0 ? `E${tileDominantEthnicity}` : "-";
|
||||||
const tilePopulation = w.population?.[i] || 0;
|
const tilePopulation = w.population?.[i] || 0;
|
||||||
const nearbyCampaigns = activeCampaignsNear(i);
|
const nearbyCampaigns = activeCampaignsNear(i);
|
||||||
els.tooltip.innerHTML = `
|
els.tooltip.innerHTML = `
|
||||||
|
|
@ -430,7 +452,9 @@ for (const campaign of sim.campaigns) {
|
||||||
if (campaign.status !== "active" || campaign.targetTile == null) continue;
|
if (campaign.status !== "active" || campaign.targetTile == null) continue;
|
||||||
const cx = campaign.targetTile % w.size;
|
const cx = campaign.targetTile % w.size;
|
||||||
const cy = Math.floor(campaign.targetTile / w.size);
|
const cy = Math.floor(campaign.targetTile / w.size);
|
||||||
if (Math.abs(cx - x) + Math.abs(cy - y) > 3) continue;
|
const nearTarget = Math.abs(cx - x) + Math.abs(cy - y) <= 3;
|
||||||
|
const nearFront = campaign.affectedTiles?.includes(tile);
|
||||||
|
if (!nearTarget && !nearFront) continue;
|
||||||
labels.push(`${campaign.type.replaceAll("_", " ")} ${Math.round(clamp(campaign.progress, 0, 1) * 100)}%`);
|
labels.push(`${campaign.type.replaceAll("_", " ")} ${Math.round(clamp(campaign.progress, 0, 1) * 100)}%`);
|
||||||
}
|
}
|
||||||
return labels.slice(0, 2).join("; ");
|
return labels.slice(0, 2).join("; ");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue