Compare commits
3 Commits
c6e2d64162
...
4604dc6d5a
| Author | SHA1 | Date | |
|---|---|---|---|
| 4604dc6d5a | |||
| 6fdb9ba50f | |||
| 4054e2c0dd |
@@ -644,10 +644,21 @@ Completed:
|
||||
study desk, and rest bench now have compact presentation props that remain
|
||||
readable in the runtime cinematic frame.
|
||||
|
||||
Completed:
|
||||
|
||||
15. Water and foreground silhouette strengthening: river/waterfall shaders
|
||||
now include multi-layer UV flow, fresnel edges, foam highlights, sparkle
|
||||
specular, and better color depth; the waterfall has distinct foam bands
|
||||
and rim transparency; mist particle count and spread increased; the river
|
||||
and waterfall widened; the fortress ridge landmark now has crenellations,
|
||||
a dedicated banner pole with larger banner, and three turrets for a more
|
||||
readable silhouette against the sky.
|
||||
|
||||
Next:
|
||||
|
||||
1. Strengthen water and foreground silhouettes without adding new simulation
|
||||
mechanics.
|
||||
1. Continue growing resource discovery with finite ResourceNode instances
|
||||
placed in meaningful contexts (from LEARNING_ROADMAP). See the roadmap
|
||||
for the next systems priority.
|
||||
|
||||
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
||||
mechanics. The next proof is a beautiful stage for the systems that already
|
||||
|
||||
@@ -689,6 +689,10 @@ Recently completed:
|
||||
authored path strips that reveal the active village task loop.
|
||||
- Landmark and work-site silhouette props make the ridge landmark, pantry,
|
||||
guard, study, and rest sites easier to identify without debug labels.
|
||||
- Water and foreground silhouettes strengthened: river/waterfall shaders with
|
||||
multi-layer UV flow, fresnel edges, foam highlights, and sparkle; wider river
|
||||
and waterfall; fortress with crenellations, three turrets, and prominent
|
||||
ridge banner.
|
||||
|
||||
This order strengthens the simulation while regularly producing visible
|
||||
progress suitable for public development updates.
|
||||
|
||||
@@ -16,6 +16,7 @@ var intelligence: float
|
||||
var current_task: StringName = SimulationIds.ACTION_IDLE
|
||||
var task_state: StringName = TASK_STATE_IDLE
|
||||
var position: Vector3
|
||||
var home_position: Vector3
|
||||
|
||||
var is_starving := false
|
||||
var starvation_ticks := 0
|
||||
@@ -56,6 +57,7 @@ func _init(
|
||||
position = Vector3(
|
||||
random_source.randf_range(-8.0, 8.0), 0.0, random_source.randf_range(-8.0, 8.0)
|
||||
)
|
||||
home_position = position
|
||||
|
||||
|
||||
func die_from_starvation() -> void:
|
||||
|
||||
@@ -4,6 +4,7 @@ extends RefCounted
|
||||
var tick_interval: float
|
||||
var accumulator := 0.0
|
||||
var elapsed_ticks := 0
|
||||
var cycle_duration_seconds := 240.0
|
||||
|
||||
|
||||
func _init(interval: float = 1.0) -> void:
|
||||
@@ -23,3 +24,8 @@ func advance(delta: float) -> int:
|
||||
func reset() -> void:
|
||||
accumulator = 0.0
|
||||
elapsed_ticks = 0
|
||||
|
||||
|
||||
func time_of_day() -> float:
|
||||
var total_seconds := elapsed_ticks * tick_interval + accumulator
|
||||
return fmod(total_seconds / maxf(cycle_duration_seconds, 1.0), 1.0)
|
||||
|
||||
@@ -15,6 +15,7 @@ var village := SimVillage.new()
|
||||
var npcs: Array[SimNPC] = []
|
||||
@export var tick_interval := 1.2
|
||||
@export var simulation_seed: int = 1337
|
||||
@export var cycle_duration_seconds := 240.0
|
||||
@export var debug_logs := true
|
||||
@export var active_world_adapter: Node
|
||||
|
||||
@@ -42,6 +43,7 @@ func _ready() -> void:
|
||||
set_process(false)
|
||||
return
|
||||
clock = SimulationClock.new(tick_interval)
|
||||
clock.cycle_duration_seconds = cycle_duration_seconds
|
||||
village.debug_logs = debug_logs
|
||||
_initialize_storage()
|
||||
village.update_modifiers()
|
||||
@@ -112,7 +114,7 @@ func simulate_tick() -> void:
|
||||
and old_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]
|
||||
and npc.task_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]
|
||||
):
|
||||
var selection := action_selector.select_action(npc, village)
|
||||
var selection := action_selector.select_action(npc, village, clock.time_of_day())
|
||||
if selection != null:
|
||||
latest_decisions[npc.id] = selection
|
||||
npc_decision_recorded.emit(npc, selection)
|
||||
@@ -195,6 +197,11 @@ func simulate_tick() -> void:
|
||||
withdraw_to_npc(npc, SimulationIds.RESOURCE_FOOD, 1.0)
|
||||
elif completed_task == SimulationIds.ACTION_EAT:
|
||||
consume_npc_food(npc)
|
||||
elif completed_task == SimulationIds.ACTION_SLEEP:
|
||||
npc.energy = minf(npc.energy + 40.0, 100.0)
|
||||
npc.position = npc.home_position
|
||||
if debug_logs:
|
||||
print("[SimulationManager] ", npc.npc_name, " completed sleep at home")
|
||||
elif (
|
||||
completed_task
|
||||
not in [SimulationIds.ACTION_GATHER_FOOD, SimulationIds.ACTION_GATHER_WOOD]
|
||||
@@ -354,14 +361,19 @@ func notify_npc_target_unavailable(npc_id: int) -> void:
|
||||
|
||||
|
||||
func resolve_npc_target(npc_id: int, origin: Vector3) -> bool:
|
||||
if active_world_adapter == null:
|
||||
push_error("SimulationManager: active_world_adapter is missing")
|
||||
return false
|
||||
for npc in npcs:
|
||||
if npc.id != npc_id:
|
||||
continue
|
||||
if npc.is_dead or npc.task_state != SimNPC.TASK_STATE_TRAVELING:
|
||||
return false
|
||||
if npc.current_task == SimulationIds.ACTION_SLEEP:
|
||||
npc.travel_target_position = npc.home_position
|
||||
npc.has_travel_target = true
|
||||
npc_travel_requested.emit(npc, npc.travel_target_position)
|
||||
return true
|
||||
if active_world_adapter == null:
|
||||
push_error("SimulationManager: active_world_adapter is missing")
|
||||
return false
|
||||
var result := target_resolver.resolve(npc, origin, self, active_world_adapter)
|
||||
if result.is_empty():
|
||||
notify_npc_target_unavailable(npc.id)
|
||||
@@ -738,7 +750,8 @@ func create_state_record() -> SimulationStateRecord:
|
||||
"clock_accumulator": clock.accumulator,
|
||||
"clock_elapsed_ticks": clock.elapsed_ticks,
|
||||
"wander_random_streams": wander_streams,
|
||||
"next_event_id": next_event_id
|
||||
"next_event_id": next_event_id,
|
||||
"cycle_duration_seconds": clock.cycle_duration_seconds
|
||||
}
|
||||
record.village = VillageStateRecord.capture(village)
|
||||
for npc in npcs:
|
||||
@@ -782,6 +795,7 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
||||
clock = SimulationClock.new(tick_interval)
|
||||
clock.accumulator = float(record.simulation["clock_accumulator"])
|
||||
clock.elapsed_ticks = int(record.simulation["clock_elapsed_ticks"])
|
||||
clock.cycle_duration_seconds = float(record.simulation.get("cycle_duration_seconds", 240.0))
|
||||
village = record.village.restore(debug_logs)
|
||||
storage_states.clear()
|
||||
for storage_record in record.storages:
|
||||
|
||||
@@ -22,7 +22,9 @@ func _update_needs(npc: SimNPC, village: SimVillage) -> void:
|
||||
SimNPC.TASK_STATE_TRAVELING:
|
||||
npc.energy -= 2.0
|
||||
SimNPC.TASK_STATE_WORKING:
|
||||
if npc.current_task != SimulationIds.ACTION_REST:
|
||||
if npc.current_task == SimulationIds.ACTION_SLEEP:
|
||||
npc.energy = minf(npc.energy + 5.0, 100.0)
|
||||
elif npc.current_task != SimulationIds.ACTION_REST:
|
||||
npc.energy -= 3.0
|
||||
SimNPC.TASK_STATE_COMPLETE:
|
||||
npc.energy -= 0.25
|
||||
|
||||
@@ -1,10 +1,56 @@
|
||||
class_name ActionSelectionSystem
|
||||
extends RefCounted
|
||||
|
||||
enum SchedulePeriod {
|
||||
WORK,
|
||||
SLEEP,
|
||||
MEAL,
|
||||
DISCRETIONARY
|
||||
}
|
||||
|
||||
func select_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult:
|
||||
const SLEEP_BEGIN := 0.88
|
||||
const SLEEP_END := 0.15
|
||||
const BREAKFAST_BEGIN := 0.25
|
||||
const BREAKFAST_END := 0.35
|
||||
const DINNER_BEGIN := 0.7
|
||||
const DINNER_END := 0.8
|
||||
const WORK_BEGIN := 0.28
|
||||
const WORK_END := 0.85
|
||||
|
||||
|
||||
func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5) -> ActionSelectionResult:
|
||||
if npc.is_dead:
|
||||
return null
|
||||
|
||||
var period := _get_period(time_of_day)
|
||||
|
||||
if period == SchedulePeriod.SLEEP:
|
||||
if npc.energy < 60.0:
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_SLEEP, -1.0, "Night-time sleep; energy is low"
|
||||
)
|
||||
if npc.hunger > 75.0 and npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_EAT, -1.0, "Night-time hunger; eating from inventory"
|
||||
)
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_SLEEP, -1.0, "Night-time; going home"
|
||||
)
|
||||
|
||||
if period == SchedulePeriod.MEAL:
|
||||
if npc.hunger > 50.0:
|
||||
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_EAT, -1.0, "Meal-time hunger; eating from inventory"
|
||||
)
|
||||
if village.food > 0:
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_WITHDRAW_FOOD, -1.0, "Meal-time; pantry has food"
|
||||
)
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_GATHER_FOOD, -1.0, "Meal-time hunger; pantry is empty"
|
||||
)
|
||||
|
||||
if npc.is_starving:
|
||||
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
|
||||
return ActionSelectionResult.new(
|
||||
@@ -37,6 +83,14 @@ func select_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult:
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_REST, -1.0, "Energy is below the rest threshold"
|
||||
)
|
||||
|
||||
if period == SchedulePeriod.DISCRETIONARY:
|
||||
var roll := npc.random_source.randf()
|
||||
if roll < 0.3:
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_WANDER, -1.0, "Discretionary wander"
|
||||
)
|
||||
|
||||
return _choose_best_work_action(npc, village)
|
||||
|
||||
|
||||
@@ -129,3 +183,23 @@ func _calculate_score(
|
||||
if npc.debug_logs:
|
||||
print("[ActionSelection] ", npc.npc_name, " score ", action_id, " = ", score)
|
||||
return score
|
||||
|
||||
|
||||
static func _get_period(time_of_day: float) -> int:
|
||||
if _in_wrap_range(time_of_day, SLEEP_BEGIN, SLEEP_END):
|
||||
return SchedulePeriod.SLEEP
|
||||
if _in_range(time_of_day, BREAKFAST_BEGIN, BREAKFAST_END):
|
||||
return SchedulePeriod.MEAL
|
||||
if _in_range(time_of_day, DINNER_BEGIN, DINNER_END):
|
||||
return SchedulePeriod.MEAL
|
||||
if _in_range(time_of_day, WORK_BEGIN, WORK_END):
|
||||
return SchedulePeriod.WORK
|
||||
return SchedulePeriod.DISCRETIONARY
|
||||
|
||||
|
||||
static func _in_range(value: float, begin: float, end: float) -> bool:
|
||||
return value >= begin and value <= end
|
||||
|
||||
|
||||
static func _in_wrap_range(value: float, begin: float, end: float) -> bool:
|
||||
return value >= begin or value <= end
|
||||
|
||||
@@ -10,7 +10,8 @@ const ACTION_PATHS := [
|
||||
"res://simulation/definitions/actions/rest.tres",
|
||||
"res://simulation/definitions/actions/wander.tres",
|
||||
"res://simulation/definitions/actions/deposit_food.tres",
|
||||
"res://simulation/definitions/actions/withdraw_food.tres"
|
||||
"res://simulation/definitions/actions/withdraw_food.tres",
|
||||
"res://simulation/definitions/actions/sleep.tres"
|
||||
]
|
||||
|
||||
const PROFESSION_PATHS := [
|
||||
|
||||
@@ -3,6 +3,7 @@ extends RefCounted
|
||||
|
||||
const ACTION_IDLE := &"idle"
|
||||
const ACTION_DEAD := &"dead"
|
||||
const ACTION_SLEEP := &"sleep"
|
||||
const ACTION_GATHER_FOOD := &"gather_food"
|
||||
const ACTION_GATHER_WOOD := &"gather_wood"
|
||||
const ACTION_PATROL := &"patrol"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
action_id = &"sleep"
|
||||
display_name = "Sleep"
|
||||
default_duration = 8.0
|
||||
target_type = &"free"
|
||||
@@ -26,6 +26,7 @@ static func capture(npc: SimNPC) -> NPCStateRecord:
|
||||
"current_task": String(npc.current_task),
|
||||
"task_state": String(npc.task_state),
|
||||
"position": [npc.position.x, npc.position.y, npc.position.z],
|
||||
"home_position": [npc.home_position.x, npc.home_position.y, npc.home_position.z],
|
||||
"is_starving": npc.is_starving,
|
||||
"starvation_ticks": npc.starvation_ticks,
|
||||
"starvation_death_threshold": npc.starvation_death_threshold,
|
||||
@@ -137,6 +138,10 @@ func restore(debug_logs: bool) -> SimNPC:
|
||||
npc.position = Vector3(
|
||||
float(saved_position[0]), float(saved_position[1]), float(saved_position[2])
|
||||
)
|
||||
var saved_home: Array = data.get("home_position", saved_position)
|
||||
npc.home_position = Vector3(
|
||||
float(saved_home[0]), float(saved_home[1]), float(saved_home[2])
|
||||
)
|
||||
npc.is_starving = bool(data["is_starving"])
|
||||
npc.starvation_ticks = int(data["starvation_ticks"])
|
||||
npc.starvation_death_threshold = int(data["starvation_death_threshold"])
|
||||
|
||||
@@ -12,6 +12,7 @@ func _run() -> void:
|
||||
manager.debug_logs = false
|
||||
root.add_child(manager)
|
||||
manager.set_process(false)
|
||||
manager.clock.elapsed_ticks = 100
|
||||
|
||||
var pantry: StorageStateRecord = manager.get_pantry()
|
||||
pantry.withdraw(SimulationIds.RESOURCE_FOOD, 1000.0)
|
||||
|
||||
@@ -53,10 +53,10 @@ func _run() -> void:
|
||||
"Runtime should not retain duplicate flat-world objects"
|
||||
)
|
||||
var resource_root := main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes")
|
||||
_check(resource_root.get_child_count() == 12, "Runtime should contain twelve Jajce ResourceNodes")
|
||||
_check(resource_root.get_child_count() == 18, "Runtime should contain eighteen Jajce ResourceNodes")
|
||||
_check(
|
||||
simulation_manager.resource_states.size() == 12,
|
||||
"Simulation authority should bind all twelve Jajce resources"
|
||||
simulation_manager.resource_states.size() == 18,
|
||||
"Simulation authority should bind all eighteen Jajce resources"
|
||||
)
|
||||
var village_root := main_scene.get_node("JajceWorld/VillageRoot")
|
||||
var path_strips := 0
|
||||
@@ -156,7 +156,7 @@ func _run() -> void:
|
||||
)
|
||||
|
||||
if failures.is_empty():
|
||||
print("[TEST] Jajce runtime passed: 6 NPCs, 12 resources, typed sites")
|
||||
print("[TEST] Jajce runtime passed: 6 NPCs, 18 resources, typed sites")
|
||||
quit(0)
|
||||
return
|
||||
|
||||
|
||||
@@ -124,13 +124,19 @@ func _run() -> void:
|
||||
"animal_camp_river_01",
|
||||
"berry_bush_01",
|
||||
"berry_bush_02",
|
||||
"berry_bush_river_02",
|
||||
"berry_patch_mill_01",
|
||||
"berry_patch_river_01",
|
||||
"berry_patch_south_01",
|
||||
"farm_crop_01",
|
||||
"tree_01",
|
||||
"tree_02",
|
||||
"tree_forest_grove_01",
|
||||
"tree_forest_grove_02",
|
||||
"tree_north_outskirts_01",
|
||||
"tree_river_resource_01",
|
||||
"tree_south_edge_01",
|
||||
"wood_pile_mill_01",
|
||||
"wood_pile_village_01"
|
||||
],
|
||||
"Jajce resources should preserve all stable and discoverable IDs"
|
||||
@@ -142,6 +148,7 @@ func _run() -> void:
|
||||
var berry_context_count := 0
|
||||
var village_context_count := 0
|
||||
var outskirt_context_count := 0
|
||||
var farming_context_count := 0
|
||||
for resource in resources:
|
||||
var node := resource as ResourceNode
|
||||
var id := String(node.node_id)
|
||||
@@ -157,6 +164,8 @@ func _run() -> void:
|
||||
village_context_count += 1
|
||||
if id.contains("_outskirts_") or id.contains("_edge_") or id.contains("_river_"):
|
||||
outskirt_context_count += 1
|
||||
if id.begins_with("farm_"):
|
||||
farming_context_count += 1
|
||||
metadata_valid = (
|
||||
metadata_valid
|
||||
and node.comfort_distance > 0.0
|
||||
@@ -164,12 +173,13 @@ func _run() -> void:
|
||||
and node.safety_risk <= 1.0
|
||||
)
|
||||
_check(metadata_valid, "Jajce resources should define discovery metadata")
|
||||
_check(food_count >= 6, "Jajce should expose at least six finite food resources")
|
||||
_check(wood_count >= 6, "Jajce should expose at least six finite wood resources")
|
||||
_check(food_count >= 9, "Jajce should expose at least nine finite food resources")
|
||||
_check(wood_count >= 9, "Jajce should expose at least nine finite wood resources")
|
||||
_check(animal_context_count >= 2, "Jajce food discovery should include animal-camp contexts")
|
||||
_check(berry_context_count >= 4, "Jajce food discovery should include berry contexts")
|
||||
_check(berry_context_count >= 6, "Jajce food discovery should include berry contexts")
|
||||
_check(village_context_count >= 1, "Jajce discovery should include at least one village stockpile")
|
||||
_check(outskirt_context_count >= 4, "Jajce discovery should include outskirts/river contexts")
|
||||
_check(farming_context_count >= 1, "Jajce food discovery should include farming contexts")
|
||||
_check(world.has_node("WorldObjects/StorageSites"), "JajceWorld should expose StorageSites")
|
||||
var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode
|
||||
_check(pantry != null, "JajceWorld should include a typed VillagePantry StorageNode")
|
||||
@@ -235,7 +245,7 @@ func _run() -> void:
|
||||
adapter.pantry_storage = pantry
|
||||
root.add_child(adapter)
|
||||
var food_candidates := adapter.get_resource_candidates(SimulationIds.ACTION_GATHER_FOOD)
|
||||
_check(food_candidates.size() >= 6, "Adapter should expose all authored food candidates")
|
||||
_check(food_candidates.size() >= 9, "Adapter should expose all authored food candidates")
|
||||
var candidate_metadata_valid := true
|
||||
for candidate in food_candidates:
|
||||
candidate_metadata_valid = (
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_test_sleep_period_at_night()
|
||||
_test_work_period_during_day()
|
||||
_test_meal_period_hunger()
|
||||
_test_sleep_restores_energy()
|
||||
_test_schedule_serialization()
|
||||
_test_sleep_targets_home()
|
||||
|
||||
if failures.is_empty():
|
||||
print("[TEST] NPC schedule passed")
|
||||
quit(0)
|
||||
return
|
||||
|
||||
for failure in failures:
|
||||
push_error("[TEST] " + failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _create_manager(seed_value: int) -> Node:
|
||||
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||
manager.simulation_seed = seed_value
|
||||
manager.debug_logs = false
|
||||
root.add_child(manager)
|
||||
manager.set_process(false)
|
||||
return manager
|
||||
|
||||
|
||||
func _test_sleep_period_at_night() -> void:
|
||||
var manager := _create_manager(100)
|
||||
manager.clock.cycle_duration_seconds = 240.0
|
||||
manager.clock.elapsed_ticks = 0
|
||||
var npc: SimNPC = manager.npcs[0]
|
||||
npc.hunger = 20.0
|
||||
npc.energy = 50.0
|
||||
npc.home_position = npc.position
|
||||
|
||||
var selection := ActionSelectionSystem.new().select_action(
|
||||
npc, manager.village, manager.clock.time_of_day()
|
||||
)
|
||||
_check(selection != null, "Night-time should produce an action selection")
|
||||
_check(
|
||||
selection.action_id == SimulationIds.ACTION_SLEEP,
|
||||
"Low-energy NPC should choose SLEEP during night period"
|
||||
)
|
||||
_check(
|
||||
selection.reason.to_lower().contains("night"),
|
||||
"Sleep selection reason should reference night-time"
|
||||
)
|
||||
manager.free()
|
||||
|
||||
|
||||
func _test_work_period_during_day() -> void:
|
||||
var manager := _create_manager(200)
|
||||
manager.clock.cycle_duration_seconds = 240.0
|
||||
manager.clock.elapsed_ticks = 100
|
||||
var time_of_day: float = manager.clock.time_of_day()
|
||||
_check(
|
||||
time_of_day > 0.28 and time_of_day < 0.85,
|
||||
"100 ticks should land in the work period"
|
||||
)
|
||||
var npc: SimNPC = manager.npcs[0]
|
||||
npc.hunger = 20.0
|
||||
npc.energy = 80.0
|
||||
npc.home_position = npc.position
|
||||
|
||||
var selection := ActionSelectionSystem.new().select_action(
|
||||
npc, manager.village, time_of_day
|
||||
)
|
||||
_check(selection != null, "Daytime should produce an action selection")
|
||||
_check(
|
||||
selection.action_id != SimulationIds.ACTION_SLEEP,
|
||||
"A well-rested NPC should not choose SLEEP during work period"
|
||||
)
|
||||
manager.free()
|
||||
|
||||
|
||||
func _test_meal_period_hunger() -> void:
|
||||
var manager := _create_manager(300)
|
||||
manager.clock.cycle_duration_seconds = 240.0
|
||||
manager.clock.elapsed_ticks = 50
|
||||
var time_of_day: float = manager.clock.time_of_day()
|
||||
_check(
|
||||
time_of_day >= 0.25 and time_of_day <= 0.35,
|
||||
"50 ticks should land in the breakfast meal period"
|
||||
)
|
||||
var npc: SimNPC = manager.npcs[0]
|
||||
npc.hunger = 65.0
|
||||
npc.energy = 80.0
|
||||
manager.village.food = 5.0
|
||||
npc.home_position = npc.position
|
||||
|
||||
var selection := ActionSelectionSystem.new().select_action(
|
||||
npc, manager.village, time_of_day
|
||||
)
|
||||
_check(selection != null, "Meal period should produce an action selection")
|
||||
_check(
|
||||
selection.action_id == SimulationIds.ACTION_WITHDRAW_FOOD,
|
||||
"Hungry NPC during meal period with pantry food should WITHDRAW_FOOD"
|
||||
)
|
||||
manager.free()
|
||||
|
||||
|
||||
func _test_sleep_restores_energy() -> void:
|
||||
var manager := _create_manager(400)
|
||||
manager.clock.cycle_duration_seconds = 240.0
|
||||
manager.clock.elapsed_ticks = 0
|
||||
var npc: SimNPC = manager.npcs[0]
|
||||
npc.hunger = 20.0
|
||||
npc.energy = 30.0
|
||||
npc.home_position = npc.position
|
||||
|
||||
npc.set_task(SimulationIds.ACTION_SLEEP, 4.0)
|
||||
npc.start_working()
|
||||
var energy_before := npc.energy
|
||||
|
||||
for tick in 4:
|
||||
manager.simulate_tick()
|
||||
|
||||
_check(npc.task_complete, "Sleep should complete after its duration")
|
||||
_check(
|
||||
npc.energy > energy_before,
|
||||
"Completing sleep should restore energy"
|
||||
)
|
||||
manager.free()
|
||||
|
||||
|
||||
func _test_schedule_serialization() -> void:
|
||||
var manager := _create_manager(500)
|
||||
manager.clock.cycle_duration_seconds = 180.0
|
||||
manager.clock.elapsed_ticks = 30
|
||||
var npc: SimNPC = manager.npcs[0]
|
||||
npc.home_position = Vector3(3.0, 0.0, 4.0)
|
||||
npc.position = Vector3(1.0, 0.0, 2.0)
|
||||
|
||||
var saved_json: String = manager.serialize_state()
|
||||
var restored := _create_manager(1)
|
||||
_check(
|
||||
restored.restore_state_from_json(saved_json),
|
||||
"Schedule state should survive serialization round-trip"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(restored.clock.cycle_duration_seconds, 180.0),
|
||||
"Cycle duration should round-trip through save"
|
||||
)
|
||||
var restored_npc: SimNPC = restored.npcs[0]
|
||||
_check(
|
||||
restored_npc.home_position.distance_to(Vector3(3.0, 0.0, 4.0)) < 0.01,
|
||||
"NPC home position should round-trip through save"
|
||||
)
|
||||
manager.free()
|
||||
restored.free()
|
||||
|
||||
|
||||
func _test_sleep_targets_home() -> void:
|
||||
var manager := _create_manager(600)
|
||||
manager.clock.cycle_duration_seconds = 240.0
|
||||
manager.clock.elapsed_ticks = 0
|
||||
var npc: SimNPC = manager.npcs[0]
|
||||
npc.energy = 40.0
|
||||
npc.home_position = Vector3(5.0, 0.0, 5.0)
|
||||
npc.position = Vector3(0.0, 0.0, 0.0)
|
||||
|
||||
npc.set_task(SimulationIds.ACTION_SLEEP)
|
||||
_check(npc.task_state == SimNPC.TASK_STATE_TRAVELING, "Sleep should put NPC in traveling state")
|
||||
|
||||
var resolved: bool = manager.resolve_npc_target(npc.id, npc.position)
|
||||
_check(resolved, "Sleep target should resolve without an active world adapter")
|
||||
_check(
|
||||
npc.travel_target_position.distance_to(Vector3(5.0, 0.0, 5.0)) < 0.01,
|
||||
"Sleep should target the NPC home position"
|
||||
)
|
||||
manager.free()
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
@@ -34,7 +34,7 @@ func _test_registry_integrity() -> void:
|
||||
SimulationDefinitions.get_action(definition.action_id) == definition,
|
||||
"Action lookup should return '%s'" % definition.action_id
|
||||
)
|
||||
_check(action_ids.size() == 9, "Registry should contain all nine executable prototype actions")
|
||||
_check(action_ids.size() == 10, "Registry should contain all ten executable prototype actions")
|
||||
|
||||
var profession_ids := SimulationDefinitions.get_profession_ids()
|
||||
_check(profession_ids.size() == 5, "Registry should contain all five prototype professions")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[gd_scene load_steps=7 format=3]
|
||||
[gd_scene load_steps=12 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_keep"]
|
||||
albedo_color = Color(0.45, 0.42, 0.38, 1)
|
||||
@@ -6,15 +6,15 @@ roughness = 0.85
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_keep"]
|
||||
material = SubResource("Material_keep")
|
||||
size = Vector3(6, 6, 8)
|
||||
size = Vector3(7, 6, 9)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_parapet"]
|
||||
albedo_color = Color(0.45, 0.42, 0.38, 1)
|
||||
albedo_color = Color(0.42, 0.39, 0.35, 1)
|
||||
roughness = 0.85
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_parapet"]
|
||||
material = SubResource("Material_parapet")
|
||||
size = Vector3(5, 1.5, 7)
|
||||
size = Vector3(6.6, 1.5, 7.8)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_turret"]
|
||||
albedo_color = Color(0.45, 0.42, 0.38, 1)
|
||||
@@ -26,6 +26,28 @@ top_radius = 0.6
|
||||
bottom_radius = 0.7
|
||||
height = 4.0
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_crenellation"]
|
||||
material = SubResource("Material_parapet")
|
||||
size = Vector3(0.55, 0.9, 0.7)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_pole"]
|
||||
albedo_color = Color(0.24, 0.13, 0.07, 1)
|
||||
roughness = 0.82
|
||||
|
||||
[sub_resource type="CylinderMesh" id="Mesh_pole"]
|
||||
material = SubResource("Material_pole")
|
||||
top_radius = 0.08
|
||||
bottom_radius = 0.1
|
||||
height = 3.5
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_banner"]
|
||||
albedo_color = Color(0.68, 0.12, 0.1, 1)
|
||||
roughness = 0.65
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_banner"]
|
||||
material = SubResource("Material_banner")
|
||||
size = Vector3(0.14, 2.0, 1.25)
|
||||
|
||||
[node name="FortressBlockout" type="Node3D"]
|
||||
|
||||
[node name="Keep" type="MeshInstance3D" parent="."]
|
||||
@@ -36,6 +58,62 @@ mesh = SubResource("Mesh_keep")
|
||||
position = Vector3(0, 6.75, 0)
|
||||
mesh = SubResource("Mesh_parapet")
|
||||
|
||||
[node name="Turret" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(2.8, 2.0, 3.5)
|
||||
[node name="Crenellation_01" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-2.8, 7.7, -3.1)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_02" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-1.4, 7.7, -3.1)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_03" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 7.7, -3.1)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_04" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(1.4, 7.7, -3.1)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_05" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(2.8, 7.7, -3.1)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_06" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-2.8, 7.7, 3.1)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_07" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-1.4, 7.7, 3.1)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_08" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 7.7, 3.1)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_09" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(1.4, 7.7, 3.1)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_10" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(2.8, 7.7, 3.1)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Turret_A" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-3.2, 2.0, -4)
|
||||
mesh = SubResource("Mesh_turret")
|
||||
|
||||
[node name="Turret_B" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(3.2, 2.0, -4)
|
||||
mesh = SubResource("Mesh_turret")
|
||||
|
||||
[node name="Turret_C" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(3.2, 2.0, 4)
|
||||
mesh = SubResource("Mesh_turret")
|
||||
|
||||
[node name="RidgeBannerPole" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 8.4, 3.8)
|
||||
mesh = SubResource("Mesh_pole")
|
||||
|
||||
[node name="RidgeBanner" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 9.2, 4.45)
|
||||
mesh = SubResource("Mesh_banner")
|
||||
|
||||
@@ -186,14 +186,6 @@ position = Vector3(-25, 7, 9)
|
||||
|
||||
[node name="Keep" parent="FortressBlockout" instance=ExtResource("5_fortress")]
|
||||
|
||||
[node name="RidgeBannerPole" type="MeshInstance3D" parent="FortressBlockout/Keep"]
|
||||
position = Vector3(2.2, 8.6, 2.8)
|
||||
mesh = SubResource("Mesh_site_pole")
|
||||
|
||||
[node name="RidgeBanner" type="MeshInstance3D" parent="FortressBlockout/Keep"]
|
||||
position = Vector3(2.2, 8.95, 3.35)
|
||||
mesh = SubResource("Mesh_landmark_banner")
|
||||
|
||||
[node name="WaterRoot" type="Node3D" parent="."]
|
||||
|
||||
[node name="RiverSurface" parent="WaterRoot" instance=ExtResource("9_water")]
|
||||
@@ -414,6 +406,66 @@ safety_risk = 0.02
|
||||
comfort_distance = 16.0
|
||||
discovery_priority = 0.75
|
||||
|
||||
[node name="FarmCrop_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||
position = Vector3(-11, 0, 3)
|
||||
node_id = &"farm_crop_01"
|
||||
initial_amount = 8.0
|
||||
yield_per_action = 2.0
|
||||
safety_risk = 0.02
|
||||
comfort_distance = 16.0
|
||||
discovery_priority = 0.85
|
||||
|
||||
[node name="BerryBush_River_02" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||
position = Vector3(28, 0, -2)
|
||||
node_id = &"berry_bush_river_02"
|
||||
initial_amount = 5.0
|
||||
yield_per_action = 1.5
|
||||
safety_risk = 0.22
|
||||
comfort_distance = 28.0
|
||||
discovery_priority = 0.2
|
||||
|
||||
[node name="Tree_Forest_Grove_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||
position = Vector3(-8, 0, -22)
|
||||
node_id = &"tree_forest_grove_01"
|
||||
action_id = &"gather_wood"
|
||||
resource_id = &"wood"
|
||||
initial_amount = 14.0
|
||||
yield_per_action = 3.0
|
||||
safety_risk = 0.3
|
||||
comfort_distance = 30.0
|
||||
discovery_priority = -0.1
|
||||
|
||||
[node name="Tree_Forest_Grove_02" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||
position = Vector3(-3, 0, -24)
|
||||
node_id = &"tree_forest_grove_02"
|
||||
action_id = &"gather_wood"
|
||||
resource_id = &"wood"
|
||||
initial_amount = 11.0
|
||||
yield_per_action = 2.5
|
||||
safety_risk = 0.32
|
||||
comfort_distance = 32.0
|
||||
discovery_priority = -0.2
|
||||
|
||||
[node name="WoodPile_Mill_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||
position = Vector3(21, 0, 1)
|
||||
node_id = &"wood_pile_mill_01"
|
||||
action_id = &"gather_wood"
|
||||
resource_id = &"wood"
|
||||
initial_amount = 4.0
|
||||
yield_per_action = 1.0
|
||||
safety_risk = 0.03
|
||||
comfort_distance = 14.0
|
||||
discovery_priority = 0.8
|
||||
|
||||
[node name="BerryPatch_Mill_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||
position = Vector3(14, 0, 3)
|
||||
node_id = &"berry_patch_mill_01"
|
||||
initial_amount = 5.0
|
||||
yield_per_action = 1.5
|
||||
safety_risk = 0.1
|
||||
comfort_distance = 20.0
|
||||
discovery_priority = 0.45
|
||||
|
||||
[node name="StorageSites" type="Node3D" parent="WorldObjects"]
|
||||
|
||||
[node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
|
||||
|
||||
@@ -7,10 +7,10 @@ shader = ExtResource("1_water_shader")
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_water_body"]
|
||||
material = SubResource("ShaderMaterial_water")
|
||||
size = Vector3(5, 0.08, 56)
|
||||
size = Vector3(8, 0.08, 58)
|
||||
|
||||
[node name="WaterBody" type="Node3D"]
|
||||
position = Vector3(25, 0.12, 0)
|
||||
position = Vector3(25, 0.08, 0)
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("Mesh_water_body")
|
||||
|
||||
@@ -7,10 +7,10 @@ shader = ExtResource("1_waterfall_shader")
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_waterfall_body"]
|
||||
material = SubResource("ShaderMaterial_waterfall")
|
||||
size = Vector3(5, 10, 0.5)
|
||||
size = Vector3(8, 12, 0.5)
|
||||
|
||||
[node name="WaterfallBody" type="Node3D"]
|
||||
position = Vector3(25, 5, -24)
|
||||
position = Vector3(25, 6, -24)
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("Mesh_waterfall_body")
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_mist"]
|
||||
lifetime_randomness = 0.2
|
||||
lifetime_randomness = 0.35
|
||||
spread = 180.0
|
||||
gravity = Vector3(0, -0.5, 0)
|
||||
initial_velocity_min = 0.3
|
||||
initial_velocity_max = 0.8
|
||||
scale_min = 0.1
|
||||
scale_max = 1.5
|
||||
color = Color(0.85, 0.95, 0.95, 0.4)
|
||||
gravity = Vector3(0, -0.35, 0)
|
||||
initial_velocity_min = 0.25
|
||||
initial_velocity_max = 1.2
|
||||
scale_min = 0.08
|
||||
scale_max = 2.0
|
||||
color = Color(0.82, 0.94, 0.96, 0.35)
|
||||
emission_shape = 1
|
||||
emission_box_extents = Vector3(2.5, 1.0, 0.5)
|
||||
emission_box_extents = Vector3(4.5, 1.5, 1.0)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_mist"]
|
||||
albedo_color = Color(0.85, 0.95, 0.95, 0.35)
|
||||
albedo_color = Color(0.82, 0.94, 0.96, 0.28)
|
||||
transparency = 1
|
||||
shading_mode = 0
|
||||
billboard_mode = 1
|
||||
@@ -21,14 +21,14 @@ cull_mode = 0
|
||||
|
||||
[sub_resource type="QuadMesh" id="Mesh_mist"]
|
||||
material = SubResource("Material_mist")
|
||||
size = Vector2(1.5, 1.5)
|
||||
size = Vector2(1.8, 1.8)
|
||||
|
||||
[node name="WaterfallMist" type="GPUParticles3D"]
|
||||
emitting = true
|
||||
amount = 48
|
||||
lifetime = 2.5
|
||||
amount = 72
|
||||
lifetime = 3.0
|
||||
one_shot = false
|
||||
preprocess = 1.0
|
||||
preprocess = 1.5
|
||||
speed_scale = 1.0
|
||||
local_coords = true
|
||||
draw_order = 0
|
||||
|
||||
@@ -1,20 +1,54 @@
|
||||
shader_type spatial;
|
||||
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
|
||||
|
||||
uniform float wave_strength : hint_range(0.0, 1.0) = 0.15;
|
||||
uniform float wave_speed : hint_range(0.0, 5.0) = 1.2;
|
||||
uniform float wave_strength : hint_range(0.0, 0.5) = 0.08;
|
||||
uniform float wave_speed : hint_range(0.0, 5.0) = 0.8;
|
||||
uniform float flow_speed : hint_range(0.0, 3.0) = 0.6;
|
||||
uniform vec3 deep_color : source_color = vec3(0.06, 0.24, 0.38);
|
||||
uniform vec3 shallow_color : source_color = vec3(0.18, 0.44, 0.58);
|
||||
uniform vec3 foam_color : source_color = vec3(0.82, 0.88, 0.86);
|
||||
uniform float rim_power : hint_range(0.5, 5.0) = 2.5;
|
||||
uniform float sparkle_strength : hint_range(0.0, 1.0) = 0.12;
|
||||
|
||||
global uniform vec3 sun_direction = vec3(0.4, 0.8, 0.4);
|
||||
|
||||
void vertex() {
|
||||
vec3 pos = VERTEX;
|
||||
float wave = sin(pos.z * 0.4 + TIME * wave_speed) * wave_strength;
|
||||
pos.y += wave;
|
||||
float wave_a = sin(pos.x * 0.35 + pos.z * 0.25 + TIME * wave_speed) * wave_strength;
|
||||
float wave_b = cos(pos.x * 0.45 - pos.z * 0.55 + TIME * wave_speed * 0.7) * wave_strength * 0.6;
|
||||
float wave_c = sin(pos.z * 0.7 + TIME * wave_speed * 1.3) * wave_strength * 0.35;
|
||||
pos.y += wave_a + wave_b + wave_c;
|
||||
VERTEX = pos;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
ALBEDO = vec3(0.12, 0.39, 0.55);
|
||||
METALLIC = 0.15;
|
||||
ROUGHNESS = 0.1;
|
||||
EMISSION = vec3(0.05, 0.19, 0.24) * 0.45;
|
||||
ALPHA = 0.72;
|
||||
vec3 view_dir = VIEW;
|
||||
vec3 normal = NORMAL;
|
||||
vec3 view_norm = normalize(view_dir);
|
||||
|
||||
float rim = 1.0 - abs(dot(normal, view_norm));
|
||||
rim = pow(rim, rim_power);
|
||||
rim = smoothstep(0.15, 0.7, rim);
|
||||
|
||||
vec2 uv = UV;
|
||||
uv.x += TIME * flow_speed * 0.4;
|
||||
uv.y += TIME * flow_speed * 0.3;
|
||||
float flow_a = sin(uv.x * 8.0 + uv.y * 5.0) * 0.5 + 0.5;
|
||||
float flow_b = cos(uv.y * 6.0 - uv.x * 4.0 + TIME * 0.25) * 0.5 + 0.5;
|
||||
float flow = mix(flow_a, flow_b, 0.5);
|
||||
|
||||
vec3 water_color = mix(deep_color, shallow_color, flow * 0.6 + 0.2);
|
||||
water_color = mix(water_color, foam_color, rim * 0.65);
|
||||
|
||||
vec3 half_vec = normalize(view_norm + normalize(sun_direction));
|
||||
float specular = pow(max(dot(normal, half_vec), 0.0), 120.0);
|
||||
specular *= smoothstep(0.4, 0.85, rim);
|
||||
float sparkle = specular * sparkle_strength;
|
||||
|
||||
ALBEDO = water_color;
|
||||
METALLIC = 0.08;
|
||||
ROUGHNESS = 0.22;
|
||||
EMISSION = water_color * 0.04 + foam_color * sparkle;
|
||||
ALPHA = 0.76;
|
||||
ALPHA_SCISSOR_THRESHOLD = 0.0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
shader_type spatial;
|
||||
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
|
||||
|
||||
uniform float scroll_speed : hint_range(0.0, 5.0) = 2.0;
|
||||
uniform float scroll_speed : hint_range(0.0, 5.0) = 1.8;
|
||||
uniform vec3 deep_color : source_color = vec3(0.1, 0.45, 0.55);
|
||||
uniform vec3 mid_color : source_color = vec3(0.45, 0.78, 0.82);
|
||||
uniform vec3 foam_color : source_color = vec3(0.9, 0.95, 0.95);
|
||||
uniform float foam_contrast : hint_range(0.5, 4.0) = 2.2;
|
||||
uniform float sparkle_strength : hint_range(0.0, 0.8) = 0.25;
|
||||
|
||||
void vertex() {
|
||||
}
|
||||
@@ -9,13 +14,37 @@ void vertex() {
|
||||
void fragment() {
|
||||
vec2 uv = UV;
|
||||
uv.y += TIME * scroll_speed;
|
||||
float pattern = fract(uv.y * 4.0);
|
||||
pattern = smoothstep(0.3, 0.7, pattern);
|
||||
pattern = mix(0.4, 1.0, pattern);
|
||||
vec3 base_color = mix(vec3(0.6, 0.85, 0.9), vec3(1.0, 1.0, 1.0), pattern);
|
||||
ALBEDO = base_color;
|
||||
ALPHA = mix(0.3, 0.7, pattern);
|
||||
EMISSION = vec3(0.36, 0.52, 0.54) * 0.35 * pattern;
|
||||
METALLIC = 0.0;
|
||||
ROUGHNESS = 0.2;
|
||||
|
||||
float band_a = fract(uv.y * 5.0 + sin(uv.x * 2.0) * 0.15);
|
||||
band_a = smoothstep(0.2, 0.55, band_a) * smoothstep(0.8, 0.45, band_a);
|
||||
|
||||
float band_b = fract(uv.y * 3.5 + cos(uv.x * 2.5 + TIME * 0.3) * 0.12);
|
||||
band_b = smoothstep(0.1, 0.5, band_b) * smoothstep(0.85, 0.4, band_b);
|
||||
|
||||
float foam_a = fract(uv.y * 4.0);
|
||||
foam_a = pow(smoothstep(0.0, 0.12, foam_a) * smoothstep(0.3, 0.15, foam_a), foam_contrast);
|
||||
|
||||
float foam_b = fract(uv.y * 6.0 + uv.x * 1.5);
|
||||
foam_b = pow(smoothstep(0.5, 0.58, foam_b) * smoothstep(0.7, 0.6, foam_b), foam_contrast * 0.8);
|
||||
|
||||
float foam = foam_a * 0.7 + foam_b * 0.3;
|
||||
|
||||
vec3 base = mix(deep_color, mid_color, band_a * 0.5 + band_b * 0.3);
|
||||
vec3 color = mix(base, foam_color, foam);
|
||||
|
||||
float sparkle = pow(foam_a, 3.0) * sparkle_strength;
|
||||
float highlight = band_a * band_b * 0.35;
|
||||
float emission_value = sparkle + highlight * 0.3;
|
||||
|
||||
ALBEDO = color;
|
||||
ALPHA = mix(0.25, 0.78, foam * 0.7 + 0.15);
|
||||
EMISSION = color * emission_value;
|
||||
METALLIC = 0.05;
|
||||
ROUGHNESS = 0.15;
|
||||
|
||||
vec3 normal = NORMAL;
|
||||
vec3 view_dir = VIEW;
|
||||
vec3 view_norm = normalize(view_dir);
|
||||
float rim = 1.0 - abs(dot(normal, view_norm));
|
||||
ALPHA = mix(ALPHA, ALPHA * 0.5, rim * 0.6);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user