extends Node2D # Original tutorial adapted from the verified Forge sprite integration fixture. # Idle art stays idle art: no claim that these five frames contain a walk cycle. const START := Vector2(90, 200) const SPEED := 150.0 const ACTIONS := ["forge_left", "forge_right", "forge_up", "forge_down"] var player: CharacterBody2D var sprite: AnimatedSprite2D var label: Label var coins: Array[Area2D] = [] var score := 0 var won := false var frozen := false var lost := false var remaining := 60.0 var patrol: Area2D var patrol_direction := 1.0 func _ready() -> void: for mapping in [["forge_left", KEY_LEFT, KEY_A], ["forge_right", KEY_RIGHT, KEY_D], ["forge_up", KEY_UP, KEY_W], ["forge_down", KEY_DOWN, KEY_S], ["forge_restart", KEY_R], ["forge_pause", KEY_P]]: if not InputMap.has_action(mapping[0]): InputMap.add_action(mapping[0]) InputMap.action_erase_events(mapping[0]) for key in mapping.slice(1): var event := InputEventKey.new() event.physical_keycode = key InputMap.action_add_event(mapping[0], event) for box in [Rect2(0, 0, 640, 12), Rect2(0, 388, 640, 12), Rect2(0, 0, 12, 400), Rect2(628, 0, 12, 400), Rect2(260, 110, 20, 180)]: var wall := StaticBody2D.new() wall.position = box.get_center() wall.collision_layer = 1 wall.collision_mask = 2 var shape := CollisionShape2D.new() var rectangle := RectangleShape2D.new() rectangle.size = box.size shape.shape = rectangle wall.add_child(shape) add_child(wall) player = CharacterBody2D.new() player.motion_mode = CharacterBody2D.MOTION_MODE_FLOATING player.collision_layer = 2 player.collision_mask = 1 var collision := CollisionShape2D.new() var body_shape := CircleShape2D.new() body_shape.radius = 10 collision.shape = body_shape player.add_child(collision) sprite = AnimatedSprite2D.new() sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST sprite.scale = Vector2(1.5, 1.5) var metadata = JSON.parse_string(FileAccess.get_file_as_string("res://forge-sprites.json")) var frames := SpriteFrames.new() frames.set_animation_speed("default", metadata.fps) frames.set_animation_loop("default", true) var texture := load("res://forge-sprites.png") as Texture2D for frame in metadata.frames: var atlas := AtlasTexture.new() atlas.atlas = texture atlas.region = Rect2(frame.cell.x, frame.cell.y, frame.cell.width, frame.cell.height) atlas.filter_clip = true frames.add_frame("default", atlas) sprite.sprite_frames = frames player.add_child(sprite) add_child(player) for location in [Vector2(190, 200), Vector2(420, 75), Vector2(480, 310)]: var coin := Area2D.new() coin.position = location coin.collision_layer = 4 coin.collision_mask = 2 var area_shape := CollisionShape2D.new() var circle := CircleShape2D.new() circle.radius = 14 area_shape.shape = circle coin.add_child(area_shape) coin.body_entered.connect(func(body: Node2D): collect(coin, body)) coins.append(coin) add_child(coin) patrol = Area2D.new() patrol.position = Vector2(200, 320) patrol.collision_layer = 8 patrol.collision_mask = 2 var patrol_shape := CollisionShape2D.new() var patrol_circle := CircleShape2D.new() patrol_circle.radius = 12 patrol_shape.shape = patrol_circle patrol.add_child(patrol_shape) patrol.body_entered.connect(func(body: Node2D): if body == player and not won and not lost and not frozen: lose() ) add_child(patrol) label = Label.new() label.position = Vector2(24, 24) add_child(label) restart() if "--forge-acceptance" in OS.get_cmdline_user_args(): var verifier = load("res://checks.gd").new() add_child(verifier) func _physics_process(delta: float) -> void: if not won and not lost and not frozen: remaining = maxf(0.0, remaining - delta) patrol.position.x = clampf(patrol.position.x + patrol_direction * 70.0 * delta, 150.0, 240.0) if patrol.position.x >= 240.0 or patrol.position.x <= 150.0: patrol_direction *= -1.0 if remaining <= 0.0: lose() refresh() queue_redraw() player.velocity = Vector2.ZERO if won or lost or frozen else Input.get_vector(ACTIONS[0], ACTIONS[1], ACTIONS[2], ACTIONS[3]) * SPEED player.move_and_slide() if Input.is_action_just_pressed("forge_restart"): restart() if Input.is_action_just_pressed("forge_pause") and not won and not lost: frozen = not frozen clear_movement() if frozen: sprite.pause() else: sprite.play() refresh() func collect(coin: Area2D, body: Node2D) -> void: if body != player or not coin.visible or won or lost or frozen: return coin.hide() score += 1 if score == 3: won = true clear_movement() sprite.pause() refresh() queue_redraw() func lose() -> void: if won or lost: return lost = true clear_movement() sprite.pause() refresh() queue_redraw() func clear_movement() -> void: for action in ACTIONS: Input.action_release(action) if is_instance_valid(player): player.velocity = Vector2.ZERO func _notification(what: int) -> void: if what == NOTIFICATION_APPLICATION_FOCUS_OUT: clear_movement() if is_instance_valid(sprite): frozen = true sprite.pause() refresh() func restart() -> void: clear_movement() player.position = START score = 0 won = false lost = false remaining = 60.0 patrol.position = Vector2(200, 320) patrol_direction = 1.0 frozen = false for coin in coins: coin.show() sprite.stop() sprite.play() refresh() queue_redraw() func refresh() -> void: label.text = ("Goal reached! R to restart" if won else ("Run ended! R to restart" if lost else "Collect 3 circles: %s/3 | %ss left" % [score, ceili(remaining)])) + "\nArrows / WASD move | P pause/resume | R restart" + ("\nPaused — press P to resume" if frozen else "") func _draw() -> void: draw_rect(Rect2(0, 0, 640, 400), Color("08131f")) for box in [Rect2(0, 0, 640, 12), Rect2(0, 388, 640, 12), Rect2(0, 0, 12, 400), Rect2(628, 0, 12, 400), Rect2(260, 110, 20, 180)]: draw_rect(box, Color("526578")) for coin in coins: if coin.visible: draw_circle(coin.position, 10, Color("ffb547")) if is_instance_valid(patrol): draw_circle(patrol.position, 12, Color("ef6576"))