v0.3.0: rendering pass, generated props, selection model and Web build
Rendering and presentation - Shared surface-material module (src/game/materials.gd) with per-surface roughness/metallic/uv profiles; ACES tonemapping, glow, fog and phase-driven sky energy. WorldBuilder rebuilt on those profiles with facade windows that light up, door trim/handles, curb, lane dashes, bollards, bench, bin and planter foliage. - NPC silhouettes: per-role clothing palettes, caps/hoods/backpacks/sunglasses, hands and shoes, arm/leg walk swing. - Six generated low-poly GLB props (bollards, bench, bin, planter, newspaper box, hydrant) built by tools/model_pipeline.py, loaded by WorldBuilder with a procedural fallback; ADR-0012. Selection and movement - Focus ranking by view angle then distance (NpcFocus.rank_indices); Tab and the mouse wheel cycle nearby people initially, right click clears focus with a re-acquire grace, HUD shows the candidate count, Shift walks at half speed. ADR-0011. Windowed smoke extended to cover wheel cycling and clearing. QA tooling - screen_designs.py catalogue grown to 19 screens (8 scene, 11 UI) plus a `list` subcommand; draft generation keeps existing AI references unless --force. Web export - New Web preset using the non-threaded Godot template (static hosting, no COOP/COEP). tools/build_releases.sh exports build/web and packages build/step-line-web-<ver>.zip; publish_release.sh uploads it with the desktop artifacts. test_web_export.gd covers the preset and release wiring. Verification - Headless suite 177 -> 181, Python tooling 53 tests, all eight smokes pass (including the windowed focus/wheel smoke). Web bundle serves index.html, index.wasm (35 MB) and index.pck (30 MB) with HTTP 200 from a static server. - Version 0.3.0; README, PLAN, RELEASING, TODO/DONE and MEMORY updated.
This commit is contained in:
1 parent
f763cff744
commit
afa1d6beec
48 files changed
+2602
-147
No files matched your search
+59
-23
@@ -16,6 +16,7 @@ const ScenarioFactory := preload("res://src/content/scenario_factory.gd")
|
||||
const ShiftCoordinator := preload("res://src/core/shift_coordinator.gd")
|
||||
const VenueScenario := preload("res://src/core/venue_scenario.gd")
|
||||
const WorldBuilder := preload("res://src/game/world_builder.gd")
|
||||
const Materials := preload("res://src/game/materials.gd")
|
||||
const GuardController := preload("res://src/game/guard_controller.gd")
|
||||
const NpcAvatar := preload("res://src/game/npc_avatar.gd")
|
||||
const NpcProfile := preload("res://src/core/npc_profile.gd")
|
||||
@@ -44,24 +45,34 @@ var _last_phase: int = -1
|
||||
## structure — phase-based lighting). Night phases switch street lamps on.
|
||||
const PHASE_LIGHTING := {
|
||||
Rules.Phase.EARLY: {
|
||||
"sun_energy": 1.2, "sun_angle": -50.0, "sun_color": Color(1.0, 0.98, 0.92),
|
||||
"sun_energy": 1.05, "sun_angle": -50.0, "sun_color": Color(1.0, 0.97, 0.90),
|
||||
"sky_top": Color(0.32, 0.55, 0.95), "sky_horizon": Color(0.82, 0.88, 0.95),
|
||||
"sky_ground": Color(0.45, 0.5, 0.55), "ambient": Color(0.75, 0.78, 0.85), "night": false,
|
||||
"exposure": 0.88, "glow": 0.35, "fog_color": Color(0.80, 0.85, 0.92),
|
||||
"fog_density": 0.0028, "fog_energy": 0.75, "contrast": 1.04, "saturation": 1.06,
|
||||
"ambient_energy": 0.45,
|
||||
},
|
||||
Rules.Phase.MIDDAY: {
|
||||
"sun_energy": 1.5, "sun_angle": -62.0, "sun_color": Color(1.0, 1.0, 0.98),
|
||||
"sun_energy": 1.3, "sun_angle": -62.0, "sun_color": Color(1.0, 0.99, 0.96),
|
||||
"sky_top": Color(0.3, 0.52, 0.95), "sky_horizon": Color(0.85, 0.9, 0.96),
|
||||
"sky_ground": Color(0.5, 0.54, 0.58), "ambient": Color(0.85, 0.88, 0.95), "night": false,
|
||||
"exposure": 0.85, "glow": 0.3, "fog_color": Color(0.84, 0.88, 0.94),
|
||||
"fog_density": 0.0022, "fog_energy": 0.8, "contrast": 1.05, "saturation": 1.05,
|
||||
"ambient_energy": 0.45,
|
||||
},
|
||||
Rules.Phase.EVENING: {
|
||||
"sun_energy": 0.7, "sun_angle": -16.0, "sun_color": Color(1.0, 0.72, 0.5),
|
||||
"sky_top": Color(0.62, 0.32, 0.35), "sky_horizon": Color(1.0, 0.68, 0.42),
|
||||
"sky_ground": Color(0.3, 0.24, 0.3), "ambient": Color(0.55, 0.45, 0.5), "night": true,
|
||||
"exposure": 1.05, "glow": 0.7, "fog_color": Color(0.95, 0.68, 0.48),
|
||||
"fog_density": 0.0045, "fog_energy": 0.6, "contrast": 1.08, "saturation": 1.12,
|
||||
},
|
||||
Rules.Phase.LATE: {
|
||||
"sun_energy": 0.05, "sun_angle": -8.0, "sun_color": Color(0.3, 0.35, 0.5),
|
||||
"sky_top": Color(0.02, 0.03, 0.08), "sky_horizon": Color(0.1, 0.12, 0.2),
|
||||
"sky_ground": Color(0.05, 0.05, 0.08), "ambient": Color(0.08, 0.09, 0.16), "night": true,
|
||||
"exposure": 1.25, "glow": 0.95, "fog_color": Color(0.10, 0.13, 0.24),
|
||||
"fog_density": 0.0055, "fog_energy": 0.5, "contrast": 1.1, "saturation": 1.0,
|
||||
},
|
||||
}
|
||||
var hud: Hud = null
|
||||
@@ -103,12 +114,12 @@ var _venue_index := 0
|
||||
## RNG for venue door-staff lines so repeated interactions vary.
|
||||
var _door_rng := RandomNumberGenerator.new()
|
||||
|
||||
## Aim-free targeting: cycle through nearby NPCs with Tab (accessibility).
|
||||
const NEARBY_RADIUS := 12.0
|
||||
## Aim-free targeting lives on the guard (`GuardController.nearby_candidates`);
|
||||
## this file only reacts to the signals it emits and logs the selection.
|
||||
|
||||
## Upper bound on live avatars. Routine traffic is dropped at the cap; scripted
|
||||
## events evict the oldest unfocused NPC so authored moments still spawn.
|
||||
const MAX_LIVE_NPCS := 16
|
||||
var _nearby_index := -1
|
||||
|
||||
func _ready() -> void:
|
||||
_door_rng.randomize()
|
||||
@@ -219,7 +230,6 @@ func _reset_shift_state() -> void:
|
||||
current_incident_npc_id = ""
|
||||
targeted_npc = null
|
||||
_focused_avatar = null
|
||||
_nearby_index = -1
|
||||
inspection_open = false
|
||||
wand_open = false
|
||||
_wand_cooldown = 0.0
|
||||
@@ -429,8 +439,9 @@ func _ensure_lighting() -> void:
|
||||
sky.sky_material = _sky
|
||||
env.sky = sky
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
|
||||
env.ambient_light_energy = 0.5
|
||||
env.reflected_light_source = Environment.REFLECTION_SOURCE_SKY
|
||||
# Tonemap, glow, fog and colour grading live in SurfaceMaterials so the
|
||||
# world builder and the environment tune-up stay in one place.
|
||||
Materials.configure_environment(env, PHASE_LIGHTING[Rules.Phase.EARLY])
|
||||
_env_node.environment = env
|
||||
scene_root.add_child(_env_node)
|
||||
|
||||
@@ -448,6 +459,8 @@ func _apply_phase_lighting() -> void:
|
||||
_sky.sky_horizon_color = cfg.get("sky_horizon", Color.WHITE)
|
||||
_sky.ground_bottom_color = cfg.get("sky_ground", Color(0.5, 0.5, 0.5))
|
||||
_sky.ground_horizon_color = cfg.get("sky_horizon", Color.WHITE)
|
||||
if _env_node and _env_node.environment:
|
||||
Materials.configure_environment(_env_node.environment, cfg)
|
||||
if world_builder:
|
||||
world_builder.set_night_mode(cfg.get("night", false))
|
||||
|
||||
@@ -642,12 +655,14 @@ func _on_guard_target(target: Node3D) -> void:
|
||||
targeted_npc = target
|
||||
_set_focused_avatar(target)
|
||||
_begin_incident(target)
|
||||
_refresh_nearby_hint()
|
||||
|
||||
func _on_guard_clear() -> void:
|
||||
targeted_npc = null
|
||||
_set_focused_avatar(null)
|
||||
hud.set_target_label("")
|
||||
hud.set_action_hint("")
|
||||
_refresh_nearby_hint()
|
||||
|
||||
func _set_focused_avatar(avatar: Node3D) -> void:
|
||||
if _focused_avatar and is_instance_valid(_focused_avatar) and _focused_avatar.has_method("set_focus_visual"):
|
||||
@@ -699,6 +714,12 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
_handle_key(event)
|
||||
|
||||
func _handle_mouse_button(event: InputEventMouseButton) -> void:
|
||||
if event.is_action_pressed("cycle_target_next"):
|
||||
_cycle_nearby_target(1)
|
||||
return
|
||||
if event.is_action_pressed("cycle_target_prev"):
|
||||
_cycle_nearby_target(-1)
|
||||
return
|
||||
if event.button_index != MOUSE_BUTTON_LEFT:
|
||||
return
|
||||
if radial and radial.is_open:
|
||||
@@ -794,6 +815,10 @@ func _handle_cancel(from_mouse: bool) -> void:
|
||||
if _close_top_modal():
|
||||
return
|
||||
if from_mouse:
|
||||
# Right click with nothing open drops the current target, so the player can
|
||||
# break focus without walking or looking away.
|
||||
if targeted_npc != null:
|
||||
_clear_focus()
|
||||
return
|
||||
if settings_panel and settings_panel.visible:
|
||||
return
|
||||
@@ -974,30 +999,41 @@ func _cycle_venue() -> void:
|
||||
hud.set_wand_icon(false)
|
||||
hud.set_shift_state(coordinator, selected_scenario)
|
||||
|
||||
## Aim-free targeting: cycle through NPCs near the guard (Tab). Lets players
|
||||
## interact without precise aiming (accessibility).
|
||||
func _cycle_nearby_target() -> void:
|
||||
## Aim-free targeting: cycle through NPCs near the guard (Tab, mouse wheel).
|
||||
## Lets players interact without precise aiming (accessibility) and pins the
|
||||
## target so the focus cone does not drop someone the player is not looking at.
|
||||
func _cycle_nearby_target(direction: int = 1) -> void:
|
||||
if guard == null:
|
||||
return
|
||||
var nearby: Array = []
|
||||
for avatar in npcs:
|
||||
if not is_instance_valid(avatar):
|
||||
continue
|
||||
if (avatar.global_position - guard.global_position).length() <= NEARBY_RADIUS:
|
||||
nearby.append(avatar)
|
||||
if nearby.is_empty():
|
||||
var next_target: Node3D = guard.cycle_target(direction)
|
||||
if next_target == null:
|
||||
hud.log(Strings.lookup("hud.no_one_nearby"))
|
||||
_refresh_nearby_hint()
|
||||
return
|
||||
_nearby_index = (_nearby_index + 1) % nearby.size()
|
||||
var next_target: Node3D = nearby[_nearby_index]
|
||||
targeted_npc = next_target
|
||||
_set_focused_avatar(next_target)
|
||||
_begin_incident(next_target)
|
||||
guard.pin_target(next_target)
|
||||
var controller: Object = _targeted_controller()
|
||||
var who: String = next_target.name
|
||||
if controller and controller.profile and controller.profile.display_name != "":
|
||||
who = str(controller.profile.display_name)
|
||||
hud.log(Strings.fmt("hud.targeting", [who]))
|
||||
_refresh_nearby_hint()
|
||||
|
||||
## Drop the focus and hand selection back to the aim cone (right click).
|
||||
func _clear_focus() -> void:
|
||||
if guard:
|
||||
guard.clear_target()
|
||||
targeted_npc = null
|
||||
_set_focused_avatar(null)
|
||||
if hud:
|
||||
hud.set_target_label("")
|
||||
hud.set_action_hint("")
|
||||
_refresh_nearby_hint()
|
||||
|
||||
## How many people the aim-free ring can cycle through right now.
|
||||
func _refresh_nearby_hint() -> void:
|
||||
if hud == null or guard == null:
|
||||
return
|
||||
hud.set_nearby_hint(guard.nearby_candidates().size())
|
||||
|
||||
func _apply_interaction(level: int, action_name: String) -> void:
|
||||
if targeted_npc == null:
|
||||
|
||||
Reference in new issue
Block a user