/
kochkatech
/
CorpGame
Обзор
Документация
Войти
/
kochkatech
/
CorpGame
Код
Запросы
1
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
dev
backend/src/game/default-config.ts
219 строк
10 KB
kochkareal
chore: добавлен .gitattributes, нормализованы переносы строк (LF везде, кроме .bat/.cmd)
06 авг 2026, 15:18
06 авг 2026, 15:18
c25ef1b
Код
Авторство
О чём код?
/** * Дефолтный конфиг баланса игровой сессии. * Хранится в GameSession.config (JSON), не в коде — со временем администратор * сможет редактировать его прямо в UI без деплоя. */ export interface ResourceDef { key: string; label: string; icon: string; /** 0 — самый распространённый, 5 — самый редкий. Определяет порядок пулов городов. */ rarity: number; } export interface CityDef { key: string; name: string; /** 1..9, дальше — реже и дороже, но и очков больше */ tier: number; /** 4 ресурса от частого к редкому; уровень фил-ла режет этот список */ resourcePool: string[]; /** КД сбора на уровне 1, сек */ baseCooldownSec: number; /** Стоимость открытия города */ openCost: Record<string, number>; /** Очки, начисляемые один раз при открытии */ scoreOnOpen: number; } export interface BankConfig { /** Курс N:1 — N единиц любого ресурса меняются на 1 единицу любого другого */ rate: number; } export interface UnlockConfig { /** Сколько городов в каждой волне (тире) */ citiesPerTier: number; /** Сколько городов текущей волны нужно открыть, чтобы открылась следующая */ opensToUnlockNext: number; } export interface BranchUpgradeConfig { maxLevel: number; /** КД на уровне L = baseCooldownSec * cooldownFactor^(L-1), не ниже minCooldownSec */ cooldownFactor: number; minCooldownSec: number; } export interface CostsConfig { /** Сколько разных видов ресурсов нужно для открытия города тира 1 */ openTypeBase: number; /** +1 вид за каждые openTypeStep тиров */ openTypeStep: number; /** Не больше стольки видов сразу для открытия — иначе первый же дальний город неподъёмен */ openTypeMax: number; /** Апгрейд на уровень L добавляет upgradeExtraTypesPerLevel * L дополнительных (более редких) видов */ upgradeExtraTypesPerLevel: number; } export interface StartingKitConfig { /** Сколько разных видов ресурсов получает команда в начале — по 1 единице каждого */ types: number; amountEach: number; } export interface InteractionsConfig { /** Максимум за одну передачу — доля текущего запаса ресурса, % */ transferMaxPercent: number; theftDurationSec: number; /** Сколько команд предлагается жертве угадать (включая настоящего вора) */ theftCatchOptions: number; maxTheftsPerTeam: number; maxSanctionsPerTeam: number; /** Во сколько раз замедляется КД филиала под санкцией, если понижать уровень уже некуда */ sanctionSlowMultiplier: number; sanctionSlowDurationSec: number; } export interface DashboardConfig { /** Показывать на публичном дашборде значки ресурсов команд */ showResources: boolean; /** Показывать очки и рейтинговое место (медали) — выключается, чтобы не спойлерить победителя до конца игры */ showScore: boolean; } export interface GameConfig { resources: ResourceDef[]; cities: CityDef[]; bank: BankConfig; unlock: UnlockConfig; branchUpgrade: BranchUpgradeConfig; costs: CostsConfig; interactions: InteractionsConfig; startingKit: StartingKitConfig; dashboard: DashboardConfig; } const RESOURCES: ResourceDef[] = [ { key: 'wood', label: 'Древесина', icon: '🪵', rarity: 0 }, { key: 'grain', label: 'Зерно', icon: '🌾', rarity: 1 }, { key: 'iron', label: 'Железо', icon: '⚙️', rarity: 2 }, { key: 'oil', label: 'Нефть', icon: '🛢️', rarity: 3 }, { key: 'uranium', label: 'Уран', icon: '☢️', rarity: 4 }, { key: 'diamonds', label: 'Алмазы', icon: '💎', rarity: 5 }, ]; /** * Только эти 4 самых частых ресурса когда-либо требуются для открытия/апгрейда — уран и алмазы * добываются, дают очки и идут в банк, но НИКОГДА не блокируют прогрессию (иначе город, который их * производит, было бы невозможно открыть без них самих). */ export const COMMON_RESOURCE_KEYS = ['wood', 'grain', 'iron', 'oil']; /** Циклический сдвиг массива на shift позиций — даёт вариативность между городами одного тира */ function rotate<T>(arr: T[], shift: number): T[] { const len = arr.length; const s = ((shift % len) + len) % len; return arr.slice(s).concat(arr.slice(0, s)); } /** * 9 волн по 3 известных города. `pool` — окно из 4 ресурсов (от частого к редкому), которое ДОБЫВАЕТ * вся тройка; каждый город тира получает свой цикличный сдвиг этого окна — поэтому три города одного * тира производят РАЗНЫЕ ресурсы (а не одно и то же), и есть смысл открывать больше одного. * Редкость окна растёт с тиром: ближние тиры — частые ресурсы, дальние — уран/алмазы. */ const TIER_CITIES: { names: [string, string, string]; pool: string[] }[] = [ { names: ['Москва', 'Санкт-Петербург', 'Казань'], pool: ['wood', 'grain', 'iron', 'oil'] }, { names: ['Екатеринбург', 'Новосибирск', 'Нижний Новгород'], pool: ['wood', 'grain', 'iron', 'oil'] }, { names: ['Самара', 'Челябинск', 'Ростов-на-Дону'], pool: ['grain', 'iron', 'oil', 'uranium'] }, { names: ['Тюмень', 'Сургут', 'Уфа'], pool: ['grain', 'iron', 'oil', 'uranium'] }, { names: ['Норильск', 'Мурманск', 'Владивосток'], pool: ['iron', 'oil', 'uranium', 'diamonds'] }, { names: ['Якутск', 'Магадан', 'Иркутск'], pool: ['iron', 'oil', 'uranium', 'diamonds'] }, { names: ['Дубай', 'Хьюстон', 'Йоханнесбург'], pool: ['oil', 'uranium', 'diamonds', 'iron'] }, { names: ['Лондон', 'Нью-Йорк', 'Токио'], pool: ['oil', 'uranium', 'diamonds', 'iron'] }, { names: ['Антверпен', 'Мумбаи', 'Сингапур'], pool: ['uranium', 'diamonds', 'oil', 'iron'] }, ]; function slugify(name: string, tier: number, idx: number): string { const translit: Record<string, string> = { а: 'a', б: 'b', в: 'v', г: 'g', д: 'd', е: 'e', ё: 'e', ж: 'zh', з: 'z', и: 'i', й: 'y', к: 'k', л: 'l', м: 'm', н: 'n', о: 'o', п: 'p', р: 'r', с: 's', т: 't', у: 'u', ф: 'f', х: 'h', ц: 'c', ч: 'ch', ш: 'sh', щ: 'sch', ъ: '', ы: 'y', ь: '', э: 'e', ю: 'yu', я: 'ya', ' ': '-', '-': '-', }; const key = name .toLowerCase() .split('') .map((ch) => translit[ch] ?? ch) .join('') .replace(/[^a-z0-9-]/g, ''); return key || `city-${tier}-${idx}`; } /** Сколько разных видов ресурсов нужно для открытия города данного тира — растёт с удалённостью */ export function openTypeCount(costs: CostsConfig, tier: number): number { const grown = costs.openTypeBase + Math.floor((tier - 1) / costs.openTypeStep); return Math.min(costs.openTypeMax, grown); } /** * Стоимость открытия — всегда 1 единица каждого из N разных видов ресурса, и всегда из 4 самых частых * (уран/алмазы никогда не требуются — иначе город, который их производит, было бы невозможно открыть). * Сдвигается по индексу города внутри тира, поэтому три города одного тира требуют РАЗНЫЕ * (пересекающиеся) наборы — есть вариативность и запасные варианты для открытия. */ function buildOpenCost(typeCount: number, idx: number): Record<string, number> { const rotated = rotate(COMMON_RESOURCE_KEYS, idx); const types = rotated.slice(0, Math.min(typeCount, rotated.length)); return Object.fromEntries(types.map((key) => [key, 1])); } function buildCities(costs: CostsConfig): CityDef[] { const cities: CityDef[] = []; TIER_CITIES.forEach((tierDef, tierIdx) => { const tier = tierIdx + 1; tierDef.names.forEach((name, idx) => { cities.push({ key: slugify(name, tier, idx), name, tier, // Свой сдвиг окна на город — три города тира добывают РАЗНЫЕ ресурсы, не одно и то же. resourcePool: rotate(tierDef.pool, idx), baseCooldownSec: 60 + (tier - 1) * 15, openCost: buildOpenCost(openTypeCount(costs, tier), idx), scoreOnOpen: tier * 10, }); }); }); return cities; } const COSTS: CostsConfig = { openTypeBase: 2, openTypeStep: 2, openTypeMax: 4, upgradeExtraTypesPerLevel: 1, }; export const DEFAULT_GAME_CONFIG: GameConfig = { resources: RESOURCES, cities: buildCities(COSTS), bank: { rate: 3 }, unlock: { citiesPerTier: 3, opensToUnlockNext: 2 }, branchUpgrade: { maxLevel: 3, cooldownFactor: 0.75, minCooldownSec: 20 }, costs: COSTS, interactions: { transferMaxPercent: 50, theftDurationSec: 20, theftCatchOptions: 3, maxTheftsPerTeam: 5, maxSanctionsPerTeam: 5, sanctionSlowMultiplier: 2, sanctionSlowDurationSec: 120, }, startingKit: { types: 3, amountEach: 1 }, dashboard: { showResources: true, showScore: true }, };