#125: slow the prologue down, and give the breach a real black hole
Five things, all of them "this happens too fast or looks too thin". PACING. Every beat had a completion test that could be satisfied while the guide was still mid-sentence, which is why everything landed seconds after the thing before it. Beats now have a FLOOR as well as a deadline - a minimum length - and a beat whose point is the guide talking will not end while a line is still on screen. One rule in one place rather than tuning each condition. The run is 3400px instead of 1500, which at cruise was about four seconds, and a held line is 6.5s instead of 4.5. THE CONVOY. Escorts at the four corners of a box, holds packed in the middle of it, roughly three times the spread of the fleet's own wedge - this is freight under guard, not a fighting formation. CONVOY and CONVOY_FORMATION pair index-for-index, which is all it takes for spawn_ships to seat the guns on the outside, and the test pins that every hold sits further in than the furthest-out escort. THE DIALOG. Cut corners, brackets holding the short edges open, a tick strip along the bottom and a caret in the corner - geometry rather than art, so it scales with the banner and costs nothing. THE BREACH. It was a few lines turning around a point. It is now two halves driven by one number: DimensionalBreach, a growing black hole out in the world built on BlackHoleVisual's idiom - empty core, counter-rotating arc segments hotter toward the centre - with infalling streamers it does not have, because a planet's disc is steady and this one has to say "now"; and breach.gdshader over the top, bending the ACTUAL SCENE around it. Ships, rocks and the starfield all get dragged in, which is most of what makes a hole in space read as a hole rather than as a decal drawn on one. Inside the horizon the shader samples nothing at all - the same decision BlackHoleVisual makes, for the same reason: a black hole with something visible in the middle stops being one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7d344b2a0a
commit
d455361328
8 changed files with 527 additions and 57 deletions
43
ui/BreachOverlay.gd
Normal file
43
ui/BreachOverlay.gd
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
extends ColorRect
|
||||
|
||||
# #125: drives breach.gdshader.
|
||||
#
|
||||
# Lives in the UI layer because it works in SCREEN space - it bends the
|
||||
# rendered frame, so it has to be the last thing drawn and it has to cover
|
||||
# the whole viewport. The world-space half of the breach (the horizon, the
|
||||
# accretion disc, the infalling matter) is DimensionalBreach, a Node2D out
|
||||
# in the world; this is only the lens over the top of it.
|
||||
#
|
||||
# Inert unless something calls set_breach(): the shader is a no-op at zero
|
||||
# strength, but a full-screen quad that is never needed is still a full
|
||||
# screen of fragment work, so it hides itself instead.
|
||||
|
||||
const SHADER_PATH: String = "res://shaders/breach.gdshader"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
visible = false
|
||||
if material == null:
|
||||
var shader_material := ShaderMaterial.new()
|
||||
shader_material.shader = load(SHADER_PATH)
|
||||
material = shader_material
|
||||
|
||||
|
||||
func clear_breach() -> void:
|
||||
visible = false
|
||||
if material != null:
|
||||
material.set_shader_parameter("strength", 0.0)
|
||||
|
||||
|
||||
# `at` is in SCREEN pixels; the director projects the world point through the
|
||||
# camera before calling, because only it knows where the camera is.
|
||||
func set_breach(at: Vector2, horizon: float, lens: float, breach_strength: float) -> void:
|
||||
if material == null:
|
||||
return
|
||||
visible = breach_strength > 0.001
|
||||
material.set_shader_parameter("breach_center", at)
|
||||
material.set_shader_parameter("viewport_size", size)
|
||||
material.set_shader_parameter("horizon_radius", horizon)
|
||||
material.set_shader_parameter("lens_radius", maxf(lens, horizon + 1.0))
|
||||
material.set_shader_parameter("strength", clampf(breach_strength, 0.0, 1.0))
|
||||
|
|
@ -10,10 +10,22 @@ extends Control
|
|||
# Lines queue rather than replacing each other, so a beat that fires while
|
||||
# the previous line is still up does not silently eat it.
|
||||
|
||||
const HOLD_SECONDS: float = 4.5
|
||||
# #125: lines were gone before they had been read. A held line is the
|
||||
# cheapest possible pacing control and the prologue leans on it heavily.
|
||||
const HOLD_SECONDS: float = 6.5
|
||||
const FADE_SECONDS: float = 0.5
|
||||
const TEXT_COLOR: Color = Color(0.82, 0.92, 1.0)
|
||||
const FRAME_COLOR: Color = Color(0.45, 0.68, 0.95)
|
||||
# #125: the panel was a plain rectangle with a line under it. These are the
|
||||
# pieces that make it read as a transmission window instead - the corners
|
||||
# are cut, the short edges are brackets rather than sides, and there is a
|
||||
# tick strip along the bottom. All of it is geometry rather than art, so it
|
||||
# scales with the banner and costs nothing.
|
||||
const CUT: float = 26.0
|
||||
const BRACKET: float = 34.0
|
||||
const INSET: float = 7.0
|
||||
const TICK_COUNT: int = 24
|
||||
const TICK_HEIGHT: float = 5.0
|
||||
|
||||
var label: Label
|
||||
var queued: Array = []
|
||||
|
|
@ -97,14 +109,54 @@ func _process(delta: float) -> void:
|
|||
_advance()
|
||||
|
||||
|
||||
# The panel's outline: a rectangle with its corners cut off. Returned rather
|
||||
# than drawn so the fill and the stroke are guaranteed to be the same shape.
|
||||
func _frame() -> PackedVector2Array:
|
||||
var w: float = size.x
|
||||
var h: float = size.y
|
||||
return PackedVector2Array([
|
||||
Vector2(CUT, 0.0), Vector2(w - CUT, 0.0), Vector2(w, CUT),
|
||||
Vector2(w, h - CUT), Vector2(w - CUT, h), Vector2(CUT, h),
|
||||
Vector2(0.0, h - CUT), Vector2(0.0, CUT),
|
||||
])
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if current == "":
|
||||
return
|
||||
# Fades out over its last moments so lines hand over rather than cutting.
|
||||
var alpha: float = clampf(time_left / FADE_SECONDS, 0.0, 1.0)
|
||||
label.modulate.a = alpha
|
||||
draw_rect(Rect2(Vector2.ZERO, size), Color(0.02, 0.05, 0.12, 0.72 * alpha))
|
||||
NeonDraw.draw_glowing_line(self, Vector2(0.0, size.y), Vector2(size.x, size.y),
|
||||
Color(FRAME_COLOR, alpha), 0.7, 0.55)
|
||||
NeonDraw.draw_glowing_line(self, Vector2(0.0, 0.0), Vector2(size.x, 0.0),
|
||||
Color(FRAME_COLOR, alpha * 0.6), 0.5, 0.4)
|
||||
var frame: PackedVector2Array = _frame()
|
||||
|
||||
draw_colored_polygon(frame, Color(0.02, 0.05, 0.12, 0.78 * alpha))
|
||||
NeonDraw.draw_glowing_polygon(self, frame, Color(FRAME_COLOR, alpha), 0.8)
|
||||
|
||||
# Brackets at the short edges: a doubled inner line that stops well short
|
||||
# of the corners, so the panel reads as held open rather than closed.
|
||||
for side in [0.0, 1.0]:
|
||||
var x: float = lerpf(INSET, size.x - INSET, side)
|
||||
NeonDraw.draw_glowing_line(self, Vector2(x, CUT + INSET),
|
||||
Vector2(x, size.y - CUT - INSET), Color(FRAME_COLOR, alpha * 0.75), 0.6, 0.3)
|
||||
var arm: float = BRACKET * (1.0 if side < 0.5 else -1.0)
|
||||
for y in [CUT + INSET, size.y - CUT - INSET]:
|
||||
NeonDraw.draw_glowing_line(self, Vector2(x, y), Vector2(x + arm, y),
|
||||
Color(FRAME_COLOR, alpha * 0.75), 0.6, 0.3)
|
||||
|
||||
# A tick strip along the bottom, dimming toward the ends. It is the one
|
||||
# piece that is pure texture, and it is what stops the lower edge from
|
||||
# reading as a plain underline.
|
||||
var span: float = size.x - CUT * 2.0
|
||||
for i in range(TICK_COUNT):
|
||||
var t: float = float(i) / float(TICK_COUNT - 1)
|
||||
var tx: float = CUT + span * t
|
||||
var lit: float = 0.25 + 0.55 * sin(t * PI)
|
||||
draw_line(Vector2(tx, size.y - INSET), Vector2(tx, size.y - INSET - TICK_HEIGHT),
|
||||
Color(FRAME_COLOR, alpha * lit), 2.0, true)
|
||||
|
||||
# And a caret in the top-left corner, so a line reads as being SAID by
|
||||
# someone rather than printed on the glass.
|
||||
NeonDraw.draw_glowing_path(self, PackedVector2Array([
|
||||
Vector2(CUT + 4.0, INSET + 8.0), Vector2(CUT + 16.0, INSET + 15.0),
|
||||
Vector2(CUT + 4.0, INSET + 22.0),
|
||||
]), Color(0.72, 0.92, 1.0, alpha * 0.9), 0.9, 0.4)
|
||||
|
|
|
|||
Loading…
Reference in a new issue