# ============================================================================ # fx.ludic — Fx.*: engine-owned transient effects. A game asks for a burst of # sparks or a floating number and the engine owns the rest: it ages and moves # them every Update, draws them every Render (after the sprites, before a game's # Overlay pass, through the camera / shake / clip chokepoints), and drops them # when they expire. Nothing is an entity, so nothing needs a component, a model, # a handler or a draw call. Deterministic: velocities and lifetimes come from the # seeded RNG. # # Fx.sparks(x, y, color, count) a burst of `count` sparks flying out of (x, y) # Fx.number(x, y, value, color) a number that floats up from (x, y) and fades # Fx.clear() drop every effect (a room change, a new run) # ============================================================================ const FX_MAX_SPARKS: int = 512 const FX_MAX_NUMBERS: int = 64 const FX_SPARK_LIFE: int = 18 # frames, plus up to FX_SPARK_LIFE_JITTER const FX_SPARK_LIFE_JITTER: int = 8 const FX_SPARK_SPEED: int = 3 # px per frame, each axis, either way const FX_NUMBER_LIFE: int = 30 # frames; a number rises one pixel every other frame export state RtFxState { fx_ready: bool = false fx_sx: words = null fx_sy: words = null fx_svx: words = null fx_svy: words = null fx_slife: words = null fx_scolor: words = null fx_ssize: words = null fx_sn: int = 0 fx_nx: words = null fx_ny: words = null fx_nvalue: words = null fx_nlife: words = null fx_ncolor: words = null fx_nn: int = 0 } function fx_init(rt_fx_st: mut RtFxState) -> void { if rt_fx_st.fx_ready { return } rt_fx_st.fx_sx = words(FX_MAX_SPARKS); rt_fx_st.fx_sy = words(FX_MAX_SPARKS) rt_fx_st.fx_svx = words(FX_MAX_SPARKS); rt_fx_st.fx_svy = words(FX_MAX_SPARKS) rt_fx_st.fx_slife = words(FX_MAX_SPARKS); rt_fx_st.fx_scolor = words(FX_MAX_SPARKS); rt_fx_st.fx_ssize = words(FX_MAX_SPARKS) rt_fx_st.fx_nx = words(FX_MAX_NUMBERS); rt_fx_st.fx_ny = words(FX_MAX_NUMBERS) rt_fx_st.fx_nvalue = words(FX_MAX_NUMBERS); rt_fx_st.fx_nlife = words(FX_MAX_NUMBERS); rt_fx_st.fx_ncolor = words(FX_MAX_NUMBERS) rt_fx_st.fx_ready = true } function fx_sparks(rt_fx_st: mut RtFxState, rt_rng_st: mut RtRngState, x: int, y: int, color: int, count: int) -> void { fx_init(rt_fx_st) var i = 0 while i < count { if rt_fx_st.fx_sn >= FX_MAX_SPARKS { return } rt_fx_st.fx_sx[rt_fx_st.fx_sn] = x rt_fx_st.fx_sy[rt_fx_st.fx_sn] = y rt_fx_st.fx_svx[rt_fx_st.fx_sn] = rt_rng_range(rt_rng_st, -FX_SPARK_SPEED, FX_SPARK_SPEED) rt_fx_st.fx_svy[rt_fx_st.fx_sn] = rt_rng_range(rt_rng_st, -FX_SPARK_SPEED, FX_SPARK_SPEED) rt_fx_st.fx_slife[rt_fx_st.fx_sn] = FX_SPARK_LIFE + rt_rng_range(rt_rng_st, 0, FX_SPARK_LIFE_JITTER) rt_fx_st.fx_scolor[rt_fx_st.fx_sn] = color rt_fx_st.fx_ssize[rt_fx_st.fx_sn] = 1 + rt_rng_range(rt_rng_st, 0, 2) rt_fx_st.fx_sn += 1 i += 1 } } function fx_number(rt_fx_st: mut RtFxState, x: int, y: int, value: int, color: int) -> void { fx_init(rt_fx_st) if rt_fx_st.fx_nn >= FX_MAX_NUMBERS { return } rt_fx_st.fx_nx[rt_fx_st.fx_nn] = x rt_fx_st.fx_ny[rt_fx_st.fx_nn] = y rt_fx_st.fx_nvalue[rt_fx_st.fx_nn] = value rt_fx_st.fx_nlife[rt_fx_st.fx_nn] = FX_NUMBER_LIFE rt_fx_st.fx_ncolor[rt_fx_st.fx_nn] = color rt_fx_st.fx_nn += 1 } function fx_clear(rt_fx_st: mut RtFxState) -> void { rt_fx_st.fx_sn = 0; rt_fx_st.fx_nn = 0 } # drop spark i by moving the last one into its slot function fx_drop_spark(rt_fx_st: mut RtFxState, i: int) -> void { let last = rt_fx_st.fx_sn - 1 rt_fx_st.fx_sx[i] = rt_fx_st.fx_sx[last]; rt_fx_st.fx_sy[i] = rt_fx_st.fx_sy[last] rt_fx_st.fx_svx[i] = rt_fx_st.fx_svx[last]; rt_fx_st.fx_svy[i] = rt_fx_st.fx_svy[last] rt_fx_st.fx_slife[i] = rt_fx_st.fx_slife[last]; rt_fx_st.fx_scolor[i] = rt_fx_st.fx_scolor[last]; rt_fx_st.fx_ssize[i] = rt_fx_st.fx_ssize[last] rt_fx_st.fx_sn = last } function fx_drop_number(rt_fx_st: mut RtFxState, i: int) -> void { let last = rt_fx_st.fx_nn - 1 rt_fx_st.fx_nx[i] = rt_fx_st.fx_nx[last]; rt_fx_st.fx_ny[i] = rt_fx_st.fx_ny[last] rt_fx_st.fx_nvalue[i] = rt_fx_st.fx_nvalue[last]; rt_fx_st.fx_nlife[i] = rt_fx_st.fx_nlife[last]; rt_fx_st.fx_ncolor[i] = rt_fx_st.fx_ncolor[last] rt_fx_st.fx_nn = last } # once per Update: move and age everything, dropping what expired function fx_tick(rt_fx_st: mut RtFxState) -> void { if not rt_fx_st.fx_ready { return } var i = 0 while i < rt_fx_st.fx_sn { rt_fx_st.fx_sx[i] += rt_fx_st.fx_svx[i] rt_fx_st.fx_sy[i] += rt_fx_st.fx_svy[i] rt_fx_st.fx_slife[i] -= 1 if rt_fx_st.fx_slife[i] <= 0 { fx_drop_spark(rt_fx_st, i) } else { i += 1 } } i = 0 while i < rt_fx_st.fx_nn { rt_fx_st.fx_nlife[i] -= 1 if rt_fx_st.fx_nlife[i] % 2 == 0 { rt_fx_st.fx_ny[i] -= 1 } if rt_fx_st.fx_nlife[i] <= 0 { fx_drop_number(rt_fx_st, i) } else { i += 1 } } } # once per Render, after the sprites: everything goes through the camera chokepoints function fx_draw(rt_core_st: mut RtCoreState, rt_fx_st: RtFxState) -> void { if not rt_fx_st.fx_ready { return } var i = 0 while i < rt_fx_st.fx_sn { rt_fill_rect(rt_core_st, rt_fx_st.fx_sx[i], rt_fx_st.fx_sy[i], rt_fx_st.fx_ssize[i], rt_fx_st.fx_ssize[i], rt_fx_st.fx_scolor[i]) i += 1 } i = 0 while i < rt_fx_st.fx_nn { rt_text_int(rt_core_st, rt_fx_st.fx_nx[i] - 4, rt_fx_st.fx_ny[i] - 10, rt_fx_st.fx_nvalue[i], rt_fx_st.fx_ncolor[i], 1) i += 1 } }