This commit is contained in:
33333-33333 2026-06-08 13:18:33 +09:00
commit 2c7e525c53
21 changed files with 1730 additions and 285 deletions

View file

@ -1,5 +1,7 @@
import { STARTING_CASH, TURN_SECONDS, FARM_SHUTDOWN_GRACE_SECONDS, BUILD_COSTS, FACILITY_DEFS, GRID } from './config.js';
import { randomBetween } from './utils.js';
import { STARTING_CASH, TURN_SECONDS, FARM_SHUTDOWN_GRACE_SECONDS, BUILD_COSTS, FACILITY_DEFS, GRID, EGG_SPAWN_RANGES } from './config.js';
import { BALANCE } from './balance.js';
import { createRunSeed, generateBlockedCells } from './mapGen.js';
import { randomBetween, key, inGrid } from './utils.js';
export function newTurnStats() {
return {
@ -25,10 +27,13 @@ export function newTurnStats() {
truckPoopFine: 0,
maleTruckFine: 0,
explosionDamage: 0,
shredderBonus: 0,
correct: 0,
mistakes: 0,
contractBonus: 0,
contractResult: null
contractResult: null,
conveyorPasses: 0,
eggProduced: 0
};
}
@ -47,6 +52,7 @@ export function newTotalStats() {
poopTrash: 0,
poopMixer: 0,
explosionDamage: 0,
shredderBonus: 0,
contractBonus: 0,
zundaTax: 0,
fairiesTribute: 0,
@ -55,7 +61,9 @@ export function newTotalStats() {
truckPoopFine: 0,
maleTruckFine: 0,
contractSuccess: 0,
contractFailed: 0
contractFailed: 0,
conveyorPasses: 0,
eggProduced: 0
};
}
@ -67,8 +75,13 @@ export function createGame() {
timeLeft: TURN_SECONDS,
shutdownTimeLeft: 0,
shutdownGraceSeconds: FARM_SHUTDOWN_GRACE_SECONDS,
cleanupTimer: null,
cleanupBlown: false,
lastTimestamp: 0,
nextId: 10,
nextEventId: 1,
runSeed: createRunSeed(),
blockedCells: new Set(),
buildTool: null,
buildSession: 0,
undoStack: [],
@ -80,6 +93,8 @@ export function createGame() {
pan: null,
view: { x: 0, y: 0 },
debug: false,
debugInfiniteCash: false,
hover: null,
chicks: [],
effects: [],
floatingTexts: [],
@ -91,6 +106,10 @@ export function createGame() {
scanners: [],
facilities: {},
branchCounters: new Map(),
routingVersion: 0,
factoryGraph: null,
factoryGraphCommittedVersion: -1,
factoryGraphSnapshot: null,
mixerHalfTimer: 0,
congestion: new Map(),
lastExplodedComponent: new Map(),
@ -99,15 +118,16 @@ export function createGame() {
cardEffects: { bearing: 0, legalWork: 0, flattery: 0 },
cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 },
cardTargetPick: null,
repairman: { hiredForNextDay: false, active: false, x: GRID.x + GRID.cell * 0.5, y: GRID.y + GRID.rows * GRID.cell + 84, target: null, repairedToday: 0 },
stats: newTurnStats(),
lastResult: null,
eventLog: [],
totals: newTotalStats()
};
}
export function getSpawnRange(farm) {
const ranges = [[1.95, 4.35], [1.55, 3.45], [1.25, 2.80], [1.05, 2.35]];
return ranges[(farm?.level || 1) - 1] || ranges[0];
return EGG_SPAWN_RANGES[(farm?.level || 1) - 1] || EGG_SPAWN_RANGES[EGG_SPAWN_RANGES.length - 1] || [2, 4];
}
export function nextSpawnDelay(farm) {
@ -138,34 +158,107 @@ export function defaultFacilities() {
};
}
const INITIAL_CONVEYOR_BACKBONE = Object.freeze([
// Egg Farm -> S1 top input. S1 starts lower so the farm is not pressed against it.
[4, 1], [4, 2], [4, 3], [4, 4],
// S1 left output -> Mixer receiver on the left grid edge.
[3, 5], [2, 5], [1, 5], [0, 5],
// S1 right output -> S2 top input.
[5, 5], [6, 5], [7, 5], [8, 5], [9, 5], [10, 5], [11, 5],
[11, 6], [11, 7],
// S2 left output -> Waste Shredder receiver on bottom grid edge.
[10, 8], [10, 9], [10, 10], [10, 11], [10, 12],
// S2 right output -> Truck receiver on the bottom grid edge.
[12, 8], [13, 8], [14, 8], [15, 8], [16, 8], [17, 8], [17, 9], [17, 10], [17, 11], [17, 12]
]);
const INITIAL_SCANNERS = Object.freeze([
{ type: 'scanner', id: 1, kind: 'manual', slot: 0, role: 0, col: 4, row: 5, level: 1, queue: [], cooldown: 0, price: BUILD_COSTS.manualScanner, builtSession: null, autoMode: 'standard' },
{ type: 'scanner', id: 2, kind: 'manual', slot: 1, role: 1, col: 11, row: 8, level: 1, queue: [], cooldown: 0, price: BUILD_COSTS.manualScanner, builtSession: null, autoMode: 'standard' }
]);
const INITIAL_EGG_FARMS = Object.freeze([
{ type: 'eggFarm', id: 1, col: 4, row: 0, level: 1, nextSpawn: 2, lastInterval: 2, spawnCounter: 0, price: BUILD_COSTS.eggFarm, builtSession: null }
]);
function addConveyorTile(game, col, row, options = {}) {
if (!inGrid(col, row)) return;
const k = key(col, row);
game.conveyorTiles.add(k);
if (!game.conveyorMeta.has(k) || options.forceMeta) {
game.conveyorMeta.set(k, {
price: BUILD_COSTS.conveyor,
builtSession: null,
uses: 0,
durability: BALANCE.maintenance.durability.conveyor,
maintenanceType: 'conveyor'
});
}
}
function scannerPortCells(scanner) {
return [
{ col: scanner.col, row: scanner.row - 1 },
{ col: scanner.col - 1, row: scanner.row },
{ col: scanner.col + 1, row: scanner.row }
].filter(p => inGrid(p.col, p.row));
}
function farmOutputCells(farm) {
return [
{ col: farm.col, row: farm.row + 1 },
{ col: farm.col - 1, row: farm.row },
{ col: farm.col + 1, row: farm.row },
{ col: farm.col, row: farm.row - 1 }
].filter(p => inGrid(p.col, p.row));
}
function protectedInitialCells(game) {
const reserved = new Set([...game.conveyorTiles]);
const add = p => { if (p && inGrid(p.col, p.row)) reserved.add(key(p.col, p.row)); };
for (const scanner of game.scanners) {
add(scanner);
for (const p of scannerPortCells(scanner)) add(p);
}
for (const farm of game.eggFarms) {
add(farm);
for (const p of farmOutputCells(farm)) add(p);
}
for (const f of Object.values(game.facilities)) add(f.entry);
return reserved;
}
function installInitialConveyorBackbone(game) {
for (const [col, row] of INITIAL_CONVEYOR_BACKBONE) addConveyorTile(game, col, row);
}
function scrubBlockedCellsFromInitialBackbone(game) {
const protectedCells = protectedInitialCells(game);
for (const k of protectedCells) game.blockedCells.delete(k);
}
export function resetLayout(game) {
game.conveyorTiles.clear();
game.conveyorMeta.clear();
const initial = [
// Egg Farm -> S1 top input. S1 starts lower so the farm is not pressed against it.
[4, 1], [4, 2], [4, 3], [4, 4],
// S1 left output -> Mixer receiver on the left grid edge.
[3, 5], [2, 5], [1, 5], [0, 5],
// S1 right output -> S2 top input.
[5, 5], [6, 5], [7, 5], [8, 5], [9, 5], [10, 5], [11, 5],
[11, 6], [11, 7],
// S2 left output -> Waste Shredder receiver on bottom grid edge.
[10, 8], [10, 9], [10, 10], [10, 11], [10, 12],
// S2 right output -> Truck receiver on the bottom grid edge.
[12, 8], [13, 8], [14, 8], [15, 8], [16, 8], [17, 8], [17, 9], [17, 10], [17, 11], [17, 12]
];
for (const [col, row] of initial) {
const k = `${col},${row}`;
game.conveyorTiles.add(k);
game.conveyorMeta.set(k, { price: BUILD_COSTS.conveyor, builtSession: null });
}
game.blockedCells = new Set();
installInitialConveyorBackbone(game);
game.facilities = defaultFacilities();
game.scanners = [
{ type: 'scanner', id: 1, kind: 'manual', slot: 0, role: 0, col: 4, row: 5, level: 1, queue: [], cooldown: 0, price: BUILD_COSTS.manualScanner, builtSession: null, autoMode: 'standard' },
{ type: 'scanner', id: 2, kind: 'manual', slot: 1, role: 1, col: 11, row: 8, level: 1, queue: [], cooldown: 0, price: BUILD_COSTS.manualScanner, builtSession: null, autoMode: 'standard' }
];
const farm = { type: 'eggFarm', id: 1, col: 4, row: 0, level: 1, nextSpawn: 2, lastInterval: 2, spawnCounter: 0, price: BUILD_COSTS.eggFarm, builtSession: null };
farm.nextSpawn = nextSpawnDelay(farm);
farm.lastInterval = farm.nextSpawn;
game.eggFarms = [farm];
game.scanners = INITIAL_SCANNERS.map(scanner => ({ ...scanner, queue: [] }));
game.eggFarms = INITIAL_EGG_FARMS.map(farm => {
const nextSpawn = nextSpawnDelay(farm);
return { ...farm, nextSpawn, lastInterval: nextSpawn };
});
// Generate no-build cells only after the complete initial factory footprint is
// installed. The protected footprint includes every default conveyor segment,
// scanner body/ports, farm output, and machine receiver. This guarantees that
// the starter line is always continuous from Egg Farm -> S1 -> S2 -> exits even
// when roughly 20% of the map is blocked.
const reserved = protectedInitialCells(game);
game.blockedCells = generateBlockedCells({ seed: game.runSeed, targetRatio: BALANCE.map.blockedRatio, reserved: [...reserved] });
scrubBlockedCellsFromInitialBackbone(game);
installInitialConveyorBackbone(game);
game.routingVersion = (game.routingVersion || 0) + 1;
game.factoryGraph = null;
game.factoryGraphCommittedVersion = -1;
}