edc40f9008
- Touch controls: direct InputEventScreenTouch in shop_ui (bypass relay) - ItemDB: static preload list instead of DirAccess scan (export fix) - All 18 items with EN localisation (name_en, desc_en, category_en) - Ship playstyles: NOVA-1 shield, INFERNO ram, AURORA agile/tank - Quasar: SMBH visual, jet boost, merge, push, BH-eating - Atlas & UI text updated EN+DE Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
203 lines
7.3 KiB
GDScript
203 lines
7.3 KiB
GDScript
extends RefCounted
|
|
class_name BlackHole
|
|
|
|
const GRAVITY_STRENGTH := 0.8
|
|
# Original was 150px for ~1440px-wide window (10% of screen).
|
|
# Our canvas is 960px; 10% = 96px. Use ~160 for noticeable-but-fair gravity.
|
|
const PULL_RADIUS := 160.0
|
|
const SWALLOW_RADIUS := 14.0 # slightly smaller for fairer play
|
|
const MAX_BH_SPEED := 0.72 # 0.3 * 2.4
|
|
const DRIFT_PER_TICK := 0.012 # 0.005 * 2.4
|
|
const SUPERNOVA_AT := 30 # original value
|
|
const MAX_RADIUS := 60.0 # reasonable size at 960px
|
|
const FORCE_MULT := 45.0 # tuned: meaningful pull without instant death
|
|
const OBJ_MAX_VEL := 5.0 # velocity cap for attracted objects
|
|
|
|
# Hunting constants — slow drift, creates tension without instant death
|
|
const HUNT_DETECTION := 500.0 # roughly half-screen detection radius
|
|
const HUNT_ACCEL := 0.008 # very gentle acceleration toward player
|
|
const HUNT_MAX_SPEED := 0.38 # original 0.6 on 1440px → 0.6*(960/1440) = 0.4
|
|
const HUNT_LOSE_FRAMES := 180 # stops hunting if player out of range for 3s
|
|
const HUNT_CHANCE := 0.18
|
|
|
|
# SMBH lifespan — after SMBH_MAX_LIFE seconds it collapses
|
|
const SMBH_MAX_LIFE := 45.0
|
|
const SMBH_COLLAPSE_DURATION := 4.0
|
|
|
|
var x: float; var y: float
|
|
var vx: float = 0.0; var vy: float = 0.0
|
|
var radius: float = 12.0
|
|
var base_radius: float = 12.0
|
|
var pull_radius: float = PULL_RADIUS
|
|
var gravity: float = GRAVITY_STRENGTH
|
|
var consumed: int = 0
|
|
var pulse: float = 0.0
|
|
var dead: bool = false
|
|
var is_smbh: bool = false
|
|
var eject_timer: float = 0.0
|
|
|
|
var hunting: bool = false
|
|
var hunt_lost_timer: int = 0
|
|
|
|
var smbh_life: float = 0.0
|
|
var smbh_dying: bool = false
|
|
var smbh_collapse_timer: float = 0.0
|
|
|
|
var flash_intensity: float = 0.0
|
|
var flash_color: Color = Color(1.0, 1.0, 1.0, 0.0)
|
|
var accretion_flare: float = 0.0
|
|
|
|
func init(px: float, py: float, _mobile: bool = false) -> void:
|
|
x = px; y = py
|
|
var r := randf_range(6.0, 12.0) * 2.4 # 6..12 * 2.4
|
|
radius = r
|
|
base_radius = r
|
|
# Initial velocity: ±0.36 (original ±0.15 * 2.4)
|
|
vx = randf_range(-0.36, 0.36)
|
|
vy = randf_range(-0.36, 0.36)
|
|
hunting = randf() < HUNT_CHANCE
|
|
|
|
func become_smbh() -> void:
|
|
is_smbh = true
|
|
radius = randf_range(60.0, 90.0) # original 55-75 * ~1.4
|
|
base_radius = radius
|
|
pull_radius = min(PULL_RADIUS * 4.0, pull_radius * 2.5)
|
|
gravity = min(GRAVITY_STRENGTH * 4.0, gravity * 2.0)
|
|
|
|
func update(delta: float, players: Array, world_w: float, world_h: float) -> void:
|
|
pulse += delta * 2.0
|
|
eject_timer += delta
|
|
|
|
# Per-tick micro-drift — matches original rand(-0.005, 0.005) per frame * 2.4
|
|
vx += randf_range(-DRIFT_PER_TICK, DRIFT_PER_TICK)
|
|
vy += randf_range(-DRIFT_PER_TICK, DRIFT_PER_TICK)
|
|
|
|
# Hunting: 18% of BHs chase player within detection radius
|
|
if hunting and players.size() > 0:
|
|
var nearest_dist := INF
|
|
var nearest_p = null
|
|
for p in players:
|
|
if p.dead: continue
|
|
var dx: float = float(p.x) - x
|
|
var dy: float = float(p.y) - y
|
|
var d: float = sqrt(dx*dx + dy*dy)
|
|
if d < nearest_dist:
|
|
nearest_dist = d
|
|
nearest_p = p
|
|
if nearest_p != null and nearest_dist < HUNT_DETECTION and nearest_dist > 0.0:
|
|
var dx: float = float(nearest_p.x) - x
|
|
var dy: float = float(nearest_p.y) - y
|
|
var dist: float = sqrt(dx*dx + dy*dy)
|
|
vx += (dx/dist) * HUNT_ACCEL
|
|
vy += (dy/dist) * HUNT_ACCEL
|
|
var sp := sqrt(vx*vx + vy*vy)
|
|
if sp > HUNT_MAX_SPEED:
|
|
vx = vx/sp * HUNT_MAX_SPEED
|
|
vy = vy/sp * HUNT_MAX_SPEED
|
|
hunt_lost_timer = 0
|
|
else:
|
|
hunt_lost_timer += 1
|
|
if hunt_lost_timer >= HUNT_LOSE_FRAMES:
|
|
hunting = false
|
|
|
|
# SMBH lifespan — collapses after SMBH_MAX_LIFE seconds
|
|
if is_smbh and not dead:
|
|
smbh_life += delta
|
|
if smbh_life >= SMBH_MAX_LIFE and not smbh_dying:
|
|
smbh_dying = true
|
|
smbh_collapse_timer = SMBH_COLLAPSE_DURATION
|
|
if smbh_dying:
|
|
smbh_collapse_timer -= delta
|
|
radius = max(8.0, base_radius * (smbh_collapse_timer / SMBH_COLLAPSE_DURATION))
|
|
if smbh_collapse_timer <= 0.0:
|
|
dead = true
|
|
|
|
# Clamp wander speed
|
|
vx = clamp(vx, -MAX_BH_SPEED, MAX_BH_SPEED)
|
|
vy = clamp(vy, -MAX_BH_SPEED, MAX_BH_SPEED)
|
|
|
|
x += vx; y += vy
|
|
|
|
# Soft bounce off edges (5% / 95% margins like original)
|
|
if x < world_w * 0.05: vx = abs(vx)
|
|
elif x > world_w * 0.95: vx = -abs(vx)
|
|
if y < world_h * 0.05: vy = abs(vy)
|
|
elif y > world_h * 0.95: vy = -abs(vy)
|
|
|
|
# Consume flash/flare decay
|
|
if flash_intensity > 0.0:
|
|
flash_intensity = maxf(0.0, flash_intensity - delta * 3.0)
|
|
if accretion_flare > 0.0:
|
|
accretion_flare = maxf(0.0, accretion_flare - delta * 1.5)
|
|
|
|
func check_swallow(ox: float, oy: float) -> bool:
|
|
var dx := ox - x; var dy := oy - y
|
|
return sqrt(dx*dx + dy*dy) < SWALLOW_RADIUS + radius * 0.5
|
|
|
|
func apply_gravity(ox: float, oy: float, ovx: float, ovy: float) -> Vector2:
|
|
var dx := ox - x; var dy := oy - y
|
|
var dist := sqrt(dx*dx + dy*dy)
|
|
var nvx := ovx; var nvy := ovy
|
|
if dist < pull_radius and dist > 0.01:
|
|
var force := gravity / (dist * dist) * FORCE_MULT
|
|
nvx += (dx/dist) * -force
|
|
nvy += (dy/dist) * -force
|
|
return Vector2(nvx, nvy)
|
|
|
|
func on_swallow() -> bool:
|
|
consumed += 1
|
|
# Growth formula from original: radius = min(MAX, base + consumed * 0.4) * 2.4
|
|
radius = min(MAX_RADIUS, base_radius + consumed * 0.96) # 0.4 * 2.4
|
|
pull_radius = min(PULL_RADIUS * 2.0, PULL_RADIUS + consumed * 7.2) # 3 * 2.4
|
|
gravity = min(GRAVITY_STRENGTH * 2.0, GRAVITY_STRENGTH + consumed * 0.05)
|
|
if consumed >= SUPERNOVA_AT and not is_smbh:
|
|
return true
|
|
return false
|
|
|
|
func trigger_flash(col: Color, intensity: float = 1.0) -> void:
|
|
flash_intensity = intensity
|
|
flash_color = col
|
|
accretion_flare = intensity
|
|
|
|
func draw(canvas: CanvasItem) -> void:
|
|
var p := 0.6 + 0.4 * sin(pulse)
|
|
var cv := Vector2(x, y)
|
|
var ring_r := radius + 4.0 + 4.0 * sin(pulse)
|
|
var ring_col := Color(0.6, 0.2, 1.0, p * 0.9) if not is_smbh else Color(1.0, 0.4, 0.0, p)
|
|
|
|
# Outer glow halo — drawn first so core covers it
|
|
canvas.draw_circle(cv, radius + ring_r * 0.35, Color(ring_col.r, ring_col.g, ring_col.b, p * 0.08))
|
|
|
|
# Accretion disk: rotating segmented arcs — flare brightens + thickens on consume
|
|
var flare_mult: float = 1.0 + accretion_flare * 2.0
|
|
var disk_w: float = 2.5 + accretion_flare * 3.0
|
|
var disk_r := ring_r + 8.0
|
|
for seg in 6:
|
|
var sa := pulse * 0.55 + float(seg) / 6.0 * TAU
|
|
var arc_col := Color(ring_col.r, ring_col.g, ring_col.b, minf(ring_col.a * flare_mult, 1.0))
|
|
canvas.draw_arc(cv, ring_r, sa, sa + TAU / 6.0 * 0.75, 6, arc_col, disk_w)
|
|
for seg in 4:
|
|
var sa := -pulse * 0.3 + float(seg) / 4.0 * TAU
|
|
canvas.draw_arc(cv, disk_r, sa, sa + TAU / 4.0 * 0.6, 5,
|
|
Color(0.9, 0.5, 0.2, minf(p * 0.35 * flare_mult, 1.0)), disk_w * 0.6)
|
|
|
|
# Black core — single draw_circle replaces O(radius²) pixel loop
|
|
canvas.draw_circle(cv, radius, Color(0.0, 0.0, 0.0, 1.0))
|
|
|
|
# SMBH extra rings
|
|
if is_smbh:
|
|
for ri in 3:
|
|
var er := ring_r + float(ri + 1) * 10.0
|
|
var rot_dir := 1.0 if ri % 2 == 0 else -1.0
|
|
var sa := pulse * (0.35 + float(ri) * 0.1) * rot_dir
|
|
canvas.draw_arc(cv, er, sa, sa + TAU, 24,
|
|
Color(1.0, 0.6, 0.0, p * 0.3), 1.5)
|
|
|
|
# Consume flash — expanding ring + inner glow
|
|
if flash_intensity > 0.0:
|
|
var flash_r: float = radius + 10.0 + (1.0 - flash_intensity) * 25.0
|
|
canvas.draw_arc(cv, flash_r, 0.0, TAU, 24,
|
|
Color(flash_color.r, flash_color.g, flash_color.b, flash_intensity * 0.7), 2.5)
|
|
canvas.draw_circle(cv, radius + 4.0,
|
|
Color(flash_color.r, flash_color.g, flash_color.b, flash_intensity * 0.15))
|