extends ArcadeGame const Model = preload("res://games/pacman/pacman_model.gd") const View = preload("res://games/pacman/pacman_view.gd") const Sound = preload("res://games/pacman/pacman_audio.gd") @export_group("Room Objective") ## Zero means unlimited boards. Set a score target or use arcade mode. @export_range(0, 100, 1) var boards_to_win: int = 1 @export_range(0, 1000000, 100) var score_to_win: int = 0 @export_range(1, 9, 1) var starting_lives: int = 3 @export var extra_life_score: int = 10000 var model = Model.new() var view var audio var accumulator := 0.0 signal power_state_changed(state: String) const BOOT_SECONDS := 4.0 var power_state := "off" var boot_elapsed := 0.0 var credits := 0 var rfid_seen: Dictionary = {} var rfid_initialized := false func receive_device_feedback(feedback: Dictionary) -> void: var events: Array = feedback.get("events", []) if not rfid_initialized: if not bool(feedback.get("fresh", false)): return for entry in events: rfid_seen[str(entry.get("id", ""))] = true rfid_initialized = true return if not bool(feedback.get("fresh", false)): return var device_id := str(run_config.get("rfid_credit_device", "")) for entry in events: var ident := str(entry.get("id", "")) if ident.is_empty() or rfid_seen.has(ident): continue rfid_seen[ident] = true if power_state == "off" and entry.get("device") == run_config.get("power_on_device", "") and entry.get("kind") == "input" and entry.get("value") == true and str(entry.get("error", "")).is_empty(): for sample in feedback.get("devices", []): if sample.get("id") == entry.get("device") and int(entry.get("age_ms", 999999)) < 2000 and int(sample.get("age_ms", 999999)) < 2000 and str(sample.get("error", "")).is_empty(): power_on() if power_state != "ready" or device_id.is_empty() or is_running or credits > 0 or model.state != "idle": continue if entry.get("device") != device_id or entry.get("kind") != "rfid" or not str(entry.get("error", "")).is_empty(): continue var uid := str(entry.get("value", "")).to_upper() if uid.is_empty(): continue var allowed: Array = run_config.get("rfid_allowed_uids", []) if not allowed.is_empty() and not allowed.has(uid): continue for sample in feedback.get("devices", []): if sample.get("id") == device_id and int(sample.get("age_ms", 999999)) < 2000 and sample.get("value") == entry.get("value") and str(sample.get("error", "")).is_empty(): credits = 1 view.credits = credits audio.play_effect("credit") break # The bridge retains only its last 64 events. Keep deduplication bounded too. if rfid_seen.size() > 256: rfid_seen.clear() for entry in events: rfid_seen[str(entry.get("id", ""))] = true func _ready() -> void: view = View.new() view.model = model add_child(view) audio = Sound.new() add_child(audio) model.sound_requested.connect(audio.play_effect) model.finished.connect(_on_finished) _apply_config() if str(run_config.get("power_on_device", "")).is_empty(): power_on() func configure(config: Dictionary = {}) -> void: power_off() super.configure(config) credits = 0 rfid_seen.clear() rfid_initialized = false _apply_config() if str(run_config.get("power_on_device", "")).is_empty(): power_on() func power_on() -> bool: if power_state != "off": return false boot_elapsed = 0.0 power_state = "booting" view.power_state = power_state view.startup_time = 0.0 audio.stop_all() power_state_changed.emit(power_state) return true func power_off() -> void: is_running = false credits = 0 boot_elapsed = 0.0 power_state = "off" model.reset() accumulator = 0.0 if audio != null: audio.stop_all() if view != null: view.power_state = power_state view.startup_time = 0.0 view.credits = 0 power_state_changed.emit(power_state) func _apply_config() -> void: if view != null: view.credit_required = not str(run_config.get("rfid_credit_device", "")).is_empty() view.credits = credits model.boards_to_win = maxi(0, int(run_config.get("boards_to_win", boards_to_win))) model.score_to_win = maxi(0, int(run_config.get("score_to_win", score_to_win))) model.initial_lives = clampi(int(run_config.get("starting_lives", starting_lives)), 1, 9) model.extra_life_score = maxi(0, int(run_config.get("extra_life_score", extra_life_score))) func start_game() -> bool: if is_running or power_state != "ready": return false if not str(run_config.get("rfid_credit_device", "")).is_empty(): if credits < 1: return false credits -= 1 _apply_config() audio.stop_all() model.start() accumulator = 0.0 return super.start_game() func reset_game() -> void: credits = 0 if view != null: view.credits = 0 audio.stop_all() model.reset() accumulator = 0.0 super.reset_game() func win_game() -> void: if not is_running: return model.state = "won" audio.stop_all() super.win_game() func lose_game() -> void: if not is_running: return model.state = "lost" audio.stop_all() super.lose_game() func _on_finished(won: bool) -> void: if won: win_game() else: lose_game() func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("arcade_start"): start_game() get_viewport().set_input_as_handled() for pair in [["arcade_up", Vector2i.UP], ["arcade_left", Vector2i.LEFT], ["arcade_down", Vector2i.DOWN], ["arcade_right", Vector2i.RIGHT]]: if event.is_action_pressed(pair[0]): model.requested = pair[1] func _physics_process(delta: float) -> void: if power_state == "off": return if power_state == "booting": boot_elapsed = minf(BOOT_SECONDS, boot_elapsed + delta) view.startup_time = boot_elapsed if boot_elapsed >= BOOT_SECONDS: power_state = "ready" view.power_state = power_state view.startup_time = 0.0 # Ignore scans collected while the ROM check was running. rfid_initialized = false power_state_changed.emit(power_state) return if not is_running: view.startup_time += delta if not is_running: model.clock += delta if is_running: accumulator += minf(delta, 0.25) while accumulator >= 1.0 / 60.0 and is_running: model.update(1.0 / 60.0) accumulator -= 1.0 / 60.0 if audio != null: audio.set_background(model.background_sound() if is_running else "")