class_name TelegraphedStrikeEmitter extends BulletEmitter ## Marks spots in the arena, then fills them with bullets a moment later. ## ## The whole point is the moment in between. A burst that appears at your feet ## with no warning is not a pattern, it is a coin flip; the same burst with a ## second of notice is a question about where you would rather be standing. ## [member warn_ticks] is that second, and the client draws the marker from the ## TELEGRAPH event this emits. ## ## Stateless, like every other emitter -- emitters are shared resources and two ## bosses of the same kind must not stomp each other's timers. The strike ## positions are therefore *derived* from the volley number rather than rolled ## and remembered, so the announcement and the strike compute the same points ## without anything being stored between them. ## Spots marked per volley. @export var strikes: int = 3 ## Ticks between the marker appearing and the burst landing. Must be shorter ## than [member interval], or a volley would land after the next was announced. @export var warn_ticks: int = 75 ## Bullets in each burst, fired outwards from the marked spot. @export var burst_count: int = 12 ## Radius of the marked area, and the ring the burst is born on. @export var blast_radius: float = 64.0 ## Keeps strikes off the arena's walls, where half the burst would be wasted. @export var margin: float = 72.0 ## Distinguishes two strike emitters in one phase, which would otherwise derive ## the same points and stack every burst on top of itself. @export var pattern_seed: int = 0 ## Armed on two ticks per volley: the announcement, and the strike itself. func should_fire(local_tick: int) -> bool: if not is_armed(local_tick) or interval <= 0: return false var into := posmod(local_tick - start_tick, interval) return into == 0 or into == posmod(warn_ticks, interval) func fire(ctx: EmitContext) -> void: if strikes <= 0 or interval <= 0: return var offset := ctx.local_tick - start_tick var volley := int(floor(float(offset) / float(interval))) var announcing := posmod(offset, interval) == 0 for i in strikes: var at := strike_point(volley, i, ctx.bounds) if announcing: ctx.telegraph(at, blast_radius, warn_ticks) else: _burst(ctx, at) ## The burst leaves the marked spot outwards, so standing on the marker is the ## one place that is certainly wrong and the edge is survivable. func _burst(ctx: EmitContext, at: Vector2) -> void: if burst_count <= 0: return for i in burst_count: var a := TAU * (float(i) / float(burst_count)) # From the marker, not from the boss: this burst is born where the # player was warned it would be, and a pillar between the boss and the # spot has nothing to say about it. emit_shot_from(ctx, a, at + Vector2.RIGHT.rotated(a) * (blast_radius * 0.25), at) ## Where volley [param volley]'s strike [param index] lands, inside ## [param bounds]. A pure function of its arguments: called once when the ## warning goes up and again when the burst lands, and it has to agree with ## itself both times without anything being remembered in between. func strike_point(volley: int, index: int, bounds: Rect2) -> Vector2: var inner := bounds.grow(-margin) if inner.size.x <= 0.0 or inner.size.y <= 0.0: inner = bounds return inner.position + Vector2( _unit(volley, index * 2) * inner.size.x, _unit(volley, index * 2 + 1) * inner.size.y) ## A small integer hash in [0, 1). Not a good random number generator and does ## not need to be -- it needs to be scattered, and it needs to give the same ## answer twice. func _unit(a: int, b: int) -> float: var h := (a * 374761393 + b * 668265263 + pattern_seed * 2246822519) & 0x7FFFFFFF h = ((h ^ (h >> 13)) * 1274126177) & 0x7FFFFFFF return float((h >> 7) & 0xFFFF) / 65536.0