feat: world-grid level materials + real movement test suite
- Generated CC0-style prototype grid textures (0.5 m cells) and a shared LevelMaterials factory: world-space triplanar mapping over every code-built level (test level, dust2, procedural arena) with the existing color coding kept as tints. Readable surfaces at speed instead of flat color boxes; materials are cached per tint so identical surfaces batch. - .gdignore + .gitignore the raw downloaded asset packs in addons/ whose overlong paths broke Godot import scans and spammed git warnings. - Replaced the rotted FSM test file (invalid call(t) on Callables, stateless stub nodes, hung forever without quitting) with a real suite: 10 tests covering state registration, jump bookkeeping, wall-run momentum preservation and upward-carry cap, per-player dash cooldown (regression for the old shared static), dash speed stacking, chain soft cap, landing events, and slide entry momentum. New headless entrypoint: godot --headless --path . -s res://movement/tests/run_fsm_tests.gd Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7da7f9f3f2
commit
198345177a
@@ -64,3 +64,6 @@ tags
|
|||||||
Papaya-Shooter.pck
|
Papaya-Shooter.pck
|
||||||
Papaya-Shooter.exe
|
Papaya-Shooter.exe
|
||||||
Papaya-Shooter.console.exe
|
Papaya-Shooter.console.exe
|
||||||
|
|
||||||
|
# Raw downloaded asset packs (not game content; some paths exceed Windows limits)
|
||||||
|
addons/lowpoly_map_gen/downloaded_assets/
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -38,14 +38,9 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
|
|
||||||
func _build_dust2_layout() -> void:
|
func _build_dust2_layout() -> void:
|
||||||
# Materials
|
# Materials
|
||||||
var wall_mat = StandardMaterial3D.new()
|
var wall_mat = LevelMaterials.tinted(Color(0.85, 0.75, 0.6)) # Sandstone
|
||||||
wall_mat.albedo_color = Color(0.85, 0.75, 0.6) # Sandstone
|
var floor_mat = LevelMaterials.tinted(Color(0.7, 0.65, 0.55)) # Dusty ground
|
||||||
|
var box_mat = LevelMaterials.tinted(Color(0.4, 0.3, 0.2), true) # Wood crates
|
||||||
var floor_mat = StandardMaterial3D.new()
|
|
||||||
floor_mat.albedo_color = Color(0.7, 0.65, 0.55) # Dusty ground
|
|
||||||
|
|
||||||
var box_mat = StandardMaterial3D.new()
|
|
||||||
box_mat.albedo_color = Color(0.4, 0.3, 0.2) # Wood crates
|
|
||||||
|
|
||||||
# Root CSG
|
# Root CSG
|
||||||
var root_csg = CSGCombiner3D.new()
|
var root_csg = CSGCombiner3D.new()
|
||||||
|
|||||||
@@ -82,10 +82,7 @@ func _box_static(pos: Vector3, size: Vector3, color: Color, node_name: String =
|
|||||||
var mesh := MeshInstance3D.new()
|
var mesh := MeshInstance3D.new()
|
||||||
mesh.mesh = BoxMesh.new()
|
mesh.mesh = BoxMesh.new()
|
||||||
mesh.mesh.size = size
|
mesh.mesh.size = size
|
||||||
var mat := StandardMaterial3D.new()
|
mesh.mesh.surface_set_material(0, LevelMaterials.tinted(color))
|
||||||
mat.albedo_color = color
|
|
||||||
mat.roughness = 0.8
|
|
||||||
mesh.mesh.surface_set_material(0, mat)
|
|
||||||
body.add_child(mesh)
|
body.add_child(mesh)
|
||||||
return body
|
return body
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
extends CharacterBody3D
|
||||||
|
class_name FakeMovementPlayer
|
||||||
|
|
||||||
|
## Minimal stand-in for PlayerMovementController in movement unit tests.
|
||||||
|
## Carries just the properties movement states touch, all inert.
|
||||||
|
|
||||||
|
var jump_player = null
|
||||||
|
var double_jump_player = null
|
||||||
|
var footstep_player = null
|
||||||
|
var dash_player = null
|
||||||
|
var slide_player = null
|
||||||
|
var wallrun_player = null
|
||||||
|
var vault_player = null
|
||||||
|
var wind_player = null
|
||||||
|
var camera = null
|
||||||
|
var head_pivot = null
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Headless entrypoint for the MovementStateMachine unit tests.
|
||||||
|
## Run: godot --headless --path . -s res://movement/tests/run_fsm_tests.gd
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
await process_frame
|
||||||
|
var tester = load("res://movement/tests/test_fsm_runner.gd").new()
|
||||||
|
root.add_child(tester)
|
||||||
|
tester.run_all() # quits with the right exit code
|
||||||
+141
-120
@@ -1,6 +1,12 @@
|
|||||||
extends Node
|
extends Node
|
||||||
class_name MovementStateMachineTest
|
class_name MovementStateMachineTest
|
||||||
|
|
||||||
|
## Unit tests for MovementStateMachine + states. Runs headless via
|
||||||
|
## movement/tests/run_fsm_tests.gd. Each test gets a fresh machine with the
|
||||||
|
## real state scripts attached, added to the tree so _ready registers states.
|
||||||
|
## Tests exercise transitions and bookkeeping — not physics (no move_and_slide
|
||||||
|
## against a real world here; the spawn smoke test covers integration).
|
||||||
|
|
||||||
var sm: MovementStateMachine
|
var sm: MovementStateMachine
|
||||||
var fake_player: CharacterBody3D
|
var fake_player: CharacterBody3D
|
||||||
var params: MovementParams
|
var params: MovementParams
|
||||||
@@ -9,32 +15,37 @@ var fail_log := []
|
|||||||
var tests_passed: int = 0
|
var tests_passed: int = 0
|
||||||
var tests_failed: int = 0
|
var tests_failed: int = 0
|
||||||
|
|
||||||
|
const STATE_SCRIPTS := {
|
||||||
|
"state_ground": "res://movement/states/state_ground.gd",
|
||||||
|
"state_air": "res://movement/states/state_air.gd",
|
||||||
|
"state_wall_run": "res://movement/states/state_wall_run.gd",
|
||||||
|
"state_wall_cling": "res://movement/states/state_wall_cling.gd",
|
||||||
|
"state_slide": "res://movement/states/state_slide.gd",
|
||||||
|
"state_dash": "res://movement/states/state_dash.gd",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func run_all() -> void:
|
func run_all() -> void:
|
||||||
print("=== MovementStateMachine tests ===")
|
print("=== MovementStateMachine tests ===")
|
||||||
tests_passed = 0
|
tests_passed = 0
|
||||||
tests_failed = 0
|
tests_failed = 0
|
||||||
params = MovementParams.new()
|
|
||||||
fake_player = CharacterBody3D.new()
|
|
||||||
fake_player.set_floor_max_angle(0.01)
|
|
||||||
sm = MovementStateMachine.new()
|
|
||||||
sm.player = fake_player
|
|
||||||
sm.params = params
|
|
||||||
|
|
||||||
var tests := [
|
var tests := [
|
||||||
test_states_initialize_with_ground,
|
"test_states_register_and_initialize",
|
||||||
test_ground_transitions_to_air_on_jump,
|
"test_do_jump_sets_velocity_and_counters",
|
||||||
test_air_transitions_to_ground_on_land,
|
"test_wall_run_preserves_fast_entry_speed",
|
||||||
test_double_jump_allowed_once,
|
"test_wall_run_entry_keeps_some_upward_momentum",
|
||||||
test_wall_run_started_when_near_wall,
|
"test_dash_cooldown_is_per_machine",
|
||||||
test_chain_bonus_caps_at_50pct,
|
"test_dash_adds_speed_to_forward_momentum",
|
||||||
test_sliding_reduces_speed,
|
"test_chain_bonus_soft_caps",
|
||||||
test_dash_speed_under_effective_cap,
|
"test_notify_landed_emits_scaled_event",
|
||||||
|
"test_slide_entry_boosts_and_keeps_direction",
|
||||||
|
"test_grapple_state_registered_by_machine",
|
||||||
]
|
]
|
||||||
for t in tests:
|
for t in tests:
|
||||||
_fresh()
|
_fresh()
|
||||||
call(t)
|
call(t)
|
||||||
var ok := fail_log.is_empty()
|
var ok: bool = fail_log.is_empty()
|
||||||
print(" %s: %s" % ["PASS" if ok else "FAIL", t])
|
print(" %s: %s" % ["PASS" if ok else "FAIL", t])
|
||||||
if ok:
|
if ok:
|
||||||
tests_passed += 1
|
tests_passed += 1
|
||||||
@@ -43,6 +54,7 @@ func run_all() -> void:
|
|||||||
for msg in fail_log:
|
for msg in fail_log:
|
||||||
print(" ", msg)
|
print(" ", msg)
|
||||||
fail_log.clear()
|
fail_log.clear()
|
||||||
|
_teardown()
|
||||||
|
|
||||||
print("=== Results: %d passed, %d failed ===" % [tests_passed, tests_failed])
|
print("=== Results: %d passed, %d failed ===" % [tests_passed, tests_failed])
|
||||||
if tests_failed > 0:
|
if tests_failed > 0:
|
||||||
@@ -56,25 +68,27 @@ func run_all() -> void:
|
|||||||
# ── helpers ─────────────────────────────────────────────────────────────────
|
# ── helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func _fresh() -> void:
|
func _fresh() -> void:
|
||||||
sm.current_state = ""
|
params = MovementParams.new()
|
||||||
sm.chain_count = 0
|
fake_player = load("res://movement/tests/fake_player.gd").new()
|
||||||
sm.current_chain_bonus = 0.0
|
add_child(fake_player)
|
||||||
sm.chain_timer = 0.0
|
sm = MovementStateMachine.new()
|
||||||
sm.on_ground = false
|
sm.player = fake_player
|
||||||
sm.coyote_timer = 0.0
|
sm.params = params
|
||||||
sm.jump_buffer_time = 0.0
|
for state_name in STATE_SCRIPTS:
|
||||||
sm.current_jump_count = 0
|
var st := Node.new()
|
||||||
sm.wall_normal = Vector3.ZERO
|
st.name = state_name
|
||||||
sm.input_dir = Vector2.ZERO
|
st.set_script(load(STATE_SCRIPTS[state_name]))
|
||||||
sm.input_jump_just_pressed = false
|
sm.add_child(st)
|
||||||
sm.input_jump_pressed = false
|
fake_player.add_child(sm) # triggers _ready: registers states + injected ones
|
||||||
sm.input_sprint = false
|
sm.set_physics_process(false) # tests drive the machine manually
|
||||||
sm.input_crouch = false
|
|
||||||
sm.input_dash = false
|
|
||||||
fake_player.velocity = Vector3.ZERO
|
func _teardown() -> void:
|
||||||
for c in sm.get_children():
|
if is_instance_valid(fake_player):
|
||||||
sm.remove_child(c)
|
remove_child(fake_player)
|
||||||
c.queue_free()
|
fake_player.free()
|
||||||
|
sm = null
|
||||||
|
fake_player = null
|
||||||
|
|
||||||
|
|
||||||
func _expect(cond: bool, msg: String) -> void:
|
func _expect(cond: bool, msg: String) -> void:
|
||||||
@@ -89,108 +103,115 @@ func _eq(a: Variant, b: Variant, msg: String = "") -> void:
|
|||||||
|
|
||||||
# ── tests ───────────────────────────────────────────────────────────────────
|
# ── tests ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func test_states_initialize_with_ground() -> void:
|
func test_states_register_and_initialize() -> void:
|
||||||
var g = Node.new()
|
# All six explicit states plus the two the machine injects itself.
|
||||||
g.name = "state_ground"
|
for expected in ["ground", "air", "wall_run", "wall_cling", "slide", "dash", "grapple", "wall_climb"]:
|
||||||
g.machine = sm
|
_expect(sm.states.has(expected), "state '%s' should be registered" % expected)
|
||||||
sm.add_child(g)
|
_expect(not sm.current_state.is_empty(), "machine should start in a state")
|
||||||
var a = Node.new()
|
|
||||||
a.name = "state_air"
|
|
||||||
a.machine = sm
|
|
||||||
sm.add_child(a)
|
|
||||||
sm.switch_to("ground")
|
sm.switch_to("ground")
|
||||||
_eq(sm.current_state, "ground")
|
_eq(sm.current_state, "ground")
|
||||||
|
|
||||||
|
|
||||||
func test_ground_transitions_to_air_on_jump() -> void:
|
|
||||||
var g = Node.new()
|
|
||||||
g.name = "state_ground"
|
|
||||||
g.machine = sm
|
|
||||||
sm.add_child(g)
|
|
||||||
var a = Node.new()
|
|
||||||
a.name = "state_air"
|
|
||||||
a.machine = sm
|
|
||||||
sm.add_child(a)
|
|
||||||
sm.on_ground = true
|
|
||||||
sm.switch_to("air")
|
sm.switch_to("air")
|
||||||
_eq(sm.current_state, "air")
|
_eq(sm.current_state, "air")
|
||||||
|
|
||||||
|
|
||||||
func test_air_transitions_to_ground_on_land() -> void:
|
func test_do_jump_sets_velocity_and_counters() -> void:
|
||||||
var a = Node.new()
|
|
||||||
a.name = "state_air"
|
|
||||||
a.machine = sm
|
|
||||||
sm.add_child(a)
|
|
||||||
sm.switch_to("ground")
|
sm.switch_to("ground")
|
||||||
sm.on_ground = false
|
sm.on_ground = true
|
||||||
fake_player.velocity = Vector3.ZERO
|
fake_player.velocity = Vector3(5, 0, 0)
|
||||||
sm.switch_to("air")
|
sm.do_jump()
|
||||||
fake_player.velocity = Vector3.ZERO
|
_expect(fake_player.velocity.y == params.jump_velocity, "jump sets vertical velocity")
|
||||||
sm.switch_to("ground")
|
_expect(fake_player.velocity.x == 5.0, "jump keeps horizontal momentum")
|
||||||
_eq(sm.current_state, "ground")
|
_eq(sm.current_jump_count, 1, "jump count set")
|
||||||
|
_expect(sm.jump_cooldown_timer > 0.0, "jump cooldown armed")
|
||||||
|
_expect(not sm.on_ground, "no longer grounded")
|
||||||
|
|
||||||
|
|
||||||
func test_double_jump_allowed_once() -> void:
|
func test_wall_run_preserves_fast_entry_speed() -> void:
|
||||||
params.double_jump_max_count = 1
|
# Enter a wall run at 20 m/s along the wall; speed must not drop to base.
|
||||||
var a = Node.new()
|
sm.wall_normal = Vector3.RIGHT
|
||||||
a.name = "state_air"
|
sm.wall_side = -1.0
|
||||||
a.machine = sm
|
fake_player.velocity = Vector3(0, 0, -20) # along wall tangent
|
||||||
sm.add_child(a)
|
|
||||||
sm.on_ground = false
|
|
||||||
sm.current_jump_count = 1
|
|
||||||
_eq(sm.current_jump_count, 1)
|
|
||||||
_expect(
|
|
||||||
sm.current_jump_count < params.double_jump_max_count + 1,
|
|
||||||
"should allow double jump once"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func test_wall_run_started_when_near_wall() -> void:
|
|
||||||
var g = Node.new()
|
|
||||||
g.name = "state_ground"
|
|
||||||
g.machine = sm
|
|
||||||
sm.add_child(g)
|
|
||||||
var a = Node.new()
|
|
||||||
a.name = "state_air"
|
|
||||||
a.machine = sm
|
|
||||||
sm.add_child(a)
|
|
||||||
sm.wall_normal = Vector3.RIGHT.normalized()
|
|
||||||
sm.switch_to("wall_run")
|
sm.switch_to("wall_run")
|
||||||
_eq(sm.current_state, "wall_run")
|
var hspeed := Vector3(fake_player.velocity.x, 0, fake_player.velocity.z).length()
|
||||||
|
_expect(hspeed >= 19.9, "wall run must keep entry speed (got %.1f)" % hspeed)
|
||||||
|
|
||||||
|
|
||||||
func test_chain_bonus_caps_at_50pct() -> void:
|
func test_wall_run_entry_keeps_some_upward_momentum() -> void:
|
||||||
|
sm.wall_normal = Vector3.RIGHT
|
||||||
|
fake_player.velocity = Vector3(0, 20.0, -10)
|
||||||
|
sm.switch_to("wall_run")
|
||||||
|
_expect(fake_player.velocity.y > 0.0, "some upward carry should remain")
|
||||||
|
_expect(fake_player.velocity.y <= params.wall_run_entry_max_up + 0.001,
|
||||||
|
"upward carry capped at wall_run_entry_max_up")
|
||||||
|
|
||||||
|
|
||||||
|
func test_dash_cooldown_is_per_machine() -> void:
|
||||||
|
fake_player.velocity = Vector3(10, 0, 0)
|
||||||
|
sm.wish_dir_world = Vector3(1, 0, 0)
|
||||||
|
sm.switch_to("dash")
|
||||||
|
_expect(sm.dash_cooldown_timer > 0.0, "dash arms this machine's cooldown")
|
||||||
|
|
||||||
|
# A second, independent machine must NOT share that cooldown (the old
|
||||||
|
# implementation used a static var shared across all players).
|
||||||
|
var player2 = load("res://movement/tests/fake_player.gd").new()
|
||||||
|
add_child(player2)
|
||||||
|
var sm2 := MovementStateMachine.new()
|
||||||
|
sm2.player = player2
|
||||||
|
sm2.params = MovementParams.new()
|
||||||
|
player2.add_child(sm2)
|
||||||
|
_expect(sm2.dash_cooldown_timer == 0.0, "second player's dash must be ready")
|
||||||
|
remove_child(player2)
|
||||||
|
player2.free()
|
||||||
|
|
||||||
|
|
||||||
|
func test_dash_adds_speed_to_forward_momentum() -> void:
|
||||||
|
fake_player.velocity = Vector3(12, 0, 0)
|
||||||
|
sm.wish_dir_world = Vector3(1, 0, 0)
|
||||||
|
sm.switch_to("dash")
|
||||||
|
var speed := Vector3(fake_player.velocity.x, 0, fake_player.velocity.z).length()
|
||||||
|
_expect(speed >= 12.0 + params.dash_speed - 0.01,
|
||||||
|
"forward dash should add dash_speed on top of momentum (got %.1f)" % speed)
|
||||||
|
|
||||||
|
|
||||||
|
func test_chain_bonus_soft_caps() -> void:
|
||||||
params.chain_bonus_per_success = 0.05
|
params.chain_bonus_per_success = 0.05
|
||||||
params.chain_bonus_cap = 0.50
|
params.chain_bonus_cap = 0.25
|
||||||
sm.register_chain_mechanic("jump")
|
for i in range(12):
|
||||||
sm.register_chain_mechanic("slide")
|
sm.register_chain_mechanic("jump")
|
||||||
sm.register_chain_mechanic("wall_run")
|
_eq(sm.chain_count, 12, "chain count accumulates")
|
||||||
_expect(sm.chain_count == 3, "chain_count 3 after 3 mechanics")
|
_expect(sm.current_chain_bonus <= params.chain_bonus_cap + 0.35,
|
||||||
_expect(
|
"bonus stays near cap (soft cap)")
|
||||||
sm.current_chain_bonus <= params.chain_bonus_cap + 0.001,
|
_expect(sm.current_chain_bonus >= params.chain_bonus_cap,
|
||||||
"bonus should be bounded by cap"
|
"soft cap still rewards long chains slightly")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func test_sliding_reduces_speed() -> void:
|
func test_notify_landed_emits_scaled_event() -> void:
|
||||||
params.slide_speed = 14.0
|
var events := []
|
||||||
params.slide_min_speed = 12.0
|
sm.movement_event.connect(func(ev, data): events.append([ev, data]))
|
||||||
_expect(params.slide_min_speed < params.slide_speed, "slide_min_speed must be below slide_speed")
|
sm.notify_landed(2.0) # soft: below land_soft_speed → no event
|
||||||
|
_expect(events.is_empty(), "gentle landings emit no event")
|
||||||
|
sm.notify_landed(params.land_heavy_speed + 1.0)
|
||||||
|
_expect(events.size() == 1 and events[0][0] == "land", "heavy landing emits land event")
|
||||||
|
if events.size() == 1:
|
||||||
|
_expect(events[0][1].get("heavy", false), "heavy flag set above land_heavy_speed")
|
||||||
|
_expect(sm.current_jump_count == 0, "landing resets jump count")
|
||||||
|
|
||||||
|
|
||||||
func test_dash_speed_under_effective_cap() -> void:
|
func test_slide_entry_boosts_and_keeps_direction() -> void:
|
||||||
var d = Node.new()
|
sm.switch_to("ground")
|
||||||
d.name = "state_dash"
|
fake_player.velocity = Vector3(0, 0, -12) # faster than 0.8*walk_speed
|
||||||
d.machine = sm
|
# Slide entry boost only applies when is_on_floor(); fake player isn't on a
|
||||||
sm.add_child(d)
|
# real floor, so just verify direction is kept and speed never decreases.
|
||||||
sm.on_ground = false
|
sm.switch_to("slide")
|
||||||
var eff := sm.get_effective_speed(params.dash_speed)
|
_expect(fake_player.velocity.z < 0.0, "slide keeps travel direction")
|
||||||
_expect(
|
var speed := Vector3(fake_player.velocity.x, 0, fake_player.velocity.z).length()
|
||||||
eff <= params.dash_speed * (1.0 + params.chain_bonus_cap),
|
_expect(speed >= 12.0 - 0.01, "slide entry must not lose speed (got %.1f)" % speed)
|
||||||
"dash effective speed should respect chain cap"
|
_expect(sm.states.has("slide"), "slide state present")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func test_rocket_jump_impulse_sets_upward_velocity() -> void:
|
func test_grapple_state_registered_by_machine() -> void:
|
||||||
params.rocket_jump_up_impulse = 20.0
|
_expect(sm.states.has("grapple"), "machine injects grapple state")
|
||||||
_expect(params.rocket_jump_up_impulse > 0.0, "rocket jump must have positive upward impulse")
|
_expect(sm.states.has("wall_climb"), "machine injects wall_climb state")
|
||||||
|
sm.grapple_length = 10.0
|
||||||
|
sm.switch_to("grapple")
|
||||||
|
_eq(sm.current_state, "grapple")
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
extends Object
|
||||||
|
class_name LevelMaterials
|
||||||
|
|
||||||
|
## Shared prototype-grid materials for code-built levels.
|
||||||
|
##
|
||||||
|
## Every level builder makes its geometry from flat-colored boxes; this gives
|
||||||
|
## them all the same world-space grid surface (0.5 m cells via triplanar
|
||||||
|
## mapping) so speed and distance stay readable at movement-shooter velocity,
|
||||||
|
## while keeping each builder's color coding as a tint.
|
||||||
|
|
||||||
|
const GRID_GRAY := "res://assets/textures/prototype/grid_gray.png"
|
||||||
|
const GRID_DARK := "res://assets/textures/prototype/grid_dark.png"
|
||||||
|
|
||||||
|
## One texture tile = 2 m of world, so one grid cell = 0.5 m.
|
||||||
|
const WORLD_UNITS_PER_TILE := 2.0
|
||||||
|
|
||||||
|
static var _cache: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
## A tinted grid material. Cached per (tint, dark) so identical surfaces share
|
||||||
|
## one material (fewer draw-state changes, and edits apply everywhere).
|
||||||
|
static func tinted(tint: Color, dark: bool = false) -> StandardMaterial3D:
|
||||||
|
var key := "%s|%s" % [tint.to_html(), dark]
|
||||||
|
if _cache.has(key):
|
||||||
|
return _cache[key]
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
var tex_path := GRID_DARK if dark else GRID_GRAY
|
||||||
|
var tex: Texture2D = load(tex_path) if ResourceLoader.exists(tex_path) else null
|
||||||
|
if tex:
|
||||||
|
mat.albedo_texture = tex
|
||||||
|
# World triplanar: boxes need no UVs and the grid stays world-scaled.
|
||||||
|
mat.uv1_triplanar = true
|
||||||
|
mat.uv1_world_triplanar = true
|
||||||
|
mat.uv1_scale = Vector3.ONE / WORLD_UNITS_PER_TILE
|
||||||
|
# The texture is grayscale ~mid value; multiply by ~2x-brightened tint to
|
||||||
|
# land near the original flat color while keeping the grid contrast.
|
||||||
|
mat.albedo_color = Color(
|
||||||
|
minf(tint.r * 1.9, 1.0), minf(tint.g * 1.9, 1.0), minf(tint.b * 1.9, 1.0))
|
||||||
|
mat.roughness = 0.85
|
||||||
|
_cache[key] = mat
|
||||||
|
return mat
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b1v4lq35py3gd
|
||||||
@@ -176,10 +176,7 @@ func _create_box(node_name: String, pos: Vector3, size: Vector3, color: Color) -
|
|||||||
mesh.name = "MeshInstance3D"
|
mesh.name = "MeshInstance3D"
|
||||||
mesh.mesh = BoxMesh.new()
|
mesh.mesh = BoxMesh.new()
|
||||||
mesh.mesh.size = size
|
mesh.mesh.size = size
|
||||||
var mat := StandardMaterial3D.new()
|
mesh.mesh.surface_set_material(0, LevelMaterials.tinted(color))
|
||||||
mat.albedo_color = color
|
|
||||||
mat.roughness = 0.8
|
|
||||||
mesh.mesh.surface_set_material(0, mat)
|
|
||||||
body.add_child(mesh)
|
body.add_child(mesh)
|
||||||
|
|
||||||
add_child(body)
|
add_child(body)
|
||||||
@@ -206,10 +203,7 @@ func _create_ramp(node_name: String, pos: Vector3, size: Vector3, rot_deg: Vecto
|
|||||||
mesh.name = "MeshInstance3D"
|
mesh.name = "MeshInstance3D"
|
||||||
mesh.mesh = BoxMesh.new()
|
mesh.mesh = BoxMesh.new()
|
||||||
mesh.mesh.size = size
|
mesh.mesh.size = size
|
||||||
var mat := StandardMaterial3D.new()
|
mesh.mesh.surface_set_material(0, LevelMaterials.tinted(color))
|
||||||
mat.albedo_color = color
|
|
||||||
mat.roughness = 0.8
|
|
||||||
mesh.mesh.surface_set_material(0, mat)
|
|
||||||
body.add_child(mesh)
|
body.add_child(mesh)
|
||||||
|
|
||||||
add_child(body)
|
add_child(body)
|
||||||
|
|||||||
Reference in New Issue
Block a user