extends Node
## GATE — THE DICE MODIFIER (#120 L1, docs/node-tree-and-stacks.md §12.4, ModFold).
## D1 a 2 x 1 x 2 box diced 2 x 1 x 2: four 1 x 1 x 1 cells, one per quarter
## D2 every cell is solid exactly once (no overlap, no gap) at the quarter centres
## D3 dice runs BEFORE the copies whatever the record order: + an array of 2 along x
## copies the four cells (8 pieces)
## D4 a cut across the cells cuts each cell it reaches (more pieces than before)
## D5 a second fold names the same pieces
## D6 remove the dice: the box is one piece again (the array copies it whole)
##
## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_mod_dice.tscn
var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var level: Node
func _ok(what: String, cond: bool, note: Variant = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", str(note)])
func _ready() -> void:
var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
gs.route("world:level:*", 1, "peer")
var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "gate-author"
get_tree().root.get_node("/root/GameMode").set_mode("build")
map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
add_child(map)
for i in 6:
await get_tree().process_frame
await _run()
get_tree().root.get_node("/root/GameMode").set_mode("play")
if checks < 7:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE mod_dice: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _box(at: Vector3, size: Vector3) -> String:
var d: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, at)),
"size": [size.x, size.y, size.z], "material_id": 2, "level_id": 0})
persist.refold()
return d
func _solids(dot: String) -> Array:
var out: Array = []
for b: int in (level.get_group_brush_ids(dot) as PackedInt32Array):
var d: Dictionary = level.get_brush(b)
if not bool(d.get("clearance", false)) and not bool(d.get("cutter", false)):
out.append(b)
return out
## Each solid piece's lowest corner, "x,z" rounded: one entry per piece, sorted.
func _cells(dot: String) -> Array:
var out: Array = []
for b: int in _solids(dot):
var lo := Vector3(INF, INF, INF)
for v: Vector3 in FaceKit.corners(level.get_brush(b)["planes"]):
lo = lo.min(v)
out.append("%d,%d" % [roundi(lo.x), roundi(lo.z)])
out.sort()
return out
func _count(dot: String, p: Vector3) -> int:
var n := 0
for b: int in _solids(dot):
var pl: PackedFloat32Array = level.get_brush(b)["planes"]
var inside := true
for i in pl.size() / 4:
if Vector3(pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2]).dot(p) > pl[i * 4 + 3] + 1.0e-4:
inside = false
break
if inside:
n += 1
return n
func _keys(dot: String) -> Array:
var out: Array = []
for b: int in (level.get_group_brush_ids(dot) as PackedInt32Array):
out.append(String(level.get_brush_key(b)))
out.sort()
return out
func _run() -> void:
persist = map.get("_persist")
level = map.get("level")
persist.refold()
var s := _box(Vector3(120, 0, 120), Vector3(2, 1, 2))
# ---- D3 (record order): the array is added FIRST, the dice after it ----
var arr: String = map.add_modifier("array", s, {"count": 2})
var dice: String = map.add_modifier("dice", s, {"count": [2, 1, 2]})
# ---- D1 / D2 (the original box's four cells, x 120..122) ----
var c: Array = _cells(s).filter(func(k: String) -> bool: return int(k.get_slice(",", 0)) < 122)
_ok("D1 diced 2 x 1 x 2: four cells in the original box", dice != "" and c == ["120,120", "120,121", "121,120", "121,121"], c)
var once := true
for q: Vector3 in [Vector3(120.5, 0.5, 120.5), Vector3(121.5, 0.5, 120.5), Vector3(120.5, 0.5, 121.5), Vector3(121.5, 0.5, 121.5)]:
if _count(s, q) != 1:
once = false
_ok("D2 each quarter centre is solid exactly once", once)
# ---- D3 ----
_ok("D3 dice before the copies: the array copies the cells (8 pieces)", _solids(s).size() == 8, _solids(s).size())
# ---- D4 ----
var cutter := _box(Vector3(120.5, -0.5, 120.5), Vector3(1, 2, 1))
persist.begin_gesture("Live subtract")
var op: String = persist.record("op.create", {"op": "subtract", "cutter": cutter, "targets": [s]})
persist.end_gesture()
persist.refold()
_ok("D4 a cut across the cells: the hole is air", _count(s, Vector3(121, 0.5, 121)) == 0)
_ok("D4 ... and beside it still solid once", _count(s, Vector3(120.2, 0.5, 120.2)) == 1)
# ---- D5 ----
var k1 := _keys(s)
persist.refold()
_ok("D5 a second fold names the same pieces", _keys(s) == k1, k1.size())
# ---- D6 ----
map.remove_op(op)
map.remove_modifier(dice)
_ok("D6 dice removed: the box and its one array copy (2 pieces)", _solids(s).size() == 2, _solids(s).size())
map.remove_modifier(arr)