rimlike/scenes/world/camera_rig.gd
megaproxy cf43ef9a98 camera_rig defers to active paint tool + gitignore build artifacts
K: camera_rig._unhandled_input checks World.designation_ctl.active_tool()
before starting drag-pan or applying ScreenDrag. Fixes drag-paint being
silently downgraded to single-cell when a designate/build/stockpile
tool is active. Reverse-tree input dispatch gave CameraRig first crack
at drag events (CameraRig is later in world.tscn than DesignationCtl).

S: .gitignore now covers *.pck, Rimlike.sh, export_presets.cfg, and
.claude/scheduled_tasks.lock.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:27:26 +01:00

163 lines
6.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends Camera2D
## World-view camera rig — pinch-zoom, drag-pan, double-tap-centre.
## All input handled via _unhandled_input so CanvasLayer UI overlays swallow first.
## Call set_world_bounds(rect) once from the world scene after map generation.
const MIN_ZOOM: float = 0.5 # strategic / whole-map-ish
const MAX_ZOOM: float = 4.0 # close / sprite-readable
const BOUNDS_BLEED_PX: int = 32 # 2 tiles of bleed at map edge
const DOUBLE_TAP_MS: int = 300 # window for double-tap detection
const DOUBLE_TAP_DIST_PX: float = 16.0 # 1 tile radius
var target_zoom: float = 1.0
var _dragging: bool = false # desktop left-mouse drag tracking
var _last_tap_time_ms: int = 0
var _last_tap_world_pos: Vector2 = Vector2.ZERO
var _centre_tween: Tween = null
func _ready() -> void:
position_smoothing_enabled = false
# Phase 5 visual polish: default 2.5× — at 1× the world feels sparse on phone
# and pawns become 6-pixel dots; 2.5× shows ~30 tiles wide which is the
# "comfortable inspection" zoom level.
target_zoom = 2.5
zoom = Vector2(target_zoom, target_zoom)
## Pan speed in world pixels per second at zoom 1.0.
## Scales inversely with zoom so on-screen apparent speed feels constant.
const PAN_SPEED_PX_PER_SEC: float = 600.0
func _process(delta: float) -> void:
# Lerp zoom toward target each render frame.
# Plain 0.15 factor is correct at 60 fps; frame-rate independent form used for safety.
var t: float = 1.0 - pow(1.0 - 0.15, delta * 60.0)
zoom = zoom.lerp(Vector2(target_zoom, target_zoom), t)
# Keyboard pan — WASD / arrow keys. Strength gives an analogue-stick-style
# value (0..1) so held-key is continuous and action-just-pressed is not needed.
var pan_input := Vector2(
Input.get_action_strength("pan_right") - Input.get_action_strength("pan_left"),
Input.get_action_strength("pan_down") - Input.get_action_strength("pan_up"),
)
if pan_input != Vector2.ZERO:
position += pan_input.normalized() * (PAN_SPEED_PX_PER_SEC / zoom.x) * delta
## Returns true when a Designation paint tool is active.
## Camera drag-pan must yield to the paint tool so drag strokes reach Designation.
func _paint_tool_active() -> bool:
var d = World.designation_ctl
return d != null and d.active_tool() != Designation.TOOL_NONE
func _unhandled_input(event: InputEvent) -> void:
# --- Pinch-zoom via magnify gesture (trackpad + native touch pinch) ---
if event is InputEventMagnifyGesture:
target_zoom = clampf(target_zoom * event.factor, MIN_ZOOM, MAX_ZOOM)
return
# --- Keyboard zoom (= / + / KP_ADD and - / KP_SUBTRACT) ---
if event.is_action_pressed("zoom_in"):
target_zoom = clampf(target_zoom * 1.1, MIN_ZOOM, MAX_ZOOM)
Audit.log("camera", "zoom_in → %.2f" % target_zoom)
return
if event.is_action_pressed("zoom_out"):
target_zoom = clampf(target_zoom / 1.1, MIN_ZOOM, MAX_ZOOM)
Audit.log("camera", "zoom_out → %.2f" % target_zoom)
return
# --- Scroll-wheel zoom (desktop) ---
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
target_zoom = clampf(target_zoom * 1.1, MIN_ZOOM, MAX_ZOOM)
return
if event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed:
target_zoom = clampf(target_zoom / 1.1, MIN_ZOOM, MAX_ZOOM)
return
# --- Desktop left-mouse: drag-pan tracking + double-tap detection ---
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
# Don't start a pan drag while a paint tool is active; the
# button-down belongs to Designation's stroke start.
if not _paint_tool_active():
_dragging = true
_check_double_tap(event.position)
else:
_dragging = false
return
# --- Touch drag-pan (skip when a paint tool is active) ---
if event is InputEventScreenDrag:
if not _paint_tool_active():
_apply_pan(event.relative)
return
# --- Desktop mouse-motion drag-pan (only while left mouse held) ---
# _dragging is only set when no paint tool was active at press time, so this
# guard is sufficient for the desktop case.
if event is InputEventMouseMotion and _dragging:
_apply_pan(event.relative)
return
# --- Touch press: double-tap detection ---
if event is InputEventScreenTouch and event.pressed:
_check_double_tap(event.position)
func _apply_pan(relative: Vector2) -> void:
# zoom.x > 1 means more zoomed-in; each screen pixel = fewer world units.
position -= relative / zoom.x
func _check_double_tap(screen_pos: Vector2) -> void:
var now_ms: int = Time.get_ticks_msec()
var world_pos: Vector2 = get_canvas_transform().affine_inverse() * screen_pos
var dt: int = now_ms - _last_tap_time_ms
var dist: float = world_pos.distance_to(_last_tap_world_pos)
if dt < DOUBLE_TAP_MS and dist < DOUBLE_TAP_DIST_PX:
_centre_on(world_pos)
# Reset so a triple-tap doesn't re-trigger immediately
_last_tap_time_ms = 0
else:
_last_tap_time_ms = now_ms
_last_tap_world_pos = world_pos
func _centre_on(world_pos: Vector2) -> void:
# Kill any in-progress centre tween before starting a new one
if _centre_tween != null and _centre_tween.is_valid():
_centre_tween.kill()
_centre_tween = create_tween()
_centre_tween.set_ease(Tween.EASE_OUT)
_centre_tween.set_trans(Tween.TRANS_QUAD)
_centre_tween.tween_property(self, "position", world_pos, 0.2)
## Public API — smooth-pan the camera to the centre of a tile (world space).
## Used by StorytellerBanner and StorytellerModal "Go there" buttons.
## Reuses the same _centre_tween slot so a rapid second call replaces the first.
func pan_to_tile(tile: Vector2i) -> void:
const TILE_PX: int = 16
var world_pos := Vector2(tile.x * TILE_PX + TILE_PX / 2, tile.y * TILE_PX + TILE_PX / 2)
_centre_on(world_pos)
Audit.log("camera", "pan_to_tile %s → world %s" % [tile, world_pos])
## Public API — call once from the world scene after map bounds are known.
func set_world_bounds(rect: Rect2) -> void:
limit_left = int(rect.position.x) - BOUNDS_BLEED_PX
limit_top = int(rect.position.y) - BOUNDS_BLEED_PX
limit_right = int(rect.end.x) + BOUNDS_BLEED_PX
limit_bottom = int(rect.end.y) + BOUNDS_BLEED_PX
Audit.log("camera", "bounds set: %s (bleed %d)" % [rect, BOUNDS_BLEED_PX])