Initial project sync
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
class_name TimeAnchorChartTool
|
||||
extends RefCounted
|
||||
|
||||
## Chart-side tooling for time anchors (design doc 3.4):
|
||||
## generate explicit periodic anchors, validate anchor/mask configuration,
|
||||
## and simulate the runtime periodic scheduler for a constant streak.
|
||||
|
||||
const ChartEventScript := preload("res://resources/chart_event.gd")
|
||||
|
||||
const VALID_TIME_PHASES: Array[StringName] = [&"past", &"future"]
|
||||
const VALID_TIME_PHASE_MASKS: Array[StringName] = [&"", &"both", &"past", &"future"]
|
||||
|
||||
|
||||
static func validate_chart(chart: Resource, beat_time := 0.5, bad_window := 0.2, judgement_scale := 1.0) -> Array[String]:
|
||||
var errors: Array[String] = []
|
||||
if chart == null:
|
||||
errors.append("chart is null")
|
||||
return errors
|
||||
if bad_window * judgement_scale > beat_time * 0.5:
|
||||
errors.append("bad window %.3f exceeds half a beat %.3f; overlapping anchor windows at the 1-beat tier" % [bad_window * judgement_scale, beat_time * 0.5])
|
||||
var initial = chart.get("initial_time_phase")
|
||||
if initial != null and not VALID_TIME_PHASES.has(StringName(str(initial))):
|
||||
errors.append("initial_time_phase must be past/future, got %s" % str(initial))
|
||||
var seen_anchor_beats: Dictionary = {}
|
||||
for event: Resource in chart.call("all_events"):
|
||||
var event_key := str(event.call("key"))
|
||||
var mask = event.get("time_phase_mask")
|
||||
if mask != null and not VALID_TIME_PHASE_MASKS.has(StringName(str(mask))):
|
||||
errors.append("%s: invalid time_phase_mask %s" % [event_key, str(mask)])
|
||||
var event_type := StringName(str(event.get("event_type")))
|
||||
if event_type == &"time_anchor":
|
||||
if int(event.get("subdivision")) != 0:
|
||||
errors.append("%s: time_anchor must sit on an integer beat" % event_key)
|
||||
var beat := int(event.get("beat_index"))
|
||||
if seen_anchor_beats.has(beat):
|
||||
errors.append("%s: duplicate time_anchor on beat %d" % [event_key, beat])
|
||||
seen_anchor_beats[beat] = true
|
||||
elif event_type == &"force_time_phase":
|
||||
var payload = event.get("payload")
|
||||
var target := StringName(str(payload.get("target_time_phase", ""))) if payload is Dictionary else &""
|
||||
if not VALID_TIME_PHASES.has(target):
|
||||
errors.append("%s: force_time_phase needs payload.target_time_phase past/future" % event_key)
|
||||
return errors
|
||||
|
||||
|
||||
static func validate_time_phase_profile(profile: Resource) -> Array[String]:
|
||||
var errors: Array[String] = []
|
||||
if profile == null:
|
||||
return errors
|
||||
for property_name: String in ["past_effects", "future_effects"]:
|
||||
var definitions = profile.get(property_name)
|
||||
if not definitions is Array:
|
||||
continue
|
||||
for definition: Variant in definitions:
|
||||
if definition is Resource and StringName(str((definition as Resource).get("duration_type"))) != &"infinite":
|
||||
errors.append("%s: effect %s must be duration_type infinite (lifecycle owned by TimePhaseAdapter)" % [property_name, str((definition as Resource).get("id"))])
|
||||
return errors
|
||||
|
||||
|
||||
static func generate_periodic_time_anchor_events(interval_beats: int, total_beats: int, lead_beats := 2.0) -> Array[Resource]:
|
||||
var events: Array[Resource] = []
|
||||
var safe_interval := maxi(1, interval_beats)
|
||||
var beat := safe_interval
|
||||
while beat < total_beats:
|
||||
var event: Resource = ChartEventScript.new()
|
||||
event.set("event_id", StringName("time_anchor_%d" % beat))
|
||||
event.set("beat_index", beat)
|
||||
event.set("event_type", &"time_anchor")
|
||||
event.set("lead_beats", lead_beats)
|
||||
events.append(event)
|
||||
beat += safe_interval
|
||||
return events
|
||||
|
||||
|
||||
static func simulate_periodic_schedule(frequency_profile: Resource, streak: int, total_beats: int, explicit_beats: Array[int] = []) -> Array[int]:
|
||||
var window_beats := 4
|
||||
if frequency_profile != null and frequency_profile.get("window_beats") != null:
|
||||
window_beats = maxi(1, int(frequency_profile.get("window_beats")))
|
||||
var offsets: Array[int] = [window_beats]
|
||||
if frequency_profile != null and frequency_profile.has_method("anchor_offsets_for_streak"):
|
||||
var profile_offsets = frequency_profile.call("anchor_offsets_for_streak", streak)
|
||||
if profile_offsets is Array and not profile_offsets.is_empty():
|
||||
offsets = []
|
||||
for offset: Variant in profile_offsets:
|
||||
offsets.append(clampi(int(offset), 1, window_beats))
|
||||
var scheduled: Array[int] = []
|
||||
var origin := 0
|
||||
while origin < total_beats:
|
||||
var explicit_hit := false
|
||||
for explicit_beat: int in explicit_beats:
|
||||
if explicit_beat > origin and explicit_beat <= origin + window_beats:
|
||||
origin = explicit_beat
|
||||
explicit_hit = true
|
||||
break
|
||||
if explicit_hit:
|
||||
continue
|
||||
for offset: int in offsets:
|
||||
var beat := origin + offset
|
||||
if beat < total_beats:
|
||||
scheduled.append(beat)
|
||||
origin += window_beats
|
||||
return scheduled
|
||||
Reference in New Issue
Block a user