Three bugs that could produce unsolvable puzzles:
1. Feature placement fallback used leaf_center without duplicate checking,
allowing treasures to spawn on the same cell as buttons in dense rooms
(level 2 puts 8 features in one room). Fixed with exhaustive cell scan
fallback.
2. Solvability checker ignored treasure entities entirely. Boulders cannot
be pushed through treasures (TreasureEntity.bump returns False for
non-player entities), so the solver now models them as boulder-blocking
obstacles while allowing player movement through them.
3. Added explicit button-blocked-by-obstacle check before running the
full BFS solver, catching the most common failure mode early.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces jam-quality code with production engine features (addresses #248):
- cos_level.py: Replace custom BinaryRoomNode/RoomGraph with mcrfpy.BSP
for room generation, using adjacency graph for corridor placement
- cos_solver.py: New Sokoban BFS solvability checker; levels retry up
to 10 times if unsolvable
- game.py: Add ColorLayer fog of war with room-reveal mechanic
(visible/discovered/unknown states), compute_fov per player move
- cos_entities.py: Enemy AI state machine (idle/aggressive/fleeing)
with A* pathfinding, fix duplicate direction bug on line 428/444
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full screen "wasm-game" for viewport compatibility between desktop and
web interfaces. Viewport modes ("fit", "center", and "stretch") should
now work the same way under WASM/SDL and SFML. This should also enable
android or web-for-mobile aspect ratios to be supported more easily.
Replace no-op audio stubs in SDL2Types.h with real SDL2_mixer-backed
implementations of SoundBuffer, Sound, and Music. This enables audio
playback in the browser with zero changes to Python bindings.
- Add -sUSE_SDL_MIXER=2 to Emscripten compile/link flags (CMakeLists.txt)
- Initialize Mix_OpenAudio in SDL2Renderer::init(), Mix_CloseAudio in shutdown()
- SoundBuffer: Mix_LoadWAV/Mix_LoadWAV_RW with duration computation
- Sound: channel-based playback with Mix_ChannelFinished tracking
- Music: global channel streaming via Mix_LoadMUS/Mix_PlayMusic
- Volume conversion: SFML 0-100 scale to SDL_mixer 0-128 scale
Known limitations on web: Music.duration and Music.position getters
return 0 (SDL_mixer 2.0.2 lacks Mix_MusicDuration/Mix_GetMusicPosition).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaced the NotImplementedError stub with a full animation
implementation. Entity3D now supports animating: x, y, z,
world_x, world_y, world_z, rotation, rot_y, scale, scale_x,
scale_y, scale_z, sprite_index, visible.
Added Entity3D as a third target type in the Animation system
(alongside UIDrawable and UIEntity), with startEntity3D(),
applyValue(Entity3D*), and proper callback support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EntityCollection3D now has API parity with UIEntityCollection:
- pop(index=-1): Remove and return entity at index
- find(name): Search by entity name, return Entity3D or None
- extend(iterable): Append multiple Entity3D objects
Also adds `name` property to Entity3D for use with find().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
screen_to_world() previously only intersected the Y=0 plane.
Now accepts an optional y_plane parameter (default 0.0) for
intersecting arbitrary horizontal planes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously enableRenderTexture() silently failed. Now emits a
stderr warning with the requested dimensions, consistent with
the engine's logging pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The root cause was PyViewport3DType being declared `static` in
Viewport3D.h, creating per-translation-unit copies. Entity3D.cpp's
copy was never passed through PyType_Ready, causing segfaults when
tp_alloc was called.
Changed `static` to `inline` (matching PyEntity3DType and
PyModel3DType patterns), and implemented get_viewport using the
standard type->tp_alloc pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add -sUSE_FREETYPE=1 to Emscripten build flags
- Extend Font class with FT_Library, FT_Face, and FT_Stroker handles
- Rewrite FontAtlas to use FreeType with on-demand stroked glyph loading
- Text outlines now use FT_Stroker for vector-based stroking before
rasterization, eliminating gaps at corners with thick outlines
- Use glTexSubImage2D for incremental atlas updates (major perf fix)
- Disable canvas border in shell.html per Emscripten docs (alignment fix)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- src/shell.html: Custom HTML shell with crisp pixel CSS
(image-rendering: pixelated) and zoom prevention on canvas
- src/emscripten_pre.js: Patches browser quirks that cause crashes:
- Intercepts resize/scroll events to ensure e.detail is always 0
- Wraps window properties (innerWidth, outerWidth, etc.) to
always return integers, fixing browser zoom crashes
- CMakeLists.txt: Output as .html, include shell and pre-js files
The pre-JS fix addresses "attempt to write non-integer (undefined)
into integer heap" errors that occurred when users zoomed the browser
via Ctrl+scroll or browser menu.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements SFML-compatible text outlines using multi-pass rendering:
- Draws text 8 times at offset positions (N, NE, E, SE, S, SW, W, NW)
with outline color, then draws fill text on top
- Uses existing outlineThickness_ and outlineColor_ properties
- Refactored Text::draw() with helper lambda for code reuse
- Removed debug logging code
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The FontAtlas class was missing move semantics, causing the GPU texture
to be deleted immediately after creation. When FontAtlas was moved into
the cache with std::move(), the default move constructor copied textureId_,
then the original's destructor deleted the texture the cache was using.
Added:
- Move constructor that transfers ownership and clears source textureId_
- Move assignment operator with proper cleanup of existing resources
- Deleted copy operations since GPU textures can't be shared
Also cleaned up the text fragment shader to use proper alpha sampling.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>