feat(02-02): render layer + Garden scene + scheduler integration
- src/render/garden/tile-coords.ts: GRID_LAYOUT (96px tiles + 16px gap, centered in 1024×768) + tileTopLeftCanvas / tileCenterCanvas / tileCenterToDom helpers (RESEARCH Pattern 4 + Assumption A5: handles Phaser.Scale.FIT scaling via canvas getBoundingClientRect) - src/render/garden/tile-renderer.ts: drawTiles(scene) — 16 outlined rounded rectangles with hover state (D-06) - src/render/garden/plant-renderer.ts: drawPlant(scene, idx, tile, stage) — primitive shapes per stage (sprout dot / mature stem / ready bloom) tinted by plant type (D-26); destroyPlant cleanup - src/render/garden/ready-pulse.ts: applyReadyPulse alpha-cycle tween for ready stage (D-27) - src/render/garden/index.ts + src/render/index.ts: barrels - src/game/scenes/Garden.ts: Phaser Garden scene wires scheduler ↔ store ↔ render. update() loop drains commands → simulateOneTick → simAdapter.applyTilesAndUnlocks. appStore.subscribe drives reactive plant repaint (Pitfall 6 mitigation: subscribe, not read-once-in-create). pointerdown on empty tile emits 'tile-clicked-coords' for the React seed picker. BLOCKER 3 invariant: lastTickAt is read-through from store (NOT seeded with this.currentTick). - src/game/main.ts: scene registry now [Boot, Garden] - src/game/scenes/Boot.ts: create() transitions to Garden - Lint clean; build clean; 153/153 tests pass (Task-1 sim/garden tests + all baseline)
This commit is contained in:
+7
-5
@@ -1,10 +1,12 @@
|
|||||||
import * as Phaser from 'phaser';
|
import * as Phaser from 'phaser';
|
||||||
import { Boot } from './scenes/Boot.ts';
|
import { Boot } from './scenes/Boot.ts';
|
||||||
|
import { Garden } from './scenes/Garden.ts';
|
||||||
|
|
||||||
// Phase 1: minimal Phaser config that boots cleanly. Real scenes (garden, weather,
|
// Phase 2: Phaser config now adds the Garden scene (Plan 02-02). Boot
|
||||||
// watercolor post-process) land in Phase 2+. The architectural-firewall directories
|
// transitions into Garden once the scene tree is up. The architectural-
|
||||||
// (src/sim, src/render, src/ui) are siblings to this one — see `.planning/phases/
|
// firewall directories (src/sim, src/render, src/ui) are siblings to
|
||||||
// 01-foundations-and-doctrine/01-RESEARCH.md` § "Architectural Responsibility Map".
|
// this one — see `.planning/phases/01-foundations-and-doctrine/01-RESEARCH.md`
|
||||||
|
// § "Architectural Responsibility Map".
|
||||||
|
|
||||||
const config: Phaser.Types.Core.GameConfig = {
|
const config: Phaser.Types.Core.GameConfig = {
|
||||||
type: Phaser.AUTO,
|
type: Phaser.AUTO,
|
||||||
@@ -16,7 +18,7 @@ const config: Phaser.Types.Core.GameConfig = {
|
|||||||
mode: Phaser.Scale.FIT,
|
mode: Phaser.Scale.FIT,
|
||||||
autoCenter: Phaser.Scale.CENTER_BOTH,
|
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||||
},
|
},
|
||||||
scene: [Boot],
|
scene: [Boot, Garden],
|
||||||
};
|
};
|
||||||
|
|
||||||
const StartGame = (parent: string): Phaser.Game => {
|
const StartGame = (parent: string): Phaser.Game => {
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import * as Phaser from 'phaser';
|
import * as Phaser from 'phaser';
|
||||||
|
|
||||||
// Phase 1 placeholder: empty Boot scene that just proves Phaser starts.
|
/**
|
||||||
// Phase 2 replaces this with the real boot → preloader → garden flow,
|
* Phase 2: Boot scene transitions to Garden once Phaser is up.
|
||||||
// gated behind the "Tend the garden / Begin" gesture screen that calls
|
* No assets to load in Phase 2 (D-26 = Phaser primitives only); the
|
||||||
// AudioContext.resume() per CLAUDE.md banner concern #7.
|
* Begin-screen gate that calls AudioContext.resume() lives in the
|
||||||
|
* React UI layer (src/ui/begin/BeginScreen.tsx) per CLAUDE.md banner
|
||||||
|
* concern #7.
|
||||||
|
*/
|
||||||
export class Boot extends Phaser.Scene {
|
export class Boot extends Phaser.Scene {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('Boot');
|
super('Boot');
|
||||||
}
|
}
|
||||||
|
|
||||||
preload(): void {
|
preload(): void {
|
||||||
// No assets in Phase 1.
|
// Phase 3 wires the preloader (watercolor textures, fonts, audio).
|
||||||
}
|
}
|
||||||
|
|
||||||
create(): void {
|
create(): void {
|
||||||
// Phase 2 will start the preloader from here.
|
this.scene.start('Garden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import * as Phaser from 'phaser';
|
||||||
|
import { eventBus } from '../event-bus';
|
||||||
|
import { drainTicks, wallClock, type Clock } from '../../sim/scheduler';
|
||||||
|
import type { SimState } from '../../sim/state';
|
||||||
|
import { simulateOneTick, tileGrowthStage } from '../../sim/garden';
|
||||||
|
import type { Tile } from '../../sim/garden/types';
|
||||||
|
import {
|
||||||
|
drawTiles,
|
||||||
|
drawPlant,
|
||||||
|
destroyPlant,
|
||||||
|
applyReadyPulse,
|
||||||
|
tileCenterToDom,
|
||||||
|
type TileGameObjects,
|
||||||
|
type PlantGameObject,
|
||||||
|
} from '../../render/garden';
|
||||||
|
import { appStore, simAdapter } from '../../store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 4×4 garden scene (CONTEXT D-01). Wires the tick scheduler into
|
||||||
|
* Phaser's update() loop, draws tiles, dispatches pointer events to
|
||||||
|
* the EventBus + store, and re-renders plants on store changes.
|
||||||
|
*
|
||||||
|
* The Garden scene is the ONLY place where sim + store + render meet.
|
||||||
|
* It stays thin (RESEARCH Pattern 3): subscribe, dispatch.
|
||||||
|
*/
|
||||||
|
export class Garden extends Phaser.Scene {
|
||||||
|
private accumulatorMs = 0;
|
||||||
|
private lastFrameMs = 0;
|
||||||
|
private clock: Clock = wallClock;
|
||||||
|
private currentTick = 0;
|
||||||
|
private tileObjs: TileGameObjects[] = [];
|
||||||
|
private plantObjs: Map<number, PlantGameObject> = new Map();
|
||||||
|
private readyTweens: Map<number, Phaser.Tweens.Tween> = new Map();
|
||||||
|
private storeUnsubscribe: (() => void) | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('Garden');
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): void {
|
||||||
|
// Allow Playwright (Plan 02-05) to swap in a FakeClock via a window-
|
||||||
|
// scoped slot. Production-guarded via import.meta.env.PROD by the
|
||||||
|
// application layer (PhaserGame.tsx) — this scene only reads.
|
||||||
|
const slot = (window as unknown as { __tlgClock?: Clock }).__tlgClock;
|
||||||
|
if (slot) this.clock = slot;
|
||||||
|
|
||||||
|
// Restore tickCount from the store (set on save load by saveSync).
|
||||||
|
this.currentTick = appStore.getState().tickCount;
|
||||||
|
|
||||||
|
this.tileObjs = drawTiles(this);
|
||||||
|
this.tileObjs.forEach((t, idx) => {
|
||||||
|
t.hit.on('pointerdown', () => this.handleTilePointerDown(idx));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.lastFrameMs = this.clock.now();
|
||||||
|
|
||||||
|
// Re-render plants when tiles change in the store (Pitfall 6 mitigation:
|
||||||
|
// subscribe rather than read once in create()).
|
||||||
|
this.storeUnsubscribe = appStore.subscribe((state) => {
|
||||||
|
this.repaintPlants(state.tiles as Tile[]);
|
||||||
|
});
|
||||||
|
this.repaintPlants(appStore.getState().tiles as Tile[]);
|
||||||
|
|
||||||
|
eventBus.emit('scene-ready', this);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(_time: number, _delta: number): void {
|
||||||
|
const now = this.clock.now();
|
||||||
|
const deltaMs = now - this.lastFrameMs;
|
||||||
|
this.lastFrameMs = now;
|
||||||
|
if (deltaMs > 0) this.accumulatorMs += deltaMs;
|
||||||
|
|
||||||
|
// Build current SimState snapshot from the store + drain commands.
|
||||||
|
const storeState = appStore.getState();
|
||||||
|
const commands = simAdapter.drainCommands();
|
||||||
|
|
||||||
|
// BLOCKER 3 — DO NOT seed lastTickAt with this.currentTick. lastTickAt
|
||||||
|
// is wall-clock ms owned by saveSync. The Garden scene's snapshot
|
||||||
|
// copies the value already in the store (which was hydrated from the
|
||||||
|
// save and has not been touched by the sim). tickCount is the sim's
|
||||||
|
// own counter and is read-through from the scene's local counter.
|
||||||
|
const simStateNow: SimState = {
|
||||||
|
garden: { tiles: storeState.tiles },
|
||||||
|
plants: [],
|
||||||
|
harvestedFragmentIds: storeState.harvestedFragmentIds,
|
||||||
|
lastTickAt: storeState.lastTickAt ?? 0,
|
||||||
|
tickCount: this.currentTick,
|
||||||
|
unlockedPlantTypes: storeState.unlockedPlantTypes,
|
||||||
|
luraBeatProgress: storeState.luraBeatProgress,
|
||||||
|
offlineEvents: null,
|
||||||
|
settings: {
|
||||||
|
musicVolume: 0.7,
|
||||||
|
ambientVolume: 0.5,
|
||||||
|
sfxVolume: 0.8,
|
||||||
|
persistenceToastShown: storeState.persistenceToastShown,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = drainTicks(simStateNow, this.accumulatorMs, (s, _dtMs, _silent) => {
|
||||||
|
const next = simulateOneTick(s, this.currentTick + 1, commands);
|
||||||
|
this.currentTick++;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
this.accumulatorMs = result.remainderMs;
|
||||||
|
|
||||||
|
if (result.ticksApplied > 0) {
|
||||||
|
simAdapter.applyTilesAndUnlocks(
|
||||||
|
result.state.garden.tiles,
|
||||||
|
result.state.unlockedPlantTypes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleTilePointerDown(idx: number): void {
|
||||||
|
const tiles = appStore.getState().tiles as Tile[];
|
||||||
|
const tile = tiles[idx];
|
||||||
|
if (!tile || !tile.plant) {
|
||||||
|
// Empty tile — emit event for the React seed picker.
|
||||||
|
const dom = tileCenterToDom(this, idx);
|
||||||
|
eventBus.emit('tile-clicked-coords', { tileIdx: idx, screenX: dom.x, screenY: dom.y });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Plan 02-03 wires harvest / compost on plant click.
|
||||||
|
}
|
||||||
|
|
||||||
|
private repaintPlants(tiles: Tile[]): void {
|
||||||
|
for (let idx = 0; idx < tiles.length; idx++) {
|
||||||
|
const tile = tiles[idx];
|
||||||
|
const stage = tile?.plant ? tileGrowthStage(tile, this.currentTick) : null;
|
||||||
|
const existing = this.plantObjs.get(idx);
|
||||||
|
|
||||||
|
if (!stage || !tile?.plant) {
|
||||||
|
if (existing) {
|
||||||
|
destroyPlant(existing);
|
||||||
|
this.plantObjs.delete(idx);
|
||||||
|
this.readyTweens.get(idx)?.stop();
|
||||||
|
this.readyTweens.delete(idx);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repaint if missing or stage changed.
|
||||||
|
if (!existing || existing.stage !== stage) {
|
||||||
|
if (existing) destroyPlant(existing);
|
||||||
|
const next = drawPlant(this, idx, tile, stage);
|
||||||
|
if (next) {
|
||||||
|
this.plantObjs.set(idx, next);
|
||||||
|
if (stage === 'ready') {
|
||||||
|
this.readyTweens.get(idx)?.stop();
|
||||||
|
this.readyTweens.set(idx, applyReadyPulse(this, next.shape));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.storeUnsubscribe?.();
|
||||||
|
this.storeUnsubscribe = null;
|
||||||
|
this.readyTweens.forEach((t) => t.stop());
|
||||||
|
this.readyTweens.clear();
|
||||||
|
this.plantObjs.forEach((p) => destroyPlant(p));
|
||||||
|
this.plantObjs.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Public barrel for src/render/garden/. Phaser scenes import from here.
|
||||||
|
*/
|
||||||
|
export { drawTiles } from './tile-renderer';
|
||||||
|
export type { TileGameObjects } from './tile-renderer';
|
||||||
|
export { drawPlant, destroyPlant } from './plant-renderer';
|
||||||
|
export type { PlantGameObject } from './plant-renderer';
|
||||||
|
export { applyReadyPulse } from './ready-pulse';
|
||||||
|
export { tileTopLeftCanvas, tileCenterCanvas, tileCenterToDom, GRID_LAYOUT } from './tile-coords';
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import * as Phaser from 'phaser';
|
||||||
|
import type { Tile, GrowthStage } from '../../sim/garden/types';
|
||||||
|
import { PLANT_TYPES } from '../../sim/garden/plants';
|
||||||
|
import { tileCenterCanvas, GRID_LAYOUT } from './tile-coords';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plant primitives per CONTEXT D-26.
|
||||||
|
* sprout = small dot (radius 6) near the tile bottom
|
||||||
|
* mature = stem rectangle (width 4, height 24) at tile center
|
||||||
|
* ready = bloom shape (filled circle, radius 18) at tile center
|
||||||
|
*
|
||||||
|
* Tinted by plant type (PLANT_TYPES[plantTypeId].tints[stage]).
|
||||||
|
* Phase 3 swaps in painted sprites without touching this signature.
|
||||||
|
*/
|
||||||
|
export interface PlantGameObject {
|
||||||
|
shape: Phaser.GameObjects.Shape;
|
||||||
|
stage: GrowthStage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawPlant(
|
||||||
|
scene: Phaser.Scene,
|
||||||
|
tileIdx: number,
|
||||||
|
tile: Tile,
|
||||||
|
stage: GrowthStage,
|
||||||
|
): PlantGameObject | null {
|
||||||
|
if (!tile.plant) return null;
|
||||||
|
const type = PLANT_TYPES[tile.plant.plantTypeId];
|
||||||
|
if (!type) return null;
|
||||||
|
const center = tileCenterCanvas(tileIdx);
|
||||||
|
const tint = type.tints[stage];
|
||||||
|
|
||||||
|
let shape: Phaser.GameObjects.Shape;
|
||||||
|
if (stage === 'sprout') {
|
||||||
|
shape = scene.add.circle(center.x, center.y + GRID_LAYOUT.tileSize / 4, 6, tint);
|
||||||
|
} else if (stage === 'mature') {
|
||||||
|
shape = scene.add.rectangle(center.x, center.y, 4, 24, tint);
|
||||||
|
} else {
|
||||||
|
shape = scene.add.circle(center.x, center.y, 18, tint);
|
||||||
|
}
|
||||||
|
return { shape, stage };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function destroyPlant(obj: PlantGameObject | null): void {
|
||||||
|
obj?.shape.destroy();
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import * as Phaser from 'phaser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtle alpha pulse on ready-stage plants. Per CONTEXT D-27. Phase 3
|
||||||
|
* paints over with a warmer light treatment.
|
||||||
|
*
|
||||||
|
* Returns the tween so the scene can stop it when the plant is harvested
|
||||||
|
* or the tile changes stage.
|
||||||
|
*/
|
||||||
|
export function applyReadyPulse(
|
||||||
|
scene: Phaser.Scene,
|
||||||
|
target: Phaser.GameObjects.GameObject,
|
||||||
|
): Phaser.Tweens.Tween {
|
||||||
|
return scene.tweens.add({
|
||||||
|
targets: target,
|
||||||
|
alpha: { from: 0.7, to: 1.0 },
|
||||||
|
duration: 1200,
|
||||||
|
ease: 'Sine.easeInOut',
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import * as Phaser from 'phaser';
|
||||||
|
import { GRID_COLS, GRID_SIZE } from '../../sim/garden/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4×4 garden layout in canvas pixel coordinates. Centered in the
|
||||||
|
* 1024×768 game area declared in src/game/main.ts.
|
||||||
|
*
|
||||||
|
* Tile size + spacing chosen so the grid sits comfortably with margins
|
||||||
|
* for Phase-3 watercolor frames. Phase 2 ships placeholder primitives
|
||||||
|
* inside these bounds.
|
||||||
|
*
|
||||||
|
* Math (canvas 1024×768; tileSize 96; tileGap 16):
|
||||||
|
* gridWidth = 4*96 + 3*16 = 432
|
||||||
|
* gridHeight = 4*96 + 3*16 = 432
|
||||||
|
* gridOriginX = (1024 - 432)/2 = 296
|
||||||
|
* gridOriginY = (768 - 432)/2 = 168
|
||||||
|
*/
|
||||||
|
export const GRID_LAYOUT = Object.freeze({
|
||||||
|
tileSize: 96,
|
||||||
|
tileGap: 16,
|
||||||
|
gridOriginX: 296,
|
||||||
|
gridOriginY: 168,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function tileTopLeftCanvas(idx: number): { x: number; y: number } {
|
||||||
|
if (idx < 0 || idx >= GRID_SIZE) throw new Error(`Bad tile idx: ${idx}`);
|
||||||
|
const row = Math.floor(idx / GRID_COLS);
|
||||||
|
const col = idx % GRID_COLS;
|
||||||
|
const x = GRID_LAYOUT.gridOriginX + col * (GRID_LAYOUT.tileSize + GRID_LAYOUT.tileGap);
|
||||||
|
const y = GRID_LAYOUT.gridOriginY + row * (GRID_LAYOUT.tileSize + GRID_LAYOUT.tileGap);
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tileCenterCanvas(idx: number): { x: number; y: number } {
|
||||||
|
const tl = tileTopLeftCanvas(idx);
|
||||||
|
return { x: tl.x + GRID_LAYOUT.tileSize / 2, y: tl.y + GRID_LAYOUT.tileSize / 2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a tile center from canvas pixel space to viewport DOM coordinates.
|
||||||
|
* The seed picker (DOM popover) uses this to mount itself in absolute-position
|
||||||
|
* over the canvas (RESEARCH Pattern 4 + Assumption A5).
|
||||||
|
*
|
||||||
|
* Phaser.Scale.FIT scales + letterboxes; we need the actual canvas DOMRect
|
||||||
|
* to translate canvas-space → CSS pixel space.
|
||||||
|
*/
|
||||||
|
export function tileCenterToDom(scene: Phaser.Scene, idx: number): { x: number; y: number } {
|
||||||
|
const center = tileCenterCanvas(idx);
|
||||||
|
const canvas = scene.game.canvas;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = rect.width / scene.game.scale.width;
|
||||||
|
const scaleY = rect.height / scene.game.scale.height;
|
||||||
|
return {
|
||||||
|
x: rect.left + center.x * scaleX,
|
||||||
|
y: rect.top + center.y * scaleY,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import * as Phaser from 'phaser';
|
||||||
|
import { GRID_SIZE } from '../../sim/garden/types';
|
||||||
|
import { tileTopLeftCanvas, GRID_LAYOUT } from './tile-coords';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty-tile look: faint outlined rounded rectangle with subtle hover.
|
||||||
|
* Per CONTEXT D-06; Phase 3 paints the watercolor treatment over this
|
||||||
|
* primitive without changing the function signature.
|
||||||
|
*/
|
||||||
|
const OUTLINE_COLOR = 0x4d4d52;
|
||||||
|
const OUTLINE_HOVER = 0x6e6e75;
|
||||||
|
const OUTLINE_ALPHA = 0.6;
|
||||||
|
|
||||||
|
export interface TileGameObjects {
|
||||||
|
/** Hit-area rectangle (interactive). */
|
||||||
|
hit: Phaser.GameObjects.Rectangle;
|
||||||
|
/** Outline graphic. */
|
||||||
|
outline: Phaser.GameObjects.Graphics;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawOutline(g: Phaser.GameObjects.Graphics, tlX: number, tlY: number, color: number): void {
|
||||||
|
g.clear();
|
||||||
|
g.lineStyle(2, color, OUTLINE_ALPHA);
|
||||||
|
g.strokeRoundedRect(tlX, tlY, GRID_LAYOUT.tileSize, GRID_LAYOUT.tileSize, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawTiles(scene: Phaser.Scene): TileGameObjects[] {
|
||||||
|
const tiles: TileGameObjects[] = [];
|
||||||
|
for (let i = 0; i < GRID_SIZE; i++) {
|
||||||
|
const tl = tileTopLeftCanvas(i);
|
||||||
|
const cx = tl.x + GRID_LAYOUT.tileSize / 2;
|
||||||
|
const cy = tl.y + GRID_LAYOUT.tileSize / 2;
|
||||||
|
|
||||||
|
const g = scene.add.graphics();
|
||||||
|
drawOutline(g, tl.x, tl.y, OUTLINE_COLOR);
|
||||||
|
|
||||||
|
// Hit rectangle (transparent, interactive).
|
||||||
|
const hit = scene.add.rectangle(cx, cy, GRID_LAYOUT.tileSize, GRID_LAYOUT.tileSize, 0xffffff, 0);
|
||||||
|
hit.setInteractive({ useHandCursor: true });
|
||||||
|
hit.on('pointerover', () => drawOutline(g, tl.x, tl.y, OUTLINE_HOVER));
|
||||||
|
hit.on('pointerout', () => drawOutline(g, tl.x, tl.y, OUTLINE_COLOR));
|
||||||
|
|
||||||
|
// Tag the hit object with its index for handler dispatch.
|
||||||
|
hit.setData('tileIdx', i);
|
||||||
|
|
||||||
|
tiles.push({ hit, outline: g });
|
||||||
|
}
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Top-level barrel for src/render/. App code imports from here.
|
||||||
|
*
|
||||||
|
* Per CORE-10: src/sim/** must NOT import from this module. The sim
|
||||||
|
* stays rendering-agnostic; the Phaser scene tree (src/game/**) is the
|
||||||
|
* only place sim + render meet.
|
||||||
|
*/
|
||||||
|
export * from './garden';
|
||||||
Reference in New Issue
Block a user