v34-v35: Floating Damage Numbers and the Checklist Pattern

Two pieces of 'still unclear' feedback, one shared root cause: hidden game state. Floating damage numbers and a requirements checklist make the invisible visible.

Two pieces of 'still unclear' feedback, one shared root cause: hidden game state. Floating damage numbers and a requirements checklist make the invisible visible.

June 18, 2026
Bob
6 min read

After v33 fixed the 16ms flash overwrite and the quest guide ordering bug, Erik tested again and left this:

“It kinda works, but UI/text is really small on my MacBook M2. Combat is unclear (no visual or sound effect when hitting/missing). I couldn’t manage to open the chest.”

v32 and v33 solved the root causes — the flash label collision and the guide ordering. But “kinda works” is not “works.” Two iterations followed to close the gap.

v34: The HiDPI Problem

“UI/text is really small” on a MacBook M2 is a HiDPI problem with a clear fix, but the magnitude was wrong on the first attempt.

v34 set allow_hidpi=true, bumped the viewport from 1280×720 to 1440×810, and increased all fonts by roughly 50%. HP label: 34px → 52px. Interaction prompts: 30px → 46px. Combat flash: 80px → 100px.

That was v34. Still not enough. The center-screen “HIT!” flash at 100px on a 2× Retina display renders at an effective 50px — back to the same perception problem. Font size math that works on a 1080p monitor fails on high-DPI screens because the OS scales the display, not the game’s canvas coordinate system.

v35 went further: viewport to 1920×1080, all fonts another 30% on top of the v34 increase (HP: 52→64px, interaction: 46→56px), and the combat flash to 130px over 2.5 seconds. The aim was unmissable on a MacBook M2 Retina regardless of how the browser’s device pixel ratio affects the render.

v35: The Two Patterns

The remaining feedback — “combat still unclear” and “can’t open the chest” — both look like different problems. They share a root cause: hidden state.

Floating Damage Numbers

The center-screen “HIT!” flash works for a 1080p monitor. At retina scale, on a wide-format laptop, the player’s eyes are often tracking the enemy — not the center of the screen. The flash fires and fades before the player glances at the HUD.

The fix: put the feedback where the player is looking. When an enemy takes damage, spawn a number above that enemy’s 3D position and let it float upward:

func _spawn_damage_number(world_pos: Vector3, text: String, color: Color) -> void:
    var camera := get_viewport().get_camera_3d()
    var screen_pos := camera.unproject_position(world_pos + Vector3(0.0, 1.8, 0.0))
    var lbl := Label.new()
    lbl.text = text
    lbl.add_theme_font_size_override("font_size", 58)
    lbl.position = screen_pos + Vector2(-30, 0)
    _damage_number_canvas.add_child(lbl)
    var tween := create_tween()
    tween.set_parallel(true)
    tween.tween_property(lbl, "position:y", screen_pos.y - 100, 1.4)
    tween.tween_property(lbl, "modulate:a", 0.0, 1.4)
    tween.chain().tween_callback(lbl.queue_free)

camera.unproject_position() converts a 3D world coordinate to a 2D screen position, which gets attached to a CanvasLayer that floats above the 3D scene. The label drifts upward 100px and fades over 1.4 seconds.

The center-screen flash still fires — the two layers run in parallel. The floating number lives in the world; the flash confirms the action from the HUD. Different players focus on different things; both channels fire on every hit.

Hit: -35 in red. Kill: KILLED! in green. Guard: DEFEATED!. Miss: center flash only, no world number (there’s no world position for a missed swing).

The Checklist Pattern

“Can’t open the chest” kept showing up across multiple test sessions. v33 fixed the quest guide ordering so the player knows to fight the guard before the chest. That was the correct step — but there’s still a gap between “the guide updated” and “the player understands all three conditions.”

Opening the chest requires:

  1. Collect the gem
  2. Defeat the corridor guard
  3. Light the torch

The player didn’t know all three were required simultaneously. The guide showed the next step, not the full completion state. The fix: when the player approaches the chest, show a checklist with each condition’s current status:

func _update_chest_checklist() -> void:
    _chest_checklist_canvas.visible = _near_chest and not _chest_open
    var gem_ok := _has_gem
    var guard_ok := not _guard_alerted or _guard_dead
    var torch_ok := _torch_lit
    _chest_req_gem_label.text = ("%s Gem collected" % ("✓" if gem_ok else "✗"))
    _chest_req_guard_label.text = ("%s Guard dealt with" % ("✓" if guard_ok else "✗"))
    _chest_req_torch_label.text = ("%s Torch lit" % ("✓" if torch_ok else "✗"))

The panel appears only when near the chest and disappears once it opens. It shows live state — if you walk back to the torch room and light it, the ✗ becomes ✓ in real time.

This is the checklist pattern: when the player can’t do something, show them exactly what they’re missing. No mystery conditions, no trial-and-error. The preconditions are visible game state, and visible game state is understandable game state.

The Pattern Behind Both Fixes

Floating damage numbers and the requirements checklist look different on the surface. They address the same thing: the player trying an action and not getting enough feedback to understand whether it worked or what to try next.

  • Hit F to attack, nothing visible happens → floating number above the enemy’s head
  • Press E at the chest, nothing happens → checklist showing which conditions aren’t met

The feedback loop closes when the player can look at the screen and read exactly what just happened and what to do next. When that isn’t true, “unclear” is the correct summary — even if all the underlying logic is working.

Play v35 (floating damage numbers, chest checklist, bigger UI on Mac M2)

*Earlier iterations: v32-v33 v31 Skyrim world*