phase 7: Mycelium — persistent knowledge network connecting runs
- Mycelium graph: nodes/edges/strength, deposit discoveries, weighted extraction - Fungal nodes: ECS entities on world map with bioluminescent glow animation - Knowledge system: deposit at nodes (+ auto on death), memory flash retrieval - Mycosis: visual tint overlay on prolonged node contact, reveal threshold - Spore shop: 5 Cradle bonuses (health, elements, knowledge boost) - MetaState extended with MyceliumGraphData, IndexedDB persistence updated - GameScene: node spawning, glow rendering, E-key interaction, mycosis overlay - CradleScene: spore shop UI, Mycelium stats, purchased effects forwarding - 36 new tests (385 total) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -59,6 +59,15 @@ import {
|
||||
} from '../run/crisis';
|
||||
import { getEscalationEffects } from '../run/escalation';
|
||||
|
||||
// Mycelium imports
|
||||
import { FungalNode } from '../ecs/components';
|
||||
import { spawnFungalNodes } from '../mycelium/nodes';
|
||||
import { depositKnowledge } from '../mycelium/graph';
|
||||
import { extractMemoryFlashes } from '../mycelium/knowledge';
|
||||
import { createMycosisState, updateMycosis, getMycosisVisuals } from '../mycelium/mycosis';
|
||||
import type { FungalNodeInfo, MycosisState, MemoryFlash } from '../mycelium/types';
|
||||
import { FUNGAL_NODE_CONFIG, MYCOSIS_CONFIG } from '../mycelium/types';
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
private gameWorld!: GameWorld;
|
||||
private bridge!: PhaserBridge;
|
||||
@@ -107,15 +116,31 @@ export class GameScene extends Phaser.Scene {
|
||||
private playerDead = false;
|
||||
private crisisOverlay!: Phaser.GameObjects.Rectangle;
|
||||
|
||||
// Mycelium state
|
||||
private fungalNodeData!: Map<number, FungalNodeInfo>;
|
||||
private mycosisState!: MycosisState;
|
||||
private mycosisOverlay!: Phaser.GameObjects.Rectangle;
|
||||
private memoryFlashText!: Phaser.GameObjects.Text;
|
||||
private memoryFlashTimer = 0;
|
||||
private fungalNodeGlowGraphics!: Phaser.GameObjects.Graphics;
|
||||
private hasDepositedThisRun = false;
|
||||
|
||||
constructor() {
|
||||
super({ key: 'GameScene' });
|
||||
}
|
||||
|
||||
init(data: { meta: MetaState; schoolId: string; runId: number }): void {
|
||||
// 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 {
|
||||
this.meta = data.meta;
|
||||
this.runState = createRunState(data.runId, data.schoolId);
|
||||
this.crisisState = null;
|
||||
this.playerDead = false;
|
||||
this.mycosisState = createMycosisState();
|
||||
this.hasDepositedThisRun = false;
|
||||
this.memoryFlashTimer = 0;
|
||||
this.purchasedEffects = data.purchasedEffects ?? [];
|
||||
}
|
||||
|
||||
create(): void {
|
||||
@@ -142,6 +167,11 @@ export class GameScene extends Phaser.Scene {
|
||||
this.gameWorld.world, worldData.grid, biome, this.worldSeed,
|
||||
);
|
||||
|
||||
// 5b. Spawn fungal nodes (Mycelium surface points)
|
||||
this.fungalNodeData = spawnFungalNodes(
|
||||
this.gameWorld.world, worldData.grid, biome, this.worldSeed,
|
||||
);
|
||||
|
||||
// 6. Initialize creature systems
|
||||
const allSpecies = speciesDataArray as SpeciesData[];
|
||||
this.speciesRegistry = new SpeciesRegistry(allSpecies);
|
||||
@@ -163,8 +193,9 @@ export class GameScene extends Phaser.Scene {
|
||||
this.inventory = new Inventory(500, 20);
|
||||
this.quickSlots = new QuickSlots();
|
||||
|
||||
// Give starting elements from chosen school
|
||||
// Give starting elements from chosen school + apply purchased bonuses
|
||||
this.giveStartingKit();
|
||||
this.applyPurchasedEffects();
|
||||
|
||||
// 9. Camera — follow player, zoom via scroll wheel
|
||||
const worldPixelW = biome.mapWidth * biome.tileSize;
|
||||
@@ -247,6 +278,36 @@ export class GameScene extends Phaser.Scene {
|
||||
this.crisisOverlay.setScrollFactor(0);
|
||||
this.crisisOverlay.setDepth(90);
|
||||
|
||||
// 10b. Mycosis overlay (full-screen tinted rectangle, hidden by default)
|
||||
this.mycosisOverlay = this.add.rectangle(
|
||||
this.cameras.main.width / 2, this.cameras.main.height / 2,
|
||||
this.cameras.main.width, this.cameras.main.height,
|
||||
MYCOSIS_CONFIG.tintColor, 0,
|
||||
);
|
||||
this.mycosisOverlay.setScrollFactor(0);
|
||||
this.mycosisOverlay.setDepth(91);
|
||||
|
||||
// 10c. Fungal node glow graphics (world-space, under entities)
|
||||
this.fungalNodeGlowGraphics = this.add.graphics();
|
||||
this.fungalNodeGlowGraphics.setDepth(2);
|
||||
|
||||
// 10d. Memory flash text (center of screen, fades in/out)
|
||||
this.memoryFlashText = this.add.text(
|
||||
this.cameras.main.width / 2, this.cameras.main.height / 2, '', {
|
||||
fontSize: '16px',
|
||||
color: '#88ffaa',
|
||||
fontFamily: 'monospace',
|
||||
backgroundColor: '#0a1a0fcc',
|
||||
padding: { x: 12, y: 8 },
|
||||
align: 'center',
|
||||
wordWrap: { width: 400 },
|
||||
},
|
||||
);
|
||||
this.memoryFlashText.setScrollFactor(0);
|
||||
this.memoryFlashText.setOrigin(0.5);
|
||||
this.memoryFlashText.setDepth(105);
|
||||
this.memoryFlashText.setAlpha(0);
|
||||
|
||||
// 11. Launch UIScene overlay
|
||||
this.scene.launch('UIScene');
|
||||
|
||||
@@ -261,6 +322,28 @@ export class GameScene extends Phaser.Scene {
|
||||
};
|
||||
}
|
||||
|
||||
/** Apply purchased spore bonuses from the Cradle shop */
|
||||
private applyPurchasedEffects(): void {
|
||||
for (const effect of this.purchasedEffects) {
|
||||
switch (effect.type) {
|
||||
case 'extra_health':
|
||||
Health.max[this.playerEid] = (Health.max[this.playerEid] ?? 100) + effect.amount;
|
||||
Health.current[this.playerEid] = Health.max[this.playerEid];
|
||||
break;
|
||||
case 'extra_element':
|
||||
for (let i = 0; i < effect.quantity; i++) {
|
||||
this.inventory.addItem(effect.symbol);
|
||||
}
|
||||
this.quickSlots.autoAssign(effect.symbol);
|
||||
break;
|
||||
case 'knowledge_boost':
|
||||
// Stored for use when extracting memory flashes
|
||||
// (increases clarity of extracted flashes)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Give the player their school's starting elements */
|
||||
private giveStartingKit(): void {
|
||||
const schools = schoolsData as SchoolData[];
|
||||
@@ -323,23 +406,30 @@ export class GameScene extends Phaser.Scene {
|
||||
this.projectileData,
|
||||
);
|
||||
|
||||
// 7. Resource interaction (E key, debounced)
|
||||
// 7. E key interaction (debounced) — fungal node has priority, then resources
|
||||
const isEDown = this.keys.E.isDown;
|
||||
const justPressedE = isEDown && !this.wasEDown;
|
||||
this.wasEDown = isEDown;
|
||||
const interaction = interactionSystem(
|
||||
this.gameWorld.world, justPressedE, this.inventory, this.resourceData,
|
||||
);
|
||||
if (interaction) {
|
||||
// Auto-assign collected items to quick slots
|
||||
if (interaction.type === 'collected' || interaction.type === 'depleted') {
|
||||
if (interaction.itemId) {
|
||||
this.quickSlots.autoAssign(interaction.itemId);
|
||||
// Record element discovery
|
||||
recordDiscovery(this.runState, 'element', interaction.itemId);
|
||||
|
||||
// Check if player is near a fungal node first
|
||||
const nearbyFungalNode = this.getNearestFungalNode();
|
||||
if (justPressedE && nearbyFungalNode !== null) {
|
||||
// Fungal node interaction takes priority
|
||||
this.interactWithFungalNode(nearbyFungalNode);
|
||||
} else {
|
||||
// Normal resource interaction
|
||||
const interaction = interactionSystem(
|
||||
this.gameWorld.world, justPressedE, this.inventory, this.resourceData,
|
||||
);
|
||||
if (interaction) {
|
||||
if (interaction.type === 'collected' || interaction.type === 'depleted') {
|
||||
if (interaction.itemId) {
|
||||
this.quickSlots.autoAssign(interaction.itemId);
|
||||
recordDiscovery(this.runState, 'element', interaction.itemId);
|
||||
}
|
||||
}
|
||||
this.showInteractionFeedback(interaction.type, interaction.itemId);
|
||||
}
|
||||
this.showInteractionFeedback(interaction.type, interaction.itemId);
|
||||
}
|
||||
|
||||
// 8. Quick slot selection (1-4 keys)
|
||||
@@ -396,7 +486,10 @@ export class GameScene extends Phaser.Scene {
|
||||
// 9f. Creature attacks on player
|
||||
creatureAttackPlayerSystem(this.gameWorld.world, this.speciesLookup);
|
||||
|
||||
// 9g. Crisis damage (if active)
|
||||
// 9g. Mycelium — fungal node glow, interaction, mycosis
|
||||
this.updateMycelium(delta);
|
||||
|
||||
// 9h. Crisis damage (if active)
|
||||
if (this.crisisState?.active) {
|
||||
applyCrisisDamage(this.crisisState, delta);
|
||||
const crisisDmg = getCrisisPlayerDamage(this.crisisState, delta);
|
||||
@@ -411,7 +504,7 @@ export class GameScene extends Phaser.Scene {
|
||||
this.crisisOverlay.setFillStyle(tint.color, tint.alpha);
|
||||
}
|
||||
|
||||
// 9h. Environmental damage from high escalation
|
||||
// 9i. Environmental damage from high escalation
|
||||
const escalationFx = getEscalationEffects(this.runState.escalation);
|
||||
if (escalationFx.environmentalDamage > 0) {
|
||||
const envDmg = escalationFx.environmentalDamage * (delta / 1000);
|
||||
@@ -527,6 +620,109 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
}
|
||||
|
||||
/** Find the nearest fungal node within interaction range, or null */
|
||||
private getNearestFungalNode(): number | null {
|
||||
const px = Position.x[this.playerEid];
|
||||
const py = Position.y[this.playerEid];
|
||||
const nodeEids = query(this.gameWorld.world, [Position, FungalNode]);
|
||||
|
||||
let nearestEid: number | null = null;
|
||||
let nearestDist = Infinity;
|
||||
|
||||
for (const eid of nodeEids) {
|
||||
const dx = Position.x[eid] - px;
|
||||
const dy = Position.y[eid] - py;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist <= FungalNode.interactRange[eid] && dist < nearestDist) {
|
||||
nearestEid = eid;
|
||||
nearestDist = dist;
|
||||
}
|
||||
}
|
||||
|
||||
return nearestEid;
|
||||
}
|
||||
|
||||
/** Mycelium system — glow animation, mycosis update, memory flash fade */
|
||||
private updateMycelium(delta: number): void {
|
||||
// Check proximity to any fungal node (for mycosis)
|
||||
const nearNode = this.getNearestFungalNode() !== null;
|
||||
|
||||
// Update mycosis
|
||||
updateMycosis(this.mycosisState, delta, nearNode);
|
||||
const visuals = getMycosisVisuals(this.mycosisState);
|
||||
this.mycosisOverlay.setFillStyle(visuals.tintColor, visuals.tintAlpha);
|
||||
|
||||
// Render fungal node glows
|
||||
const nodeEids = query(this.gameWorld.world, [Position, FungalNode]);
|
||||
this.fungalNodeGlowGraphics.clear();
|
||||
for (const eid of nodeEids) {
|
||||
FungalNode.glowPhase[eid] += FUNGAL_NODE_CONFIG.glowPulseSpeed * (delta / 1000);
|
||||
const phase = FungalNode.glowPhase[eid];
|
||||
const pulse = 0.4 + 0.6 * Math.sin(phase);
|
||||
const radius = FUNGAL_NODE_CONFIG.glowRadius * (0.8 + 0.4 * pulse);
|
||||
const alpha = 0.15 + 0.15 * pulse;
|
||||
|
||||
this.fungalNodeGlowGraphics.fillStyle(FUNGAL_NODE_CONFIG.glowColor, alpha);
|
||||
this.fungalNodeGlowGraphics.fillCircle(
|
||||
Position.x[eid], Position.y[eid], radius,
|
||||
);
|
||||
// Inner brighter glow
|
||||
this.fungalNodeGlowGraphics.fillStyle(FUNGAL_NODE_CONFIG.glowColor, alpha * 1.5);
|
||||
this.fungalNodeGlowGraphics.fillCircle(
|
||||
Position.x[eid], Position.y[eid], radius * 0.5,
|
||||
);
|
||||
}
|
||||
|
||||
// Fade memory flash text
|
||||
if (this.memoryFlashTimer > 0) {
|
||||
this.memoryFlashTimer -= delta;
|
||||
if (this.memoryFlashTimer <= 500) {
|
||||
this.memoryFlashText.setAlpha(this.memoryFlashTimer / 500);
|
||||
}
|
||||
if (this.memoryFlashTimer <= 0) {
|
||||
this.memoryFlashText.setAlpha(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Push mycosis data to registry for UIScene
|
||||
this.registry.set('mycosisLevel', this.mycosisState.level);
|
||||
this.registry.set('mycosisRevealing', this.mycosisState.revealing);
|
||||
}
|
||||
|
||||
/** Handle interaction with a fungal node */
|
||||
private interactWithFungalNode(nodeEid: number): void {
|
||||
// First time interacting in this run → deposit discoveries
|
||||
if (!this.hasDepositedThisRun) {
|
||||
const result = depositKnowledge(this.meta.mycelium, this.runState);
|
||||
this.hasDepositedThisRun = true;
|
||||
|
||||
const msg = result.newNodes > 0
|
||||
? `🍄 Мицелий принял ${result.newNodes} знани${result.newNodes === 1 ? 'е' : 'й'}`
|
||||
: `🍄 Мицелий укрепил ${result.strengthened} связ${result.strengthened === 1 ? 'ь' : 'ей'}`;
|
||||
this.showInteractionFeedback('collected', msg);
|
||||
}
|
||||
|
||||
// Extract memory flashes from past runs
|
||||
if (this.meta.mycelium.nodes.length > 0) {
|
||||
const flashes = extractMemoryFlashes(this.meta.mycelium, 2);
|
||||
if (flashes.length > 0) {
|
||||
this.showMemoryFlash(flashes[0]);
|
||||
}
|
||||
} else {
|
||||
this.showInteractionFeedback('collected', '🍄 Мицелий пуст... пока.');
|
||||
}
|
||||
}
|
||||
|
||||
/** Display a memory flash on screen */
|
||||
private showMemoryFlash(flash: MemoryFlash): void {
|
||||
// Show Russian text with clarity indicator
|
||||
const clarityStars = flash.clarity >= 0.7 ? '★★★' : flash.clarity >= 0.4 ? '★★☆' : '★☆☆';
|
||||
const text = `${flash.textRu}\n\n[${clarityStars}]`;
|
||||
this.memoryFlashText.setText(text);
|
||||
this.memoryFlashText.setAlpha(1);
|
||||
this.memoryFlashTimer = 4000; // Show for 4 seconds
|
||||
}
|
||||
|
||||
/** Manage run phase transitions and escalation */
|
||||
private updateRunPhase(delta: number): void {
|
||||
const phase = this.runState.phase;
|
||||
@@ -576,6 +772,12 @@ export class GameScene extends Phaser.Scene {
|
||||
this.playerDead = true;
|
||||
this.runState.alive = false;
|
||||
|
||||
// Auto-deposit all discoveries into Mycelium on death
|
||||
if (!this.hasDepositedThisRun) {
|
||||
depositKnowledge(this.meta.mycelium, this.runState);
|
||||
this.hasDepositedThisRun = true;
|
||||
}
|
||||
|
||||
// Stop UIScene
|
||||
this.scene.stop('UIScene');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user