feat: add witnessed knowledge and wind ambience
This commit is contained in:
@@ -34,8 +34,10 @@ func toggle_debug_overlay() -> void:
|
||||
debug_overlay_visible = not debug_overlay_visible
|
||||
_apply_debug_overlay_visibility()
|
||||
print(
|
||||
"[DemoController] %s"
|
||||
% ("Debug overlay visible" if debug_overlay_visible else "Cinematic overlay hidden")
|
||||
(
|
||||
"[DemoController] %s"
|
||||
% ("Debug overlay visible" if debug_overlay_visible else "Cinematic overlay hidden")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
[sub_resource type="ParticleProcessMaterial" id="3"]
|
||||
emission_shape = 1
|
||||
emission_box_extents = Vector3(0.1, 0.025, 0.1)
|
||||
gravity = Vector3(0, 0.3, 0)
|
||||
gravity = Vector3(0.065, 0.3, 0.025)
|
||||
velocity_min = 0.0
|
||||
velocity_max = 0.5
|
||||
scale_min = 0.05
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[gd_scene load_steps=37 format=3]
|
||||
[gd_scene load_steps=36 format=3]
|
||||
|
||||
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
|
||||
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
||||
@@ -16,8 +16,7 @@
|
||||
[ext_resource type="PackedScene" path="res://world/storage/StorageNode.tscn" id="14_storage"]
|
||||
[ext_resource type="Script" path="res://world/activity/ActivitySite.gd" id="15_activity"]
|
||||
[ext_resource type="NavigationMesh" path="res://world/jajce/JajceNavigationMesh.tres" id="16_nav"]
|
||||
[ext_resource type="PackedScene" path="res://world/jajce/WindStrokes.tscn" id="19_windstrokes"]
|
||||
[ext_resource type="PackedScene" path="res://world/jajce/WindSpecks.tscn" id="20_windspecks"]
|
||||
[ext_resource type="PackedScene" path="res://world/jajce/vfx/WindGustField.tscn" id="17_wind_gust"]
|
||||
|
||||
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
||||
_shader_parameters = {
|
||||
@@ -593,15 +592,10 @@ light_energy = 1.45
|
||||
shadow_enabled = true
|
||||
directional_shadow_max_distance = 180.0
|
||||
|
||||
[node name="WindStrokes_Valley" parent="." instance=ExtResource("19_windstrokes")]
|
||||
[node name="AtmosphereRoot" type="Node3D" parent="."]
|
||||
|
||||
[node name="WindStrokes_Ridge" parent="." instance=ExtResource("19_windstrokes")]
|
||||
position = Vector3(-25, 7, 9)
|
||||
|
||||
[node name="WindSpecks_Valley" parent="." instance=ExtResource("20_windspecks")]
|
||||
|
||||
[node name="WindSpecks_Ridge" parent="." instance=ExtResource("20_windspecks")]
|
||||
position = Vector3(-25, 7, 9)
|
||||
[node name="WindGustField" parent="AtmosphereRoot" instance=ExtResource("17_wind_gust")]
|
||||
position = Vector3(-2, 0, 0)
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("Environment_greybox")
|
||||
|
||||
@@ -6,9 +6,6 @@ enum CanopyVariant {
|
||||
YELLOW_GREEN,
|
||||
}
|
||||
|
||||
@export var canopy_variant: CanopyVariant = CanopyVariant.GREEN
|
||||
@export var canopy_radius: float = 1.35
|
||||
|
||||
const COLORS := {
|
||||
CanopyVariant.GREEN: Color(0.19, 0.38, 0.17),
|
||||
CanopyVariant.DARK_GREEN: Color(0.12, 0.30, 0.12),
|
||||
@@ -16,6 +13,8 @@ const COLORS := {
|
||||
}
|
||||
|
||||
const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader")
|
||||
const WIND_STRENGTH := 0.07
|
||||
const WIND_SPEED := 0.55
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -25,8 +24,9 @@ func _ready() -> void:
|
||||
var hash_seed := hash(global_position)
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = hash_seed
|
||||
canopy_variant = rng.randi() % COLORS.size() as CanopyVariant
|
||||
canopy_radius = 1.0 + rng.randf() * 0.8
|
||||
var canopy_variant := rng.randi() % COLORS.size() as CanopyVariant
|
||||
var canopy_radius := 1.0 + rng.randf() * 0.8
|
||||
var wind_phase := rng.randf_range(0.0, TAU)
|
||||
|
||||
var trunk_mesh := CylinderMesh.new()
|
||||
trunk_mesh.top_radius = 0.22
|
||||
@@ -39,6 +39,7 @@ func _ready() -> void:
|
||||
trunk_mesh.material = trunk_mat
|
||||
|
||||
var trunk := MeshInstance3D.new()
|
||||
trunk.name = "Trunk"
|
||||
trunk.mesh = trunk_mesh
|
||||
trunk.position = Vector3(0, 1.4, 0)
|
||||
add_child(trunk)
|
||||
@@ -51,9 +52,13 @@ func _ready() -> void:
|
||||
canopy_mat.shader = WIND_SHADER
|
||||
canopy_mat.set_shader_parameter("albedo", COLORS[canopy_variant])
|
||||
canopy_mat.set_shader_parameter("roughness", 0.9)
|
||||
canopy_mat.set_shader_parameter("wind_strength", WIND_STRENGTH)
|
||||
canopy_mat.set_shader_parameter("wind_speed", WIND_SPEED)
|
||||
canopy_mat.set_shader_parameter("wind_phase", wind_phase)
|
||||
canopy_mesh.material = canopy_mat
|
||||
|
||||
var canopy := MeshInstance3D.new()
|
||||
canopy.name = "Canopy"
|
||||
canopy.mesh = canopy_mesh
|
||||
canopy.position = Vector3(0, 3.2 + (canopy_radius - 1.35) * 0.5, 0)
|
||||
add_child(canopy)
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_specks"]
|
||||
lifetime_randomness = 0.8
|
||||
spread = 120.0
|
||||
flatness = 0.5
|
||||
gravity = Vector3(-0.35, 0.25, 0.0)
|
||||
initial_velocity_min = 0.2
|
||||
initial_velocity_max = 1.2
|
||||
scale_min = 0.06
|
||||
scale_max = 0.28
|
||||
color = Color(1, 0.98, 0.9, 0.3)
|
||||
color_ramp = 1
|
||||
emission_shape = 1
|
||||
emission_box_extents = Vector3(32.0, 14.0, 32.0)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_speck"]
|
||||
albedo_color = Color(1, 0.98, 0.92, 0.3)
|
||||
transparency = 1
|
||||
shading_mode = 0
|
||||
billboard_mode = 1
|
||||
cull_mode = 0
|
||||
|
||||
[sub_resource type="QuadMesh" id="Mesh_speck"]
|
||||
material = SubResource("Material_speck")
|
||||
size = Vector2(0.12, 0.12)
|
||||
|
||||
[node name="WindSpecks" type="GPUParticles3D"]
|
||||
emitting = true
|
||||
amount = 40
|
||||
lifetime = 6.0
|
||||
one_shot = false
|
||||
preprocess = 4.0
|
||||
speed_scale = 1.0
|
||||
local_coords = false
|
||||
process_material = SubResource("ParticleProcessMaterial_specks")
|
||||
draw_pass_1 = SubResource("Mesh_speck")
|
||||
@@ -1,40 +0,0 @@
|
||||
[gd_scene load_steps=5 format=3]
|
||||
|
||||
[ext_resource type="Shader" path="res://world/jajce/materials/wind_stroke.gdshader" id="1_shader"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_stroke"]
|
||||
shader = ExtResource("1_shader")
|
||||
|
||||
[sub_resource type="QuadMesh" id="Mesh_stroke"]
|
||||
material = SubResource("ShaderMaterial_stroke")
|
||||
size = Vector2(1.5, 0.07)
|
||||
|
||||
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_wind"]
|
||||
lifetime_randomness = 0.6
|
||||
spread = 55.0
|
||||
flatness = 0.7
|
||||
gravity = Vector3(-0.6, 0.35, 0.0)
|
||||
initial_velocity_min = 0.8
|
||||
initial_velocity_max = 2.8
|
||||
angular_velocity_min = -0.25
|
||||
angular_velocity_max = 0.25
|
||||
radial_accel_min = 0.0
|
||||
radial_accel_max = 0.0
|
||||
tangential_accel_min = -0.08
|
||||
tangential_accel_max = 0.08
|
||||
scale_min = 0.3
|
||||
scale_max = 1.2
|
||||
color = Color(1, 1, 1, 0.35)
|
||||
emission_shape = 1
|
||||
emission_box_extents = Vector3(28.0, 8.0, 28.0)
|
||||
|
||||
[node name="WindStrokes" type="GPUParticles3D"]
|
||||
emitting = true
|
||||
amount = 10
|
||||
lifetime = 5.0
|
||||
one_shot = false
|
||||
preprocess = 3.0
|
||||
speed_scale = 1.0
|
||||
local_coords = false
|
||||
process_material = SubResource("ParticleProcessMaterial_wind")
|
||||
draw_pass_1 = SubResource("Mesh_stroke")
|
||||
@@ -40,7 +40,7 @@ var _simulation_node: Node
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_simulation_node = get_tree().get_first_node_in_group("simulation_manager")
|
||||
_find_simulation_node()
|
||||
if _simulation_node == null:
|
||||
elapsed = cycle_duration_seconds * initial_cycle
|
||||
_apply_light_rotation(initial_cycle)
|
||||
@@ -48,8 +48,10 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _simulation_node == null or not is_instance_valid(_simulation_node):
|
||||
_find_simulation_node()
|
||||
var cycle: float
|
||||
if _simulation_node != null and "clock" in _simulation_node:
|
||||
if _simulation_node != null and "clock" in _simulation_node and _simulation_node.clock != null:
|
||||
cycle = _simulation_node.clock.time_of_day()
|
||||
else:
|
||||
elapsed += _delta
|
||||
@@ -77,18 +79,18 @@ func _apply_environment(cycle: float) -> void:
|
||||
directional_light.light_color = _lerp_color(NIGHT_LIGHT_COLOR, DAY_LIGHT_COLOR, day_factor)
|
||||
directional_light.light_energy = lerpf(NIGHT_LIGHT_ENERGY, DAY_LIGHT_ENERGY, day_factor)
|
||||
|
||||
var sunset_peak := _sunrise_sunset_weight(cycle)
|
||||
if sunset_peak > 0.0:
|
||||
var sunset_color: Color
|
||||
var transition_peak := _sunrise_sunset_weight(cycle)
|
||||
if transition_peak > 0.0:
|
||||
var transition_color: Color
|
||||
if cycle < 0.5:
|
||||
sunset_color = SUNSET_LIGHT_COLOR
|
||||
transition_color = SUNRISE_LIGHT_COLOR
|
||||
else:
|
||||
sunset_color = SUNRISE_LIGHT_COLOR
|
||||
transition_color = SUNSET_LIGHT_COLOR
|
||||
directional_light.light_color = directional_light.light_color.lerp(
|
||||
sunset_color, sunset_peak * 0.85
|
||||
transition_color, transition_peak * 0.85
|
||||
)
|
||||
directional_light.light_energy = maxf(
|
||||
directional_light.light_energy, SUNRISE_SUNSET_ENERGY * sunset_peak
|
||||
directional_light.light_energy, SUNRISE_SUNSET_ENERGY * transition_peak
|
||||
)
|
||||
|
||||
if world_environment == null or world_environment.environment == null:
|
||||
@@ -112,6 +114,10 @@ func _apply_environment(cycle: float) -> void:
|
||||
mat.ground_horizon_color = _lerp_color(NIGHT_GROUND_HORIZON, DAY_GROUND_HORIZON, day_factor)
|
||||
|
||||
|
||||
func _find_simulation_node() -> void:
|
||||
_simulation_node = get_tree().get_first_node_in_group("simulation_manager")
|
||||
|
||||
|
||||
func _day_factor(cycle: float) -> float:
|
||||
if cycle < 0.15:
|
||||
return 0.0
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
shader_type spatial;
|
||||
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
|
||||
|
||||
uniform float wind_strength : hint_range(0.0, 0.5) = 0.08;
|
||||
uniform float wind_speed : hint_range(0.0, 5.0) = 0.6;
|
||||
uniform vec2 wind_direction = vec2(0.94, 0.34);
|
||||
uniform float wind_strength : hint_range(0.0, 0.2) = 0.07;
|
||||
uniform float wind_speed : hint_range(0.0, 2.0) = 0.55;
|
||||
uniform float gust_speed : hint_range(0.0, 1.0) = 0.12;
|
||||
uniform float gust_strength : hint_range(0.0, 0.8) = 0.35;
|
||||
uniform float flutter_strength : hint_range(0.0, 0.05) = 0.012;
|
||||
uniform float wind_phase : hint_range(0.0, 6.2832) = 0.0;
|
||||
uniform vec3 albedo : source_color = vec3(0.19, 0.38, 0.17);
|
||||
uniform float roughness : hint_range(0.0, 1.0) = 0.9;
|
||||
|
||||
@@ -11,11 +16,16 @@ void vertex() {
|
||||
float height_factor = clamp((VERTEX.y + half_height) / (half_height * 2.0), 0.0, 1.0);
|
||||
height_factor = height_factor * height_factor;
|
||||
|
||||
float sway_x = sin(VERTEX.x * 0.3 + TIME * wind_speed) * wind_strength * height_factor;
|
||||
float sway_z = cos(VERTEX.z * 0.3 + TIME * wind_speed * 0.7) * wind_strength * height_factor;
|
||||
vec2 direction = normalize(wind_direction);
|
||||
vec2 crosswind = vec2(-direction.y, direction.x);
|
||||
float world_phase = wind_phase + dot(MODEL_MATRIX[3].xz, vec2(0.07, 0.05));
|
||||
float gust_wave = 0.5 + 0.5 * sin(TIME * gust_speed + world_phase * 0.7);
|
||||
float gust = mix(1.0 - gust_strength, 1.0, gust_wave);
|
||||
float bend = sin(TIME * wind_speed + world_phase) * wind_strength * gust;
|
||||
float flutter = sin(TIME * 1.7 + world_phase * 1.9 + VERTEX.y * 2.0) * flutter_strength;
|
||||
|
||||
VERTEX.x += sway_x;
|
||||
VERTEX.z += sway_z;
|
||||
VERTEX.xz += direction * bend * height_factor;
|
||||
VERTEX.xz += crosswind * flutter * height_factor;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
shader_type spatial;
|
||||
render_mode blend_mix, depth_draw_never, cull_disabled, unshaded;
|
||||
|
||||
uniform vec4 tint : source_color = vec4(0.76, 0.91, 0.75, 0.2);
|
||||
uniform float progress : hint_range(0.0, 1.0) = 0.0;
|
||||
uniform float glow_strength : hint_range(0.0, 1.0) = 0.38;
|
||||
|
||||
void fragment() {
|
||||
float edge_softness = smoothstep(0.0, 0.24, UV.y) * smoothstep(0.0, 0.24, 1.0 - UV.y);
|
||||
float head = progress * 1.18 - 0.08;
|
||||
float tail = head - 0.52;
|
||||
float head_mask = 1.0 - smoothstep(head, head + 0.07, UV.x);
|
||||
float tail_mask = smoothstep(tail - 0.08, tail + 0.03, UV.x);
|
||||
float life_fade = smoothstep(0.0, 0.12, progress) * (1.0 - smoothstep(0.82, 1.0, progress));
|
||||
float brush_texture = 0.88 + 0.12 * sin(UV.x * 31.0 + UV.y * 5.0);
|
||||
float stroke_alpha = edge_softness * head_mask * tail_mask * life_fade * brush_texture;
|
||||
float glint = exp(-pow((UV.x - head) * 20.0, 2.0)) * edge_softness * life_fade;
|
||||
|
||||
ALBEDO = tint.rgb;
|
||||
EMISSION = tint.rgb * (glow_strength * stroke_alpha + glint * 0.28);
|
||||
ALPHA = tint.a * stroke_alpha;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bjj5oxdmfpgqf
|
||||
@@ -1,14 +0,0 @@
|
||||
shader_type spatial;
|
||||
render_mode blend_mix, depth_draw_never, cull_disabled, unshaded;
|
||||
|
||||
uniform float softness : hint_range(0.0, 1.0) = 0.45;
|
||||
uniform vec3 tint : source_color = vec3(0.95, 0.93, 0.88);
|
||||
|
||||
void fragment() {
|
||||
float edge_dist = abs(UV.y - 0.5) * 2.0;
|
||||
float alpha = 1.0 - smoothstep(0.0, 1.0, edge_dist);
|
||||
alpha *= smoothstep(0.0, 0.2, UV.x) * smoothstep(0.0, 0.2, 1.0 - UV.x);
|
||||
alpha *= softness * 0.1;
|
||||
ALBEDO = tint;
|
||||
ALPHA = alpha;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
uid://b8dxberetmchf
|
||||
@@ -0,0 +1,271 @@
|
||||
class_name WindGustField
|
||||
extends Node3D
|
||||
|
||||
const GUST_SHADER := preload("res://world/jajce/materials/wind_gust.gdshader")
|
||||
const RIBBON_SEGMENTS := 18
|
||||
const STROKE_COLORS := [
|
||||
Color(0.76, 0.93, 0.74, 0.28),
|
||||
Color(1.0, 0.82, 0.52, 0.22),
|
||||
Color(0.64, 0.88, 0.92, 0.2),
|
||||
]
|
||||
|
||||
@export var terrain_path: NodePath = ^"../../TerrainRoot/Terrain3D"
|
||||
@export var wind_direction := Vector3(0.94, 0.0, 0.34)
|
||||
@export var view_spawn_extents := Vector2(8.5, 5.5)
|
||||
@export_range(5.0, 12.0, 0.1) var interval_min := 6.0
|
||||
@export_range(5.0, 12.0, 0.1) var interval_max := 10.0
|
||||
@export_range(1.0, 3.0, 0.1) var duration_min := 1.8
|
||||
@export_range(1.0, 3.0, 0.1) var duration_max := 2.35
|
||||
@export_range(1, 3, 1) var stroke_count_min := 2
|
||||
@export_range(1, 3, 1) var stroke_count_max := 3
|
||||
@export_range(0.5, 3.0, 0.1) var hover_height := 1.25
|
||||
@export var deterministic_seed := 20260711
|
||||
|
||||
var _random := RandomNumberGenerator.new()
|
||||
var _terrain: Node
|
||||
var _time_until_next_gust := 0.0
|
||||
var _active_burst: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_random.seed = deterministic_seed
|
||||
_terrain = get_node_or_null(terrain_path)
|
||||
_time_until_next_gust = _random.randf_range(2.0, 4.0)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_time_until_next_gust -= delta
|
||||
if not _active_burst.is_empty():
|
||||
_update_active_burst(delta)
|
||||
if _active_burst.is_empty() and _time_until_next_gust <= 0.0:
|
||||
_trigger_gust_at_world(_random_world_origin())
|
||||
|
||||
|
||||
func trigger_gust_at(
|
||||
local_origin: Vector3, replace_active: bool = false, initial_progress: float = 0.0
|
||||
) -> bool:
|
||||
return _trigger_gust_at_world(to_global(local_origin), replace_active, initial_progress)
|
||||
|
||||
|
||||
func _trigger_gust_at_world(
|
||||
world_origin: Vector3, replace_active: bool = false, initial_progress: float = 0.0
|
||||
) -> bool:
|
||||
if not _active_burst.is_empty():
|
||||
if not replace_active:
|
||||
return false
|
||||
_clear_active_burst(true)
|
||||
|
||||
var burst := Node3D.new()
|
||||
burst.name = "WindGustBurst"
|
||||
add_child(burst)
|
||||
var origin := world_origin
|
||||
var origin_ground_height := _sample_terrain_height(origin)
|
||||
origin.y = origin_ground_height + hover_height + _random.randf_range(0.15, 0.55)
|
||||
burst.global_position = origin
|
||||
|
||||
var duration := _random.randf_range(duration_min, duration_max)
|
||||
var stroke_count := _random.randi_range(stroke_count_min, stroke_count_max)
|
||||
var strokes: Array[Dictionary] = []
|
||||
var direction := wind_direction.normalized()
|
||||
var crosswind := Vector3.UP.cross(direction).normalized()
|
||||
for stroke_index in stroke_count:
|
||||
var stroke := MeshInstance3D.new()
|
||||
stroke.name = "Stroke_%02d" % (stroke_index + 1)
|
||||
stroke.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
var length := _random.randf_range(5.0, 7.8) * (1.0 - stroke_index * 0.08)
|
||||
var width := _random.randf_range(0.2, 0.32)
|
||||
var curve := _random.randf_range(0.18, 0.42)
|
||||
var phase := _random.randf_range(-0.7, 0.7)
|
||||
stroke.mesh = _create_ribbon_mesh(
|
||||
length, width, curve, phase, direction, origin, origin_ground_height
|
||||
)
|
||||
stroke.position = (
|
||||
crosswind * (float(stroke_index) - float(stroke_count - 1) * 0.5) * 0.62
|
||||
+ Vector3.UP * stroke_index * 0.12
|
||||
- direction * stroke_index * 0.25
|
||||
)
|
||||
var material := ShaderMaterial.new()
|
||||
material.shader = GUST_SHADER
|
||||
material.set_shader_parameter("tint", STROKE_COLORS[stroke_index])
|
||||
material.set_shader_parameter("progress", 0.0)
|
||||
stroke.material_override = material
|
||||
burst.add_child(stroke)
|
||||
(
|
||||
strokes
|
||||
. append(
|
||||
{
|
||||
"material": material,
|
||||
"delay": float(stroke_index) * 0.11,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
_active_burst = {
|
||||
"node": burst,
|
||||
"age": duration * clampf(initial_progress, 0.0, 0.95),
|
||||
"duration": duration,
|
||||
"drift_speed": _random.randf_range(0.35, 0.65),
|
||||
"strokes": strokes,
|
||||
}
|
||||
_apply_stroke_progress()
|
||||
_time_until_next_gust = _random.randf_range(interval_min, interval_max)
|
||||
return true
|
||||
|
||||
|
||||
func get_active_stroke_count() -> int:
|
||||
if _active_burst.is_empty():
|
||||
return 0
|
||||
return (_active_burst["strokes"] as Array).size()
|
||||
|
||||
|
||||
func _update_active_burst(delta: float) -> void:
|
||||
var burst := _active_burst["node"] as Node3D
|
||||
if burst == null or not is_instance_valid(burst):
|
||||
_active_burst.clear()
|
||||
return
|
||||
var age := float(_active_burst["age"]) + delta
|
||||
var duration := float(_active_burst["duration"])
|
||||
_active_burst["age"] = age
|
||||
burst.global_position += (
|
||||
wind_direction.normalized() * float(_active_burst["drift_speed"]) * delta
|
||||
)
|
||||
var desired_height := _sample_terrain_height(burst.global_position) + hover_height
|
||||
burst.global_position.y = lerpf(burst.global_position.y, desired_height, minf(delta * 2.0, 1.0))
|
||||
_apply_stroke_progress()
|
||||
if age >= duration:
|
||||
_clear_active_burst()
|
||||
|
||||
|
||||
func _apply_stroke_progress() -> void:
|
||||
var age := float(_active_burst["age"])
|
||||
var duration := float(_active_burst["duration"])
|
||||
for stroke_data in _active_burst["strokes"]:
|
||||
var delay := float(stroke_data["delay"])
|
||||
var stroke_progress := clampf((age - delay) / maxf(duration - delay, 0.01), 0.0, 1.0)
|
||||
var material := stroke_data["material"] as ShaderMaterial
|
||||
material.set_shader_parameter("progress", stroke_progress)
|
||||
|
||||
|
||||
func _clear_active_burst(free_immediately: bool = false) -> void:
|
||||
if _active_burst.is_empty():
|
||||
return
|
||||
var burst := _active_burst.get("node") as Node3D
|
||||
if burst != null and is_instance_valid(burst):
|
||||
if free_immediately:
|
||||
burst.free()
|
||||
else:
|
||||
burst.queue_free()
|
||||
_active_burst.clear()
|
||||
|
||||
|
||||
func _random_world_origin() -> Vector3:
|
||||
var camera := get_viewport().get_camera_3d()
|
||||
if camera == null:
|
||||
return to_global(
|
||||
Vector3(
|
||||
_random.randf_range(-view_spawn_extents.x, view_spawn_extents.x),
|
||||
0.0,
|
||||
_random.randf_range(-view_spawn_extents.y, view_spawn_extents.y),
|
||||
)
|
||||
)
|
||||
var center := _camera_ground_focus(camera)
|
||||
var camera_right := Vector3(camera.global_basis.x.x, 0.0, camera.global_basis.x.z).normalized()
|
||||
var camera_forward := (
|
||||
Vector3(-camera.global_basis.z.x, 0.0, -camera.global_basis.z.z).normalized()
|
||||
)
|
||||
return (
|
||||
center
|
||||
+ camera_right * _random.randf_range(-view_spawn_extents.x, view_spawn_extents.x)
|
||||
+ camera_forward * _random.randf_range(-view_spawn_extents.y, view_spawn_extents.y)
|
||||
)
|
||||
|
||||
|
||||
func _camera_ground_focus(camera: Camera3D) -> Vector3:
|
||||
var ray_origin := camera.global_position
|
||||
var ray_direction := -camera.global_basis.z.normalized()
|
||||
if ray_direction.y >= -0.01:
|
||||
return Vector3(ray_origin.x, _sample_terrain_height(ray_origin), ray_origin.z)
|
||||
var ground_height := _sample_terrain_height(global_position)
|
||||
var focus := global_position
|
||||
for _iteration in 3:
|
||||
var distance := (ground_height - ray_origin.y) / ray_direction.y
|
||||
if distance <= 0.0:
|
||||
break
|
||||
focus = ray_origin + ray_direction * distance
|
||||
ground_height = _sample_terrain_height(focus)
|
||||
focus.y = ground_height
|
||||
return focus
|
||||
|
||||
|
||||
func _sample_terrain_height(world_position: Vector3) -> float:
|
||||
if _terrain == null or not is_instance_valid(_terrain) or _terrain.get("data") == null:
|
||||
return global_position.y
|
||||
var terrain_height: float = _terrain.data.get_height(world_position)
|
||||
return global_position.y if is_nan(terrain_height) else terrain_height
|
||||
|
||||
|
||||
func _create_ribbon_mesh(
|
||||
length: float,
|
||||
base_width: float,
|
||||
curve_amount: float,
|
||||
phase: float,
|
||||
direction: Vector3,
|
||||
world_origin: Vector3,
|
||||
origin_ground_height: float
|
||||
) -> ArrayMesh:
|
||||
var centers := PackedVector3Array()
|
||||
var crosswind := Vector3.UP.cross(direction).normalized()
|
||||
for segment_index in range(RIBBON_SEGMENTS + 1):
|
||||
var t := float(segment_index) / RIBBON_SEGMENTS
|
||||
var envelope := sin(t * PI)
|
||||
var sweep := sin(t * TAU * 1.15 + phase) * curve_amount * envelope
|
||||
var lift := envelope * 0.12 + sin(t * TAU + phase) * envelope * 0.035
|
||||
var horizontal_offset := direction * ((t - 0.5) * length) + crosswind * sweep
|
||||
var terrain_offset := (
|
||||
_sample_terrain_height(world_origin + horizontal_offset) - origin_ground_height
|
||||
)
|
||||
centers.append(horizontal_offset + Vector3.UP * (terrain_offset + lift))
|
||||
|
||||
var vertices := PackedVector3Array()
|
||||
var uvs := PackedVector2Array()
|
||||
var indices := PackedInt32Array()
|
||||
for segment_index in range(RIBBON_SEGMENTS + 1):
|
||||
var t := float(segment_index) / RIBBON_SEGMENTS
|
||||
var previous := centers[maxi(segment_index - 1, 0)]
|
||||
var following := centers[mini(segment_index + 1, RIBBON_SEGMENTS)]
|
||||
var tangent := (following - previous).normalized()
|
||||
var side := Vector3.UP.cross(tangent).normalized()
|
||||
var taper := pow(maxf(sin(t * PI), 0.0), 0.72)
|
||||
var brush_belly := 1.0 + maxf(1.0 - absf(t - 0.34) / 0.34, 0.0) * 0.28
|
||||
var half_width := base_width * taper * brush_belly * 0.5
|
||||
vertices.append(centers[segment_index] - side * half_width)
|
||||
vertices.append(centers[segment_index] + side * half_width)
|
||||
uvs.append(Vector2(t, 0.0))
|
||||
uvs.append(Vector2(t, 1.0))
|
||||
if segment_index >= RIBBON_SEGMENTS:
|
||||
continue
|
||||
var vertex_index := segment_index * 2
|
||||
(
|
||||
indices
|
||||
. append_array(
|
||||
PackedInt32Array(
|
||||
[
|
||||
vertex_index,
|
||||
vertex_index + 1,
|
||||
vertex_index + 2,
|
||||
vertex_index + 1,
|
||||
vertex_index + 3,
|
||||
vertex_index + 2,
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
||||
arrays[Mesh.ARRAY_INDEX] = indices
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return mesh
|
||||
@@ -0,0 +1 @@
|
||||
uid://bceyn2pnyjfo7
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/jajce/vfx/WindGustField.gd" id="1_gust"]
|
||||
|
||||
[node name="WindGustField" type="Node3D"]
|
||||
script = ExtResource("1_gust")
|
||||
@@ -52,7 +52,15 @@ func _draw() -> void:
|
||||
var text_size := font.get_string_size(time_label, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size)
|
||||
var text_x := (SIZE_DIAL - text_size.x) / 2.0
|
||||
var text_y := SIZE_DIAL - text_size.y + 4.0
|
||||
draw_string(font, Vector2(text_x, text_y), time_label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color.WHITE)
|
||||
draw_string(
|
||||
font,
|
||||
Vector2(text_x, text_y),
|
||||
time_label,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
-1,
|
||||
font_size,
|
||||
Color.WHITE
|
||||
)
|
||||
|
||||
|
||||
func _cycle_fraction() -> float:
|
||||
|
||||
+91
-41
@@ -27,6 +27,10 @@ func _ready() -> void:
|
||||
simulation_manager.state_restored.connect(_on_state_restored)
|
||||
if simulation_manager.has_signal("speed_changed"):
|
||||
simulation_manager.speed_changed.connect(_on_speed_changed)
|
||||
if simulation_manager.has_signal("relationship_changed"):
|
||||
simulation_manager.relationship_changed.connect(_on_relationship_changed)
|
||||
if simulation_manager.has_signal("event_knowledge_changed"):
|
||||
simulation_manager.event_knowledge_changed.connect(_on_event_knowledge_changed)
|
||||
|
||||
if "village" in simulation_manager:
|
||||
_on_village_changed(simulation_manager.village)
|
||||
@@ -119,6 +123,16 @@ func _on_speed_changed(_multiplier: float) -> void:
|
||||
_on_village_changed(simulation_manager.village)
|
||||
|
||||
|
||||
func _on_relationship_changed(
|
||||
_relationship: RelationshipStateRecord, _cause_event: EconomicEventRecord
|
||||
) -> void:
|
||||
_refresh_npc_inspector()
|
||||
|
||||
|
||||
func _on_event_knowledge_changed(_knower_id: int, _event: EconomicEventRecord) -> void:
|
||||
_refresh_npc_inspector()
|
||||
|
||||
|
||||
func _refresh_npc_inspector() -> void:
|
||||
if not debug_overlay_visible:
|
||||
return
|
||||
@@ -147,44 +161,58 @@ func _refresh_npc_inspector() -> void:
|
||||
for action_id in score_keys:
|
||||
if decision.rejections.has(action_id):
|
||||
rejection_rows.append(
|
||||
"%s — %s"
|
||||
% [_get_action_name(StringName(action_id)), String(decision.rejections[action_id])]
|
||||
(
|
||||
"%s — %s"
|
||||
% [
|
||||
_get_action_name(StringName(action_id)),
|
||||
String(decision.rejections[action_id])
|
||||
]
|
||||
)
|
||||
)
|
||||
continue
|
||||
score_rows.append(
|
||||
"%s %.2f" % [_get_action_name(StringName(action_id)), float(decision.scores[action_id])]
|
||||
(
|
||||
"%s %.2f"
|
||||
% [_get_action_name(StringName(action_id)), float(decision.scores[action_id])]
|
||||
)
|
||||
)
|
||||
if not score_rows.is_empty():
|
||||
score_text = "\n\nUtility\n" + "\n".join(score_rows)
|
||||
if not rejection_rows.is_empty():
|
||||
score_text += "\n\nUnavailable\n" + "\n".join(rejection_rows)
|
||||
var event_text := _build_event_history(npc)
|
||||
var housemate_text := _build_housemate_display(npc)
|
||||
var relationship_text := _build_relationship_display(npc)
|
||||
var known_fact_text := _build_known_fact_display(npc)
|
||||
npc_inspector_label.text = (
|
||||
"%s · %s\n"
|
||||
+ "Task: %s (%s)\n"
|
||||
+ "Destination: %s\n"
|
||||
+ "Hunger: %.0f Energy: %.0f\n"
|
||||
+ "Carrying food: %.0f wood: %.0f\n"
|
||||
+ "%s\n"
|
||||
+ "Why\n%s%s\n\n"
|
||||
+ "Recent\n%s\n\n"
|
||||
+ "[Tab] Next villager [F10] Cinematic/debug [F12] Demo reset"
|
||||
) % [
|
||||
npc.npc_name,
|
||||
profession_name,
|
||||
action_name,
|
||||
npc.task_state,
|
||||
destination_name,
|
||||
npc.hunger,
|
||||
npc.energy,
|
||||
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD),
|
||||
npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD),
|
||||
housemate_text,
|
||||
reason_text,
|
||||
score_text,
|
||||
event_text
|
||||
]
|
||||
(
|
||||
"%s · %s\n"
|
||||
+ "Task: %s (%s)\n"
|
||||
+ "Destination: %s\n"
|
||||
+ "Hunger: %.0f Energy: %.0f\n"
|
||||
+ "Carrying food: %.0f wood: %.0f\n"
|
||||
+ "%s\n"
|
||||
+ "%s\n"
|
||||
+ "Why\n%s%s\n\n"
|
||||
+ "Recent\n%s\n\n"
|
||||
+ "[Tab] Next villager [F10] Cinematic/debug [F12] Demo reset"
|
||||
)
|
||||
% [
|
||||
npc.npc_name,
|
||||
profession_name,
|
||||
action_name,
|
||||
npc.task_state,
|
||||
destination_name,
|
||||
npc.hunger,
|
||||
npc.energy,
|
||||
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD),
|
||||
npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD),
|
||||
relationship_text,
|
||||
known_fact_text,
|
||||
reason_text,
|
||||
score_text,
|
||||
event_text
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
func _get_action_name(action_id: StringName) -> String:
|
||||
@@ -220,22 +248,44 @@ func _build_event_history(npc: SimNPC) -> String:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
func _build_housemate_display(npc: SimNPC) -> String:
|
||||
if npc.familiarity.is_empty():
|
||||
func _build_relationship_display(npc: SimNPC) -> String:
|
||||
if not simulation_manager.has_method("get_primary_relationship"):
|
||||
return ""
|
||||
var highest_id: int = -1
|
||||
var highest_score := -1.0
|
||||
for key in npc.familiarity:
|
||||
var other_id: int = int(key)
|
||||
var score: float = float(npc.familiarity[key])
|
||||
if score > highest_score:
|
||||
highest_score = score
|
||||
highest_id = other_id
|
||||
if highest_id < 0:
|
||||
var relationship: RelationshipStateRecord = simulation_manager.get_primary_relationship(npc.id)
|
||||
if relationship == null:
|
||||
return ""
|
||||
var other_name := "Someone"
|
||||
for other_npc in simulation_manager.npcs:
|
||||
if other_npc.id == highest_id:
|
||||
if other_npc.id == relationship.get_subject_id():
|
||||
other_name = other_npc.npc_name
|
||||
break
|
||||
return "Familiar: %s (%.0f%%)" % [other_name, highest_score * 100.0]
|
||||
var display := (
|
||||
"Relationship: %s — familiar %.0f%%, trust %.0f%%"
|
||||
% [
|
||||
other_name,
|
||||
relationship.get_familiarity() * 100.0,
|
||||
relationship.get_trust() * 100.0,
|
||||
]
|
||||
)
|
||||
if not simulation_manager.has_method("get_relationship_cause"):
|
||||
return display
|
||||
var cause: EconomicEventRecord = simulation_manager.get_relationship_cause(relationship)
|
||||
if cause == null:
|
||||
return display
|
||||
var name_map := {}
|
||||
for other_npc in simulation_manager.npcs:
|
||||
name_map[other_npc.id] = other_npc.npc_name
|
||||
return "%s\nBecause: %s" % [display, cause.description(name_map)]
|
||||
|
||||
|
||||
func _build_known_fact_display(npc: SimNPC) -> String:
|
||||
if not simulation_manager.has_method("get_known_events"):
|
||||
return ""
|
||||
var known_events: Array = simulation_manager.get_known_events(npc.id, 1)
|
||||
if known_events.is_empty():
|
||||
return "Known fact: none yet"
|
||||
var name_map := {}
|
||||
for other_npc in simulation_manager.npcs:
|
||||
name_map[other_npc.id] = other_npc.npc_name
|
||||
var latest_event := known_events[known_events.size() - 1] as EconomicEventRecord
|
||||
return "Known fact: %s" % latest_event.description(name_map)
|
||||
|
||||
Reference in New Issue
Block a user