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:
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user