Files
m3te/main.sx
swipelab c2548aa854 P6.2: score popups & match FX (sx, iOS sim)
Add a purely-visual, transient juice layer over a committed move — score
popups + a tinted particle/flash burst at the clears — with no change to the
model, score, moves, or settled board.

- assets/fx/particle.png: key the painted transparency checkerboard out of the
  provided particle art to real alpha (8-connected border flood fill +
  smooth luminance falloff that preserves the soft glow), downscaled to a
  256x256 RGBA white sparkle. tools/key_particle.sx is the reproducible tool.
- board_fx.sx: BoardFx (live particle bursts + one "+points" popup) and
  BoardFxAssets. The engine image path samples texture*white (no draw-time
  tint), so the white sprite is tinted per gem colour at LOAD time into one
  texture per colour; a burst animates by scale (grow -> shrink) and the soft
  texture edges carry the fade. Combos (cascade depth > 1) burst bigger and the
  popup is larger + gold. All driven by delta_time and self-pruning.
- board_anim.sx: AnimMove carries the model's cascade.awarded so the popup
  shows the real payout without re-deriving any scoring in the view.
- board_view.sx / main.sx: wire BoardFx + the tinted assets, tick each frame,
  spawn on a legal commit, and render bursts (clipped to the grid) under the
  popups (drawn on top). Input-lock (BoardAnim.active) is untouched; FX never
  gate input and may outlast the move slightly before vanishing.

Goldens (iPhone-17-class sim, iOS 26): p6_fx.png (combo: gold "+480" + bursts
mid-cascade), p6_fx_match.png (single match: "+30" + red burst), p6_fx_after.png
(settled board, FX fully gone). Gate: ios-sim build links, 15/15 logic tests
green (scoring/cascade goldens unchanged).
2026-06-05 02:18:55 +03:00

191 lines
7.1 KiB
Plaintext

#import "modules/std.sx";
#import "build.sx";
#import "modules/compiler.sx";
#import "modules/opengl.sx";
#import "modules/sdl3.sx";
#import "modules/math";
#import "modules/stb.sx";
#import "modules/stb_truetype.sx";
#import "modules/gpu/api.sx";
#import "modules/gpu/types.sx";
#import "modules/gpu/metal.sx";
#import "modules/ui";
#import "modules/platform/api.sx";
#import "modules/platform/sdl3.sx";
#import "modules/platform/uikit.sx";
#import "board.sx";
#import "board_view.sx";
#import "board_anim.sx";
#import "board_fx.sx";
#run configure_build();
// Fixed seed for the rendered board — the same seed tests/board_init.sx locks
// as a snapshot, so the on-screen layout matches that golden gem-for-gem.
BOARD_SEED :: 1337;
g_plat : Platform = ---;
g_pipeline : *UIPipeline = ---;
g_delta_time : f32 = 0.016;
g_viewport_w : f32 = 800.0;
g_viewport_h : f32 = 600.0;
g_safe_insets : EdgeInsets = .{};
// iOS-only concrete handles kept alongside the boxed `g_plat` so the frame loop
// can reach the CAMetalLayer pointer / pixel dims without going through the
// protocol box.
g_uikit_plat : *UIKitPlatform = null;
g_metal_gpu : *MetalGPU = null;
// The pure-sx model (board.sx) and its sprites, seeded once in main() and
// rendered every frame. Heap-allocated so the view holds stable pointers to
// the mutable state across frames.
g_board : *Board = null;
g_assets : *BoardAssets = null;
// Current cell selection (P4.4). Heap-allocated so it survives BoardView's
// per-frame rebuild; a tap hit-tests a cell and toggles this.
g_sel : *BoardSelection = null;
// In-progress touch drag (P5.2). Heap-allocated for the same reason: the press
// and release that bracket a swipe land on different per-frame BoardView values,
// so the drag start must persist between them.
g_drag : *DragInput = null;
// In-flight move animation (P6.1). Heap-allocated for the same reason: a swipe
// begins the swap/clear/fall timeline, which then plays out over many subsequent
// frames, so the timeline state must persist across BoardView's per-frame rebuild.
g_anim : *BoardAnim = null;
// Transient match FX + score popups (P6.2). Heap-allocated like the animation:
// a committed move spawns short-lived bursts/popups that play out (and prune
// themselves) over many later frames. `g_fxassets` holds the per-colour tinted
// particle textures, loaded once. Purely visual; neither gates input.
g_fx : *BoardFx = null;
g_fxassets : *BoardFxAssets = null;
// Rebuilt each frame inside the pipeline's arena; carries the current safe-area
// insets so the grid stays inside the notch / home-indicator region.
build_ui :: () -> View {
BoardView.{ board = g_board, assets = g_assets, sel = g_sel, drag = g_drag, anim = g_anim, fx = g_fx, fxassets = g_fxassets, safe = g_safe_insets }
}
frame :: () {
fc := g_plat.begin_frame();
g_delta_time = fc.delta_time;
g_viewport_w = fc.viewport_w;
g_viewport_h = fc.viewport_h;
g_safe_insets = g_plat.safe_insets();
if fc.viewport_w != g_pipeline.screen_width or fc.viewport_h != g_pipeline.screen_height {
g_pipeline.resize(fc.viewport_w, fc.viewport_h);
}
for g_plat.poll_events(): (*ev) {
inline if OS != .ios {
if ev == {
case .key_up: (e) {
if e.key == .escape { g_plat.stop(); }
}
}
}
g_pipeline.dispatch_event(ev);
}
// Advance the in-flight move animation by this frame's delta before rendering,
// so the board view draws the timeline slice for the current wall-clock time.
if g_anim != null { g_anim.tick(g_delta_time); }
if g_fx != null { g_fx.tick(g_delta_time); }
inline if OS == .ios {
// Lazy-attach Metal once -[SxAppDelegate didFinishLaunching:] has
// installed the SxMetalView and its bounds have been measured; both can
// lag the first CADisplayLink tick, and a zero-sized drawable aborts
// via XPC.
if g_uikit_plat.gl_layer == null { return; }
if g_uikit_plat.pixel_w <= 0 or g_uikit_plat.pixel_h <= 0 { return; }
if g_metal_gpu.layer == null {
g_metal_gpu.init(g_uikit_plat.gl_layer, g_uikit_plat.pixel_w, g_uikit_plat.pixel_h);
} else if g_metal_gpu.pixel_w != g_uikit_plat.pixel_w or g_metal_gpu.pixel_h != g_uikit_plat.pixel_h {
g_metal_gpu.resize(g_uikit_plat.pixel_w, g_uikit_plat.pixel_h);
}
clear : ClearColor = .{ r = 0.05, g = 0.06, b = 0.10, a = 1.0 };
if !g_metal_gpu.begin_frame(clear) { return; }
g_pipeline.tick();
g_metal_gpu.end_frame(fc.target_present_time);
} else {
glViewport(0, 0, fc.pixel_w, fc.pixel_h);
glClearColor(0.05, 0.06, 0.10, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
g_pipeline.tick();
}
g_plat.end_frame();
}
main :: () -> void {
inline if OS == .ios {
u : *UIKitPlatform = xx context.allocator.alloc(size_of(UIKitPlatform));
u.gpu_mode = .metal;
if !u.init("m3te", 800, 600) { return; }
g_plat = xx u;
g_uikit_plat = u;
// The CAMetalLayer doesn't exist until didFinishLaunching: runs after we
// return into UIApplicationMain, so attach lazily on the first frame.
// init(null, 0, 0) only needs the MTLDevice, which is enough for the
// texture uploads below.
g_metal_gpu = xx context.allocator.alloc(size_of(MetalGPU));
// alloc returns uninitialized memory; struct field defaults are NOT
// applied, so List caps/lens would be garbage without this memset.
memset(xx g_metal_gpu, 0, size_of(MetalGPU));
if !g_metal_gpu.init(null, 0, 0) { return; }
} else {
s : *SdlPlatform = xx context.allocator.alloc(size_of(SdlPlatform));
if !s.init("m3te", 800, 600) { return; }
g_plat = xx s;
}
fc := g_plat.begin_frame();
g_viewport_w = fc.viewport_w;
g_viewport_h = fc.viewport_h;
g_safe_insets = g_plat.safe_insets();
g_pipeline = xx context.allocator.alloc(size_of(UIPipeline));
// Same alloc caveat as above: zero so the optional `gpu` reads as null on
// the desktop path (where set_gpu is not called) and the Lists start empty.
memset(xx g_pipeline, 0, size_of(UIPipeline));
inline if OS == .ios {
g_pipeline.set_gpu(xx g_metal_gpu);
}
g_pipeline.init(fc.viewport_w, fc.viewport_h);
g_pipeline.init_font("assets/fonts/default.ttf", 32.0, fc.dpi_scale);
g_board = xx context.allocator.alloc(size_of(Board));
g_board.init(BOARD_SEED);
g_assets = xx context.allocator.alloc(size_of(BoardAssets));
g_assets.init();
g_assets.load(g_pipeline.gpu);
g_sel = xx context.allocator.alloc(size_of(BoardSelection));
g_sel.init();
g_drag = xx context.allocator.alloc(size_of(DragInput));
g_drag.init();
g_anim = xx context.allocator.alloc(size_of(BoardAnim));
g_anim.init();
g_fx = xx context.allocator.alloc(size_of(BoardFx));
g_fx.init();
g_fxassets = xx context.allocator.alloc(size_of(BoardFxAssets));
g_fxassets.init();
g_fxassets.load(g_pipeline.gpu);
g_pipeline.set_body(closure(build_ui));
g_plat.run_frame_loop(closure(frame));
g_plat.shutdown();
}