extends "res://scripts/building/piece.gd" ## Campfire: light, warmth and cooking, while it has fuel. ## ## Starts with a little kindling. E adds wood (or sticks) from your inventory; ## it burns down in play time and goes out at zero. While lit it is a ## "campfire" station (cooking) and a heat source (warmth within ~7 m). const MAX_FUEL := 900.0 ## 15 min const START_FUEL := 120.0 const HEAT_RADIUS := 7.0 var fuel := START_FUEL var _light: OmniLight3D var _fx: Array[CPUParticles3D] = [] var _t := 0.0 var _noise := FastNoiseLite.new() var _lit := true func _build() -> void: add_to_group("station:campfire") add_to_group("heat") _noise.frequency = 2.5 _light = OmniLight3D.new() _light.light_color = Color(1.0, 0.62, 0.3) _light.light_energy = 2.2 _light.omni_range = 11.0 _light.omni_attenuation = 1.4 _light.shadow_enabled = true _light.position = Vector3(0, 0.7, 0) add_child(_light) _fx.append(_particles("Flames", 26, 0.55, Vector3(0, 0.12, 0), 0.5, 1.0, 0.5, 0.9, _gradient([ [0.0, Color(1.0, 0.8, 0.35, 0.0)], [0.15, Color(1.0, 0.62, 0.2, 0.75)], [0.6, Color(0.95, 0.3, 0.06, 0.45)], [1.0, Color(0.5, 0.08, 0.02, 0.0)]]), true, 0.32, 0.12)) _fx.append(_particles("Embers", 10, 2.0, Vector3(0, 0.3, 0), 0.8, 2.0, 0.5, 1.0, _gradient([ [0.0, Color(1.0, 0.75, 0.35, 1.0)], [1.0, Color(1.0, 0.3, 0.05, 0.0)]]), true, 0.04, 0.15)) for p in _fx: add_child(p) # TODO: smoke. Alpha-blended unshaded particles rendered as an opaque dark # ball whatever their ramp colour (additive flames are fine). Needs a small # particle shader. _set_lit(fuel > 0.0) func display_name() -> String: return "Campfire" func is_lit() -> bool: return _lit func station_active() -> bool: return _lit ## 0..1 warmth this fire gives at `pos`. func heat_at(pos: Vector3) -> float: if not _lit: return 0.0 return 1.0 - smoothstep(1.5, HEAT_RADIUS, pos.distance_to(global_position)) func add_fuel(seconds: float) -> void: fuel = minf(fuel + seconds, MAX_FUEL) _set_lit(true) func interact_prompt(player: Node) -> String: var has_fuel: bool = player.inventory.count("wood") + player.inventory.count("stick") > 0 if _lit: var left := "%d:%02d" % [int(fuel) / 60, int(fuel) % 60] return "Campfire (%s left)%s" % [left, " [E] Add fuel" if has_fuel else " (add wood or sticks)"] return "Campfire (out)%s" % (" [E] Add fuel to light" if has_fuel else " (needs wood or sticks)") func interact(player: Node) -> void: Actions.add_fuel(player, self) func _process(delta: float) -> void: if not _lit: return fuel -= delta if fuel <= 0.0: fuel = 0.0 _set_lit(false) return _t += delta var f := _noise.get_noise_1d(_t * 10.0) # Burns lower in its last minute. var strength := clampf(fuel / 60.0, 0.35, 1.0) _light.light_energy = (2.2 + f * 0.7) * strength _light.position = Vector3(f * 0.05, 0.7 + f * 0.05, 0) func _set_lit(on: bool) -> void: _lit = on if _light != null: _light.visible = on for p in _fx: p.emitting = on func save_extra() -> Dictionary: return {"fuel": fuel} func load_extra(d: Dictionary) -> void: fuel = float(d.get("fuel", fuel)) _set_lit(fuel > 0.0) static func _gradient(stops: Array) -> Gradient: var g := Gradient.new() g.offsets = PackedFloat32Array(stops.map(func(s): return s[0])) g.colors = PackedColorArray(stops.map(func(s): return s[1])) return g static func _soft_dot() -> GradientTexture2D: var g := Gradient.new() g.offsets = PackedFloat32Array([0.0, 1.0]) g.colors = PackedColorArray([Color(1, 1, 1, 1), Color(1, 1, 1, 0)]) var t := GradientTexture2D.new() t.gradient = g t.fill = GradientTexture2D.FILL_RADIAL t.fill_from = Vector2(0.5, 0.5) t.fill_to = Vector2(0.5, 0.0) t.width = 64 t.height = 64 return t static func _particles(label: String, amount: int, lifetime: float, pos: Vector3, vmin: float, vmax: float, smin: float, smax: float, ramp: Gradient, additive: bool, quad_size: float, radius: float) -> CPUParticles3D: var p := CPUParticles3D.new() p.name = label p.amount = amount p.lifetime = lifetime p.position = pos p.direction = Vector3.UP p.spread = 12.0 p.initial_velocity_min = vmin p.initial_velocity_max = vmax p.gravity = Vector3(0.15, 0.4, 0.05) p.emission_shape = CPUParticles3D.EMISSION_SHAPE_SPHERE p.emission_sphere_radius = radius p.scale_amount_min = smin p.scale_amount_max = smax p.color_ramp = ramp # Flames shrink as they rise. var curve := Curve.new() curve.add_point(Vector2(0.0, 1.0)) curve.add_point(Vector2(1.0, 0.35)) p.scale_amount_curve = curve var quad := QuadMesh.new() quad.size = Vector2.ONE * quad_size var m := StandardMaterial3D.new() m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED m.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES m.vertex_color_use_as_albedo = true m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA m.blend_mode = BaseMaterial3D.BLEND_MODE_ADD if additive else BaseMaterial3D.BLEND_MODE_MIX m.albedo_texture = _soft_dot() if additive: m.albedo_color = Color(1.5, 1.5, 1.5) # mildly HDR so the glow picks it up quad.material = m p.mesh = quad return p