phase 9: biome expansion — 3 biomes, 40 elements, 119 reactions, 9 species

Expand beyond vertical slice with two new biomes and massive chemistry expansion:

Chemistry: +20 real elements (Li→U), +39 compounds (acids/salts/oxides/organics),
+85 reactions (Haber process, thermite variants, smelting, fermentation, etc.)

Biomes: Kinetic Mountains (physics/mechanics themed) and Verdant Forests
(biology/ecology themed), each with 8 tile types and unique generation rules.

Creatures: 6 new species — Pendulums/Mechanoids/Resonators (mountains),
Symbiotes/Mimics/Spore-bearers (forests). Species filtered by biome.

Infrastructure: CradleScene biome selector UI, generic world generator
(tile lookup by property instead of hardcoded names), actinide element category.

487 tests passing (32 new).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Денис Шкабатур
2026-02-12 17:27:15 +03:00
parent 3c24205e72
commit 6ba0746bb9
16 changed files with 2176 additions and 39 deletions

View File

@@ -8,7 +8,9 @@
import Phaser from 'phaser';
import schoolsData from '../data/schools.json';
import biomeDataArray from '../data/biomes.json';
import type { SchoolData, MetaState } from '../run/types';
import type { BiomeData } from '../world/types';
import { isSchoolUnlocked } from '../run/meta';
import { GAME_WIDTH, GAME_HEIGHT } from '../config';
import { getAvailableBonuses, purchaseBonus, canAffordBonus, resetShopSession } from '../mycelium/shop';
@@ -16,11 +18,14 @@ import { getGraphStats } from '../mycelium/graph';
import type { BonusEffect } from '../mycelium/types';
const schools = schoolsData as SchoolData[];
const biomes = biomeDataArray as BiomeData[];
export class CradleScene extends Phaser.Scene {
private meta!: MetaState;
private selectedIndex = 0;
private selectedBiomeIndex = 0;
private schoolCards: Phaser.GameObjects.Container[] = [];
private biomeCards: { bg: Phaser.GameObjects.Rectangle; label: Phaser.GameObjects.Text }[] = [];
private particles: { x: number; y: number; vx: number; vy: number; alpha: number; radius: number }[] = [];
private particleGraphics!: Phaser.GameObjects.Graphics;
private introTimer = 0;
@@ -36,7 +41,9 @@ export class CradleScene extends Phaser.Scene {
init(data: { meta: MetaState }): void {
this.meta = data.meta;
this.selectedIndex = 0;
this.selectedBiomeIndex = 0;
this.schoolCards = [];
this.biomeCards = [];
this.introTimer = 0;
this.introComplete = false;
this.purchasedEffects = [];
@@ -124,7 +131,7 @@ export class CradleScene extends Phaser.Scene {
const cx = GAME_WIDTH / 2;
// Title
const title = this.add.text(cx, 60, 'СПОРОВАЯ КОЛЫБЕЛЬ', {
const title = this.add.text(cx, 30, 'СПОРОВАЯ КОЛЫБЕЛЬ', {
fontSize: '28px',
color: '#00ff88',
fontFamily: 'monospace',
@@ -134,7 +141,7 @@ export class CradleScene extends Phaser.Scene {
title.setAlpha(0);
title.setDepth(10);
const subtitle = this.add.text(cx, 100, 'Выбери свой путь', {
const subtitle = this.add.text(cx, 65, 'Выбери биом и школу', {
fontSize: '14px',
color: '#557755',
fontFamily: 'monospace',
@@ -145,10 +152,13 @@ export class CradleScene extends Phaser.Scene {
this.tweens.add({ targets: [title, subtitle], alpha: 1, duration: 800 });
// Biome selection row
this.createBiomeSelector(cx, 105);
// School cards
const cardWidth = 320;
const cardHeight = 260;
const startY = 160;
const cardHeight = 220;
const startY = 150;
const unlockedSchools = schools.filter(s => isSchoolUnlocked(this.meta, s.id));
for (let i = 0; i < unlockedSchools.length; i++) {
@@ -361,16 +371,98 @@ export class CradleScene extends Phaser.Scene {
}
}
/** Create the biome selection row */
private createBiomeSelector(cx: number, y: number): void {
const btnWidth = 200;
const btnHeight = 36;
const spacing = 12;
const totalWidth = biomes.length * (btnWidth + spacing) - spacing;
const startX = cx - totalWidth / 2 + btnWidth / 2;
const biomeColors: Record<string, number> = {
'catalytic-wastes': 0x886622,
'kinetic-mountains': 0x5577aa,
'verdant-forests': 0x228833,
};
for (let i = 0; i < biomes.length; i++) {
const biome = biomes[i];
const bx = startX + i * (btnWidth + spacing);
const isSelected = i === this.selectedBiomeIndex;
const color = biomeColors[biome.id] ?? 0x444444;
const bg = this.add.rectangle(bx, y, btnWidth, btnHeight,
isSelected ? color : 0x0a1a0f, 0.9);
bg.setStrokeStyle(isSelected ? 3 : 1, color);
bg.setDepth(10);
const label = this.add.text(bx, y, biome.nameRu, {
fontSize: '13px',
color: isSelected ? '#ffffff' : '#888888',
fontFamily: 'monospace',
fontStyle: isSelected ? 'bold' : 'normal',
});
label.setOrigin(0.5);
label.setDepth(10);
bg.setInteractive({ useHandCursor: true });
bg.on('pointerdown', () => {
this.selectedBiomeIndex = i;
this.refreshBiomeButtons();
});
bg.on('pointerover', () => {
if (i !== this.selectedBiomeIndex) {
bg.setStrokeStyle(2, 0x00ff88);
}
});
bg.on('pointerout', () => {
const sel = i === this.selectedBiomeIndex;
bg.setStrokeStyle(sel ? 3 : 1, color);
});
// Fade in
bg.setAlpha(0);
label.setAlpha(0);
this.tweens.add({ targets: [bg, label], alpha: 1, duration: 600, delay: 100 + i * 100 });
this.biomeCards.push({ bg, label });
}
}
/** Refresh biome button visual states */
private refreshBiomeButtons(): void {
const biomeColors: Record<string, number> = {
'catalytic-wastes': 0x886622,
'kinetic-mountains': 0x5577aa,
'verdant-forests': 0x228833,
};
for (let i = 0; i < this.biomeCards.length; i++) {
const card = this.biomeCards[i];
const biome = biomes[i];
const isSelected = i === this.selectedBiomeIndex;
const color = biomeColors[biome.id] ?? 0x444444;
card.bg.setFillStyle(isSelected ? color : 0x0a1a0f, 0.9);
card.bg.setStrokeStyle(isSelected ? 3 : 1, color);
card.label.setColor(isSelected ? '#ffffff' : '#888888');
card.label.setFontStyle(isSelected ? 'bold' : 'normal');
}
}
private startRun(school: SchoolData): void {
// Flash effect
this.cameras.main.flash(300, 0, 255, 136);
const selectedBiome = biomes[this.selectedBiomeIndex];
this.time.delayedCall(400, () => {
this.scene.start('GameScene', {
meta: this.meta,
schoolId: school.id,
runId: this.meta.totalRuns + 1,
purchasedEffects: this.purchasedEffects,
biomeId: selectedBiome.id,
});
});
}

View File

@@ -132,7 +132,10 @@ export class GameScene extends Phaser.Scene {
// Purchased bonuses from Cradle shop
private purchasedEffects: import('../mycelium/types').BonusEffect[] = [];
init(data: { meta: MetaState; schoolId: string; runId: number; purchasedEffects?: import('../mycelium/types').BonusEffect[] }): void {
// Biome selection
private biomeId = 'catalytic-wastes';
init(data: { meta: MetaState; schoolId: string; runId: number; purchasedEffects?: import('../mycelium/types').BonusEffect[]; biomeId?: string }): void {
this.meta = data.meta;
this.runState = createRunState(data.runId, data.schoolId);
this.crisisState = null;
@@ -141,6 +144,7 @@ export class GameScene extends Phaser.Scene {
this.hasDepositedThisRun = false;
this.memoryFlashTimer = 0;
this.purchasedEffects = data.purchasedEffects ?? [];
this.biomeId = data.biomeId ?? 'catalytic-wastes';
}
create(): void {
@@ -149,8 +153,8 @@ export class GameScene extends Phaser.Scene {
this.bridge = new PhaserBridge(this);
this.projectileData = new Map();
// 2. Generate world
const biome = biomeDataArray[0] as BiomeData;
// 2. Generate world — use selected biome
const biome = (biomeDataArray as BiomeData[]).find(b => b.id === this.biomeId) ?? biomeDataArray[0] as BiomeData;
this.worldSeed = Date.now() % 1000000;
const worldData = generateWorld(biome, this.worldSeed);
@@ -172,17 +176,18 @@ export class GameScene extends Phaser.Scene {
this.gameWorld.world, worldData.grid, biome, this.worldSeed,
);
// 6. Initialize creature systems
// 6. Initialize creature systems — filter by biome
const allSpecies = speciesDataArray as SpeciesData[];
this.speciesRegistry = new SpeciesRegistry(allSpecies);
const biomeSpecies = allSpecies.filter(s => s.biome === biome.id);
this.speciesRegistry = new SpeciesRegistry(biomeSpecies);
this.speciesLookup = new Map<number, SpeciesData>();
for (const s of allSpecies) {
for (const s of biomeSpecies) {
this.speciesLookup.set(s.speciesId, s);
}
// 7. Spawn creatures across the map
this.creatureData = spawnInitialCreatures(
this.gameWorld.world, worldData.grid, biome, this.worldSeed, allSpecies,
this.gameWorld.world, worldData.grid, biome, this.worldSeed, biomeSpecies,
);
// 8. Create player at spawn position + inventory with starting kit