420 lines
22 KiB
JavaScript
420 lines
22 KiB
JavaScript
#!/usr/bin/env node
|
||
/* progression.mjs — модель прогрессии/экономики игры (вспомогательный инструмент балансировки).
|
||
Отвечает на вопрос «за сколько минут игрок дойдёт до цели»: берёт цены и статы снастей
|
||
ПРЯМО из index.html (ничего не дублируется), моделирует улов и цикл ловли теми же
|
||
формулами, что игра, и прогоняет жадный маршрут покупок, печатая ETA каждой цели.
|
||
|
||
Что моделируется (ссылки — на index.html):
|
||
- pickSpecies() (:2946): выбор таблицы по локации и фазе (evening+night → ночная),
|
||
веса ×(1+lure.rare·rarity), штраф дальних видов ×0.06;
|
||
- rollWeight() (:2982): s=u^(2/(1+2B)), B=rod.size+lure.size — статы снасти двигают вес;
|
||
- обрыв по maxW (:3011/:3087): если вес рыбы > предела удочки, шанс улова резко падает
|
||
(это учитывает симулятор боя);
|
||
- варианты рыб (E[mult], variantBoost :2185/:2188) — буст редкой приманки;
|
||
- цикл: cast 0.65с + ожидание клёва biteChance()/таймеры (:2974/:3178) + approach 0.45с
|
||
+ бой (fishForce/updateReel :3021/:3043, бот «держать в затишье / отпускать в рывке»)
|
||
+ карточка 1.2с; пустые клёвы учтены геометрически;
|
||
- сутки: 4 равные фазы (утро/день — дневная таблица, вечер — ночная, ночь — ночная
|
||
и клёв ×0.85), день/ночь 50/50.
|
||
|
||
Запуск: node tools/progression.mjs [опции]
|
||
Опции:
|
||
--start-coins N стартовые монеты (по умолчанию 0)
|
||
--per-day MIN минут игры в день — печатает ETA ещё и в днях (по умолчанию 15)
|
||
--route "..." маршрут покупок (см. ниже); по умолчанию — целевой из плана
|
||
--samples N выборка MC на вид для оценки боя (по умолчанию 80)
|
||
--rod i:cost what-if: цена удочки i (можно несколько раз)
|
||
--lure i:cost what-if: цена приманки i
|
||
--loc i:cost what-if: цена локации i
|
||
--rod-upg a,b,c what-if: ROD_UPG_COST
|
||
--lure-upg a,b,c what-if: LURE_UPG_COST
|
||
--upg-mult a,b,c what-if: UPG_MULT
|
||
--set id:вес what-if: вес вида во ВСЕХ таблицах (день и ночь)
|
||
--set-day id:вес what-if: только дневная таблица (SPECIES_BY_LOCATION)
|
||
--set-night id:вес what-if: только ночная таблица (SPECIES_BY_LOCATION_NIGHT)
|
||
--help
|
||
|
||
Синтаксис маршрута (через запятую):
|
||
rodN — купить удочку N (нужна удочка N−1 на 4 ур.)
|
||
rodN:lvl — поднять удочку N до уровня lvl (1..4)
|
||
lureN / lureN:lvl, locN — то же для приманок и локаций.
|
||
Пример: "rod0:4,lure0:4,rod1,lure1,loc1,rod1:4,lure1:4,rod2,lure2,loc4,rod2:4,lure2:4,rod3,lure3,loc2,loc3"
|
||
|
||
Данные НЕ дублируются: два фрагмента index.html вырезаются по якорям и исполняются
|
||
в изоляции (правки цен/статов видны сразу). Переименование якоря → понятная ошибка. */
|
||
import { readFileSync } from 'node:fs';
|
||
import { dirname, join } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||
const htmlPath = join(root, 'index.html');
|
||
|
||
function fail(msg) { console.error('progression: ОШИБКА — ' + msg); process.exit(1); }
|
||
|
||
/* ---------- 1. Данные из index.html (single source of truth) ---------- */
|
||
let html;
|
||
try { html = readFileSync(htmlPath, 'utf8'); }
|
||
catch (e) { fail(`не удалось прочитать ${htmlPath}: ${e.message}`); }
|
||
|
||
// блок 1: SPECIES … VARIANTS (тот же фрагмент, что в balance.mjs)
|
||
const a1 = html.indexOf('const SPECIES=[');
|
||
if (a1 < 0) fail('в index.html не найден якорь «const SPECIES=[»');
|
||
const vStart = html.indexOf('const VARIANTS=[', a1);
|
||
if (vStart < 0) fail('в index.html не найден якорь «const VARIANTS=[»');
|
||
const b1 = html.indexOf('\n];', vStart);
|
||
if (b1 < 0) fail('не найдено закрытие «];» таблицы VARIANTS');
|
||
const D = new Function(html.slice(a1, b1 + 3) +
|
||
'\n;return {SPECIES, SPECIES_NIGHT, RIVER_SPECIES, RIVER_SPECIES_NIGHT,' +
|
||
'LAKE_SPECIES, LAKE_SPECIES_NIGHT, SEA_SPECIES, SEA_SPECIES_NIGHT,' +
|
||
'OCEAN_SPECIES, OCEAN_SPECIES_NIGHT, SWAMP_SPECIES, SWAMP_SPECIES_NIGHT,' +
|
||
'SPECIES_BY_LOCATION, SPECIES_BY_LOCATION_NIGHT, LOCATIONS, VARIANTS, SHINY_CHANCE};')();
|
||
|
||
// блок 2: RODS … UPG_MULT
|
||
const r1 = html.indexOf('const RODS=[');
|
||
if (r1 < 0) fail('в index.html не найден якорь «const RODS=[»');
|
||
const rEnd = html.indexOf('const ROD_UPG_NEXT=', r1);
|
||
if (rEnd < 0) fail('в index.html не найден якорь «const ROD_UPG_NEXT=»');
|
||
const R = new Function(html.slice(r1, rEnd) +
|
||
'\n;return {RODS, LURES, ROD_UPG_COST, LURE_UPG_COST, UPG_MULT};')();
|
||
|
||
const SPECIES = D.SPECIES;
|
||
|
||
/* ---------- 2. Опции CLI ---------- */
|
||
const argv = process.argv.slice(2);
|
||
let startCoins = 0, perDay = 15, samples = 80;
|
||
const rodCost = {}, lureCost = {}, locCost = {}, sets = {}, setsDay = {}, setsNight = {};
|
||
let rodUpg = null, lureUpg = null, upgMult = null;
|
||
let route = null;
|
||
|
||
const DEFAULT_ROUTE =
|
||
'rod0:4,lure0:4,rod1,lure1,rod1:4,lure1:4,rod2,lure2,loc1,loc4,rod2:4,lure2:4,rod3,lure3,loc2,loc3';
|
||
|
||
for (let i = 0; i < argv.length; i++) {
|
||
const arg = argv[i];
|
||
if (arg === '--help' || arg === '-h') { printHelp(); process.exit(0); }
|
||
else if (arg === '--start-coins') { startCoins = Number(argv[++i]); if (!(startCoins >= 0)) fail('--start-coins ожидает число ≥ 0'); }
|
||
else if (arg === '--per-day') { perDay = Number(argv[++i]); if (!(perDay > 0)) fail('--per-day ожидает число > 0'); }
|
||
else if (arg === '--samples') { samples = Number(argv[++i]); if (!(samples > 0)) fail('--samples ожидает число > 0'); }
|
||
else if (arg === '--route') { route = String(argv[++i] || ''); }
|
||
else if (arg === '--rod') parseCost(rodCost, argv[++i], '--rod');
|
||
else if (arg === '--lure') parseCost(lureCost, argv[++i], '--lure');
|
||
else if (arg === '--loc') parseCost(locCost, argv[++i], '--loc');
|
||
else if (arg === '--rod-upg') rodUpg = parseList(argv[++i], '--rod-upg');
|
||
else if (arg === '--lure-upg') lureUpg = parseList(argv[++i], '--lure-upg');
|
||
else if (arg === '--upg-mult') upgMult = parseList(argv[++i], '--upg-mult');
|
||
else if (arg === '--set' || arg === '--set-day' || arg === '--set-night') {
|
||
const m = String(argv[++i] || '').split(':');
|
||
if (m.length !== 2 || !m[0].trim() || !(Number(m[1]) >= 0)) fail(`${arg} ожидает «id:вес»`);
|
||
(arg === '--set' ? sets : arg === '--set-day' ? setsDay : setsNight)[m[0].trim()] = Number(m[1]);
|
||
}
|
||
else fail(`неизвестный аргумент «${arg}» (см. node tools/progression.mjs --help)`);
|
||
}
|
||
if (!route) route = DEFAULT_ROUTE;
|
||
|
||
// what-if весов: применяем поверх таблиц локаций ДО расчётов
|
||
function applySets(map, tables, opt) {
|
||
for (const id of Object.keys(map)) {
|
||
let found = false;
|
||
for (const tbl of tables) for (const arr of tbl) for (const s of arr)
|
||
if (s.id === id) { s.weight = map[id]; found = true; }
|
||
if (!found) fail(`${opt}: вид «${id}» не найден в таблицах локаций`);
|
||
}
|
||
}
|
||
applySets(sets, [D.SPECIES_BY_LOCATION, D.SPECIES_BY_LOCATION_NIGHT], '--set');
|
||
applySets(setsDay, [D.SPECIES_BY_LOCATION], '--set-day');
|
||
applySets(setsNight, [D.SPECIES_BY_LOCATION_NIGHT], '--set-night');
|
||
|
||
function parseCost(obj, s, opt) {
|
||
const m = String(s || '').split(':');
|
||
if (m.length !== 2 || !m[0].trim() || !(Number(m[1]) >= 0)) fail(`${opt} ожидает «i:цена»`);
|
||
obj[Number(m[0])] = Number(m[1]);
|
||
}
|
||
function parseList(s, opt) {
|
||
const arr = String(s || '').split(',').map(Number);
|
||
if (arr.length !== 3 || arr.some(x => !(x >= 0))) fail(`${opt} ожидает три числа через запятую`);
|
||
return arr;
|
||
}
|
||
function printHelp() {
|
||
console.log(`progression.mjs — модель прогрессии: ETA целей по ценам снастей и локаций.
|
||
Данные (SPECIES/VARIANTS/RODS/LURES/LOCATIONS/UPG) берутся из index.html.
|
||
|
||
Опции:
|
||
--start-coins N стартовые монеты (по умолчанию 0)
|
||
--per-day MIN минут в день для перевода ETA в дни (по умолчанию 15)
|
||
--route "..." маршрут покупок (rodN, rodN:lvl, lureN, lureN:lvl, locN)
|
||
--samples N выборка Monte-Carlo на вид (по умолчанию 80)
|
||
--rod i:cost what-if цена удочки i (не сохраняется)
|
||
--lure i:cost what-if цена приманки i
|
||
--loc i:cost what-if цена локации i
|
||
--rod-upg a,b,c what-if ROD_UPG_COST
|
||
--lure-upg a,b,c what-if LURE_UPG_COST
|
||
--upg-mult a,b,c what-if UPG_MULT
|
||
--help
|
||
|
||
Маршрут по умолчанию:
|
||
${DEFAULT_ROUTE}`);
|
||
}
|
||
|
||
/* ---------- 3. Статы и цены (порт из index.html) ---------- */
|
||
const RODS = R.RODS.map((r, i) => Object.assign({}, r, rodCost[i] !== undefined ? { cost: rodCost[i] } : {}));
|
||
const LURES = R.LURES.map((l, i) => Object.assign({}, l, lureCost[i] !== undefined ? { cost: lureCost[i] } : {}));
|
||
const LOCATIONS = D.LOCATIONS.map((l, i) => Object.assign({}, l, locCost[i] !== undefined ? { cost: locCost[i] } : {}));
|
||
const ROD_UPG_COST = rodUpg || R.ROD_UPG_COST.slice();
|
||
const LURE_UPG_COST = lureUpg || R.LURE_UPG_COST.slice();
|
||
const UPG_MULT = upgMult || R.UPG_MULT.slice();
|
||
|
||
function effRod(i, lv) {
|
||
const e = Object.assign({}, RODS[i]);
|
||
if (lv >= 2) e.line *= 1.15;
|
||
if (lv >= 3) { e.drag *= 1.15; e.reel *= 1.10; }
|
||
if (lv >= 4) { e.line *= 1.15; e.size += 0.10; }
|
||
return e;
|
||
}
|
||
function effLure(i, lv) {
|
||
const e = Object.assign({}, LURES[i]);
|
||
if (lv >= 2) e.bite *= 1.15;
|
||
if (lv >= 3) e.rare += 1.0;
|
||
if (lv >= 4) e.size += 0.15;
|
||
return e;
|
||
}
|
||
function upgCost(type, i, lv) {
|
||
if (i === 0) return (type === 'rod' ? ROD_UPG_COST : LURE_UPG_COST)[lv - 1];
|
||
const base = (type === 'rod' ? RODS : LURES)[i].cost;
|
||
return Math.round(base * UPG_MULT[lv - 1] / 10) * 10;
|
||
}
|
||
function variantE(boost) {
|
||
let survive = 1, e = 0;
|
||
for (const v of D.VARIANTS) {
|
||
if (v.id === 'normal') continue;
|
||
let c = v.id === 'gold' ? D.SHINY_CHANCE : v.chance;
|
||
c *= 1 + boost * D.VARIANTS.indexOf(v);
|
||
c = Math.min(1, c);
|
||
e += survive * c * v.mult;
|
||
survive *= 1 - c;
|
||
}
|
||
return e + survive;
|
||
}
|
||
|
||
/* ---------- 4. Симулятор боя (порт updateReel/fishForce) ---------- */
|
||
const DT = 1 / 60;
|
||
function fight(w, str, rod, maxT) {
|
||
const d0 = 45 + 7 * w;
|
||
const ov = w / rod.maxW;
|
||
const aggr = Math.min(1, 0.35 * str + 0.75 * Math.min(1.25, ov) / 1.25);
|
||
let d = d0, tension = 0, escape = 0;
|
||
let st = 0.65 + 0.35 * Math.min(1, w / (rod.maxW * 1.5)); // приближение st0 (без wMax вида)
|
||
let phase = 'calm', phaseT = 0.8 + Math.random() * 0.7, t = 0;
|
||
while (t < maxT) {
|
||
t += DT; phaseT -= DT;
|
||
if (phaseT <= 0) {
|
||
if (phase === 'calm') { phase = 'pull'; phaseT = (0.8 + 2.2 * st) * (0.6 + 0.7 * aggr) * (0.8 + Math.random() * 0.45); }
|
||
else { phase = 'calm'; phaseT = Math.max(0.35, (1.3 - 0.8 * st) * (0.8 + Math.random() * 0.5)); }
|
||
}
|
||
const pulling = phase === 'pull';
|
||
// бот: максимум держать, но отпускать, когда натяжение подходит к прочности лески
|
||
// (держать в затишье неоптимально только при критичном натяжении — тогда сбрасываем)
|
||
const holding = tension < 0.75 * rod.line;
|
||
const drain = (pulling ? 0.016 + 0.02 * aggr : 0.002) + (holding ? 0.006 : 0.001) + (pulling && !holding ? 0.01 : 0);
|
||
st = Math.max(0, st - drain * DT);
|
||
const tired = st <= 0.12;
|
||
const base = pulling ? (16 + 34 * st) * (0.5 + 0.9 * aggr) * (0.7 + 0.5 * Math.min(1.5, ov)) : (5 + 7 * aggr);
|
||
const F = tired ? 0.45 * base : base;
|
||
if (holding) {
|
||
const over = F - rod.drag;
|
||
tension += (10 + Math.max(0, over) * 1.1 + 22 * aggr * (pulling ? 1 : 0.35)) * DT;
|
||
if (over > 0) d += over * 0.22 * DT;
|
||
else d -= (11 + 3 * rod.reel) * (1 + 0.6 * (1 - st)) * DT;
|
||
} else {
|
||
tension -= 38 * DT;
|
||
if (pulling) d += F * 0.32 * DT;
|
||
}
|
||
tension = Math.max(0, tension);
|
||
if (holding) escape = Math.max(0, escape - 0.5 * DT);
|
||
else if (!pulling) escape = Math.min(1, escape + (0.2 + aggr * 0.4) * DT);
|
||
if (escape >= 1) return { caught: false, t };
|
||
if (tension >= rod.line) return { caught: false, t };
|
||
if (d <= 0) return { caught: true, t };
|
||
if (d >= d0 * 1.45) return { caught: false, t };
|
||
}
|
||
return { caught: false, t };
|
||
}
|
||
|
||
/* ---------- 5. Оценка вида при снасти (кэш) ---------- */
|
||
const SPEC_CACHE = new Map();
|
||
function speciesOutcome(ri, rlv, li, llv, sp) {
|
||
const key = ri + '|' + rlv + '|' + li + '|' + llv;
|
||
let map = SPEC_CACHE.get(key);
|
||
if (!map) { map = new Map(); SPEC_CACHE.set(key, map); }
|
||
if (map.has(sp.id)) return map.get(sp.id);
|
||
const rod = effRod(ri, rlv), lure = effLure(li, llv);
|
||
const B = Math.min(0.95, rod.size + lure.size);
|
||
const a = 2 / (1 + 2 * B);
|
||
let caught = 0, tSum = 0, wCaught = 0;
|
||
for (let i = 0; i < samples; i++) {
|
||
const w = sp.wMin + Math.pow(Math.random(), a) * (sp.wMax - sp.wMin);
|
||
const r = fight(w, sp.strength, rod, 150);
|
||
tSum += r.t;
|
||
if (r.caught) { caught++; wCaught += w; }
|
||
}
|
||
const res = { p: caught / samples, t: tSum / samples, valW: wCaught / samples };
|
||
map.set(sp.id, res);
|
||
return res;
|
||
}
|
||
|
||
/* ---------- 6. Доходность локации при снасти, 🪙/мин ---------- */
|
||
const PHASES = [
|
||
{ night: false, bite: 1 },
|
||
{ night: false, bite: 1 },
|
||
{ night: true, bite: 1 },
|
||
{ night: true, bite: 0.85 }, // ночь: biteChance ×0.85
|
||
];
|
||
const DIST = 0.9; // дальность заброса (полная мощность, cast≥1)
|
||
const BASE_P = 0.65 + 0.25 * DIST;
|
||
const RATE_CACHE = new Map();
|
||
function rateFor(loc, ri, rlv, li, llv) {
|
||
const key = loc + '|' + ri + '|' + rlv + '|' + li + '|' + llv;
|
||
if (RATE_CACHE.has(key)) return RATE_CACHE.get(key);
|
||
const rod = effRod(ri, rlv), lure = effLure(li, llv);
|
||
const boost = lure.rare * 0.15 + (llv - 1) * 0.05; // variantBoost
|
||
const E = variantE(boost);
|
||
const B = Math.min(0.95, rod.size + lure.size);
|
||
const Es = (1 + 2 * B) / (3 + 2 * B); // E[s] для rollWeight
|
||
let sum = 0;
|
||
for (const ph of PHASES) {
|
||
const tbl = ph.night ? D.SPECIES_BY_LOCATION_NIGHT[loc] : D.SPECIES_BY_LOCATION[loc];
|
||
const p = BASE_P * ph.bite;
|
||
const Ewait = (7 + (1 - p) / p * 3.75) / lure.bite;
|
||
const fixed = 0.65 + Ewait + 0.45 + 1.2; // cast + ожидание клёва + approach + карточка
|
||
let expCoins = 0, expT = 0, totW = 0;
|
||
for (const sp of tbl) totW += sp.weight;
|
||
for (const sp of tbl) {
|
||
const P = sp.weight / totW;
|
||
const o = speciesOutcome(ri, rlv, li, llv, sp);
|
||
const Ew = sp.wMin + Es * (sp.wMax - sp.wMin);
|
||
expCoins += P * sp.price * (o.valW || Ew * o.p) * E;
|
||
expT += P * o.t;
|
||
}
|
||
const cycle = fixed + expT;
|
||
sum += expCoins / (cycle / 60);
|
||
}
|
||
const res = sum / PHASES.length;
|
||
RATE_CACHE.set(key, res);
|
||
return res;
|
||
}
|
||
|
||
/* ---------- 7. Жадный маршрут покупок ---------- */
|
||
const state = {
|
||
coins: startCoins,
|
||
rods: [0], rodLv: { 0: 1 }, rod: 0,
|
||
lures: [0], lureLv: { 0: 1 }, lure: 0,
|
||
locs: [0], loc: 0,
|
||
time: 0,
|
||
};
|
||
function bestRate() {
|
||
let best = 0, info = null;
|
||
for (const ri of state.rods) {
|
||
const rlv = state.rodLv[ri] || 1;
|
||
for (const li of state.lures) {
|
||
const llv = state.lureLv[li] || 1;
|
||
for (const loc of state.locs) {
|
||
const r = rateFor(loc, ri, rlv, li, llv);
|
||
if (r > best) { best = r; info = { ri, rlv, li, llv, loc }; }
|
||
}
|
||
}
|
||
}
|
||
return { rate: best, info };
|
||
}
|
||
function stepCost(tok) {
|
||
const m = /^(rod|lure|loc)(\d+)(?::(\d+))?$/.exec(tok);
|
||
if (!m) fail(`не разобрать шаг маршрута «${tok}»`);
|
||
const kind = m[1], i = Number(m[2]), lvl = m[3] ? Number(m[3]) : null;
|
||
if (kind === 'loc') return { kind, i, cost: LOCATIONS[i] ? LOCATIONS[i].cost : NaN, label: `локация «${(LOCATIONS[i] || {}).name || i}»` };
|
||
const type = kind === 'rod' ? 'rod' : 'lure';
|
||
const list = type === 'rod' ? RODS : LURES;
|
||
const lvMap = type === 'rod' ? state.rodLv : state.lureLv;
|
||
const owned = type === 'rod' ? state.rods : state.lures;
|
||
if (!list[i]) fail(`шаг «${tok}»: снасти с индексом ${i} нет`);
|
||
if (owned.indexOf(i) < 0) { // покупка
|
||
if (i > 0 && (lvMap[list[i - 1].id] || 1) < 4) fail(`шаг «${tok}»: сначала предыдущая снасть на 4 ур.`);
|
||
return { kind, type, i, cost: list[i].cost, label: `купить «${list[i].name}»` };
|
||
}
|
||
// апгрейд
|
||
const cur = lvMap[i] || 1;
|
||
if (!lvl || lvl <= cur) fail(`шаг «${tok}»: нечего улучшать (текущий ур. ${cur}, цель ${lvl})`);
|
||
let cost = 0;
|
||
for (let lv = cur; lv < lvl; lv++) cost += upgCost(type, i, lv);
|
||
return { kind, type, i, lvl, cost, label: `«${list[i].name}» ${cur}→${lvl} ур.` };
|
||
}
|
||
function applyStep(s) {
|
||
if (s.kind === 'loc') { state.locs.push(s.i); state.loc = s.i; return; }
|
||
if (s.kind === 'rod' || s.kind === 'lure') {
|
||
const type = s.kind;
|
||
const owned = type === 'rod' ? state.rods : state.lures;
|
||
const lvMap = type === 'rod' ? state.rodLv : state.lureLv;
|
||
if (owned.indexOf(s.i) < 0) { owned.push(s.i); lvMap[s.i] = 1; if (type === 'rod') state.rod = s.i; else state.lure = s.i; }
|
||
else lvMap[s.i] = s.lvl;
|
||
}
|
||
}
|
||
function fmtTime(min) {
|
||
if (min < 60) return min.toFixed(1) + ' мин';
|
||
const h = Math.floor(min / 60), m = min - h * 60;
|
||
return h + ' ч ' + m.toFixed(0) + ' мин';
|
||
}
|
||
|
||
/* ---------- 8. Печать ---------- */
|
||
const overridesUsed = [];
|
||
if (Object.keys(rodCost).length || Object.keys(lureCost).length || Object.keys(locCost).length ||
|
||
Object.keys(sets).length || Object.keys(setsDay).length || Object.keys(setsNight).length ||
|
||
rodUpg || lureUpg || upgMult) {
|
||
overridesUsed.push([...Object.entries(rodCost).map(([k, v]) => `rod${k}=${v}`),
|
||
...Object.entries(lureCost).map(([k, v]) => `lure${k}=${v}`),
|
||
...Object.entries(locCost).map(([k, v]) => `loc${k}=${v}`),
|
||
...(rodUpg ? ['rod-upg=' + rodUpg.join(',')] : []),
|
||
...(lureUpg ? ['lure-upg=' + lureUpg.join(',')] : []),
|
||
...(upgMult ? ['upg-mult=' + upgMult.join(',')] : []),
|
||
...Object.entries(sets).map(([k, v]) => `set ${k}=${v}`),
|
||
...Object.entries(setsDay).map(([k, v]) => `set-day ${k}=${v}`),
|
||
...Object.entries(setsNight).map(([k, v]) => `set-night ${k}=${v}`)].join(', '));
|
||
}
|
||
const dash = s => '─'.repeat(Math.max(2, 68 - s.length));
|
||
console.log('Прогрессия — node tools/progression.mjs');
|
||
console.log(`формула: улов/бой как в игре; день/ночь 50/50; питание вариантов E[mult] нейтрально (снасть учитывается)`);
|
||
console.log(`параметры: start-coins=${startCoins} · per-day=${perDay} мин · samples=${samples}`);
|
||
if (overridesUsed.length) console.log('WHAT-IF (НЕ сохранено): ' + overridesUsed);
|
||
console.log('');
|
||
|
||
// Сводка доходности по локациям с эталонной снастью (Титан lv1 + Блёстка lv1)
|
||
console.log('── ДОХОДНОСТЬ ПО ВОДАМ (Титан lv1 + Блёстка lv1, 🪙/мин) ' + dash('──'));
|
||
for (let i = 0; i < LOCATIONS.length; i++) {
|
||
const r = rateFor(i, 3, 1, 3, 1);
|
||
console.log((' ' + LOCATIONS[i].name + ' (' + LOCATIONS[i].cost + ' 🪙)').padEnd(34) + r.toFixed(0).padStart(6) + ' 🪙/мин');
|
||
}
|
||
console.log('');
|
||
|
||
console.log('── МАРШРУТ ' + dash('── МАРШРУТ '));
|
||
console.log(' цель'.padEnd(34) + 'цена'.padStart(9) + 'шаг'.padStart(10) + 'всего'.padStart(11) + 'доход'.padStart(10));
|
||
const tokens = route.split(',').map(s => s.trim()).filter(Boolean);
|
||
for (const tok of tokens) {
|
||
const s = stepCost(tok);
|
||
const { rate } = bestRate();
|
||
let dt = 0;
|
||
if (state.coins < s.cost) {
|
||
dt = (s.cost - state.coins) / rate; // минуты
|
||
state.time += dt;
|
||
state.coins += rate * dt;
|
||
}
|
||
state.coins -= s.cost;
|
||
const before = rate;
|
||
applyStep(s);
|
||
console.log((' ' + s.label).padEnd(34) + String(s.cost).padStart(9) + fmtTime(dt).padStart(10) +
|
||
fmtTime(state.time).padStart(11) + (before.toFixed(0) + '/м').padStart(10));
|
||
}
|
||
console.log('');
|
||
const total = state.time;
|
||
console.log(`Итого: ${fmtTime(total)} (${(total / 60).toFixed(2)} ч) до последней цели маршрута · ` +
|
||
`при ${perDay} мин/день ≈ ${(total / perDay).toFixed(1)} дней`);
|
||
console.log(`остаток монет: ${state.coins.toFixed(0)} 🪙 · время задним числом не включает ручные паузы/рекламу`);
|
||
console.log('');
|
||
console.log('Замечание: маршрут — модель «идеального» игрока (лучшая доступная вода, лучшая снасть,');
|
||
console.log('бот без ошибок). Реальные времена больше на промахи подсечки и меню; для проверки — плейтест.');
|