Basic game template addon
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 6s
Create tag and build when new code gets to main / Export (push) Successful in 1m1s

This commit is contained in:
2026-01-30 19:45:56 +01:00
parent b923f6bec2
commit 44f251ed66
406 changed files with 12602 additions and 1 deletions

View File

@@ -0,0 +1,70 @@
#!/bin/bash
# asset checker command
# Used for quickly checking that assets (like audio files) are being used where expected.
#
# Recursively searches through scene files (.tscn, .scn, .res)
# for occurrences of asset types (default: AudioStream).
# It then outputs the paths of assets discovered,
# along with the file names that use them.
short_flag=false
asset_type="AudioStream"
print_usage() {
printf "Usage: -sa %s\n" "$asset_type"
}
while getopts 'a:s' flag; do
case "${flag}" in
a)
asset_type="${OPTARG}"
;;
s)
short_flag=true
;;
*)
print_usage
exit 1
;;
esac
done
# Initialize an associative array to store paths and corresponding files
declare -A path_files
while IFS=: read -r file line; do
path=$(echo "$line" | grep -o 'path="[^"]*' | cut -d'"' -f2)
if [ -n "$path" ]; then
# Append the current file to the string of files for this path
# Note: Bash does not support having arrays as values of associative array.
# Using a pipe `|` separator instead, and then splitting on output
if [ -z "${path_files["$path"]}" ]; then
path_files["$path"]=$file
else
path_files["$path"]+="|$file"
fi
fi
done < <(egrep -ir --include=*.{tscn,scn,res} "type=\"$asset_type\"")
# Get the paths and sort them
sorted_paths=()
for key in "${!path_files[@]}"; do
sorted_paths+=("$key")
done
IFS=$'\n' sorted_paths=($(sort <<< "${sorted_paths[*]}"))
unset IFS
# Print out the results
for path in "${sorted_paths[@]}"; do
# Note: Bash does not support having arrays as values of associative array.
# Splitting the concatenated files string on the pipe `|` separator.
IFS='|' read -r -a files_array <<< "${path_files[$path]}"
files_count=${#files_array[@]}
printf "%-80s | Uses: %s\n" "$path" "$files_count"
if ! $short_flag ; then
for file in "${files_array[@]}"; do
printf "\t%82s\n" "$file"
done
echo
fi
done

View File

@@ -0,0 +1,281 @@
# MIT License
# Copyright (c) 2018 BARICHELLO
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Original file: https://github.com/abarichello/godot-ci/blob/master/.github/workflows/godot-ci.yml
# This edited version is triggered using a Github Release and uploads artifacts to the release.
# Furthermore, a new job (upload to itch.io using butler) has been added.
# Modified by: Nicolas Oulianov (github @oulianov) & Marek Belski
name: "Build and Deploy to Itch.io"
on:
release:
types: [published]
workflow_dispatch:
permissions:
# Allow release asset uploads
contents: write
env:
# Set this repository variable to the version to build with (>=4)
# This will be used for all container images: barichello/godot-ci:[#.#]
# Look at available versions here: https://hub.docker.com/r/barichello/godot-ci/tags
GODOT_VERSION: ${{ vars.GODOT_VERSION }}
# Set this repository variable to your game name (it will be the filename downloaded)
EXPORT_NAME: ${{ vars.EXPORT_NAME }}
# NOTE: If your `project.godot` is at the repository root, set `PROJECT_PATH` to "."
# If it's in a subdirectory, set it to the subdirectory name (e.g., "your-game")
PROJECT_PATH: .
jobs:
export-web:
name: Web Export
runs-on: ubuntu-24.04
container:
image: barichello/godot-ci:${{ vars.GODOT_VERSION }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: true
- name: Setup
run: |
mkdir -v -p ~/.local/share/godot/export_templates/
mkdir -v -p ~/.config/
mv /root/.config/godot ~/.config/godot || true
mv /root/.local/share/godot/export_templates/${GODOT_VERSION}.stable ~/.local/share/godot/export_templates/${GODOT_VERSION}.stable || true
- name: Web Build
run: |
mkdir -v -p build/web
EXPORT_DIR="$(readlink -f build)"
cd $PROJECT_PATH
godot --headless --verbose --export-release "Web" "$EXPORT_DIR/web/index.html"
- name: Prepare web release asset (zip)
run: |
# ensure zip is available in the container
if ! command -v zip >/dev/null 2>&1; then
apt-get update && apt-get install -y zip
fi
# Change to the web directory and zip its contents directly
cd build/web
zip -r ../${EXPORT_NAME}-web.zip .
ls -lh ../${EXPORT_NAME}-web.zip
shell: bash
- name: Upload to GitHub Release (if this run is a release)
if: ${{ github.event_name == 'release' }}
uses: svenstaro/upload-release-action@v2
with:
file: build/${{ env.EXPORT_NAME }}-web.zip
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: web
path: build/web
export-windows:
name: Windows Export
runs-on: ubuntu-24.04
container:
image: barichello/godot-ci:${{ vars.GODOT_VERSION }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: true
- name: Setup
run: |
mkdir -v -p ~/.local/share/godot/export_templates/
mkdir -v -p ~/.config/
mv /root/.config/godot ~/.config/godot || true
mv /root/.local/share/godot/export_templates/${GODOT_VERSION}.stable ~/.local/share/godot/export_templates/${GODOT_VERSION}.stable || true
- name: Windows Build
run: |
mkdir -v -p build/windows
EXPORT_DIR="$(readlink -f build)"
cd $PROJECT_PATH
godot --headless --verbose --export-release "Windows Desktop" "$EXPORT_DIR/windows/${EXPORT_NAME}.exe"
- name: Archive Build
run: |
# ensure zip is available in the container
if ! command -v zip >/dev/null 2>&1; then
apt-get update && apt-get install -y zip
fi
# Change to the web directory and zip its contents directly
cd build/windows
zip -r ../${EXPORT_NAME}-windows.zip .
ls -lh ../${EXPORT_NAME}-windows.zip
shell: bash
- name: Upload to GitHub Release (if this run is a release)
if: ${{ github.event_name == 'release' }}
uses: svenstaro/upload-release-action@v2
with:
file: build/${{ env.EXPORT_NAME }}-windows.zip
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: windows
path: build/${{ env.EXPORT_NAME }}-windows.zip
export-linux:
name: Linux Export
runs-on: ubuntu-24.04
container:
image: barichello/godot-ci:${{ vars.GODOT_VERSION }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: true
- name: Setup
run: |
mkdir -v -p ~/.local/share/godot/export_templates/
mkdir -v -p ~/.config/
mv /root/.config/godot ~/.config/godot || true
mv /root/.local/share/godot/export_templates/${GODOT_VERSION}.stable ~/.local/share/godot/export_templates/${GODOT_VERSION}.stable || true
- name: Linux Build
run: |
mkdir -v -p build/linux
EXPORT_DIR="$(readlink -f build)"
cd $PROJECT_PATH
godot --headless --verbose --export-release "Linux" "$EXPORT_DIR/linux/${EXPORT_NAME}.x86_64"
- name: Archive Build
run: |
# ensure zip is available in the container
if ! command -v zip >/dev/null 2>&1; then
apt-get update && apt-get install -y zip
fi
# Change to the web directory and zip its contents directly
cd build/linux
zip -r ../${EXPORT_NAME}-linux.zip .
ls -lh ../${EXPORT_NAME}-linux.zip
shell: bash
- name: Upload to GitHub Release (if this run is a release)
if: ${{ github.event_name == 'release' }}
uses: svenstaro/upload-release-action@v2
with:
file: build/${{ env.EXPORT_NAME }}-linux.zip
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: linux
path: build/${{ env.EXPORT_NAME }}-linux.zip
export-mac:
name: macOS Export
runs-on: ubuntu-24.04
container:
image: barichello/godot-ci:${{ vars.GODOT_VERSION }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: true
- name: Setup
run: |
mkdir -v -p ~/.local/share/godot/export_templates/
mkdir -v -p ~/.config/
mv /root/.config/godot ~/.config/godot || true
mv /root/.local/share/godot/export_templates/${GODOT_VERSION}.stable ~/.local/share/godot/export_templates/${GODOT_VERSION}.stable || true
- name: Mac Build
run: |
mkdir -v -p build/mac
EXPORT_DIR="$(readlink -f build)"
cd $PROJECT_PATH
godot --headless --verbose --export-release "macOS" "$EXPORT_DIR/mac/${EXPORT_NAME}-mac.zip"
- name: Upload to GitHub Release (if this run is a release)
if: ${{ github.event_name == 'release' }}
uses: svenstaro/upload-release-action@v2
with:
file: build/mac/${{ env.EXPORT_NAME }}-mac.zip
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: mac
path: build/mac
deploy-to-itch:
name: Deploy to Itch.io
needs: [export-web, export-windows, export-linux, export-mac]
runs-on: ubuntu-24.04
env:
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }} # Create this SECRET on Github
ITCH_USERNAME: ${{ vars.ITCH_USERNAME }} # Create this VARIABLE on Github
ITCH_GAME: ${{ vars.ITCH_GAME }} # Create this VARIABLE on Github
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: builds
- name: Install Butler
run: |
curl -L -o butler.zip https://broth.itch.zone/butler/linux-amd64/LATEST/archive/default
unzip butler.zip
chmod +x butler
./butler -V
- name: Get version from tag or set default
id: version
run: |
if [ "${{ github.event_name }}" = "release" ]; then
VERSION="${{ github.event.release.tag_name }}"
else
VERSION="dev-${{ github.run_number }}"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Deploying version: $VERSION"
- name: Upload to Itch.io - Web
run: |
./butler push builds/web ${{ env.ITCH_USERNAME }}/${{ env.ITCH_GAME }}:html5 --userversion "${{ steps.version.outputs.version }}"
- name: Upload to Itch.io - Windows
run: |
./butler push builds/windows ${{ env.ITCH_USERNAME }}/${{ env.ITCH_GAME }}:windows --userversion "${{ steps.version.outputs.version }}"
- name: Upload to Itch.io - Linux
run: |
./butler push builds/linux ${{ env.ITCH_USERNAME }}/${{ env.ITCH_GAME }}:linux --userversion "${{ steps.version.outputs.version }}"
- name: Upload to Itch.io - macOS
run: |
./butler push builds/mac ${{ env.ITCH_USERNAME }}/${{ env.ITCH_GAME }}:mac --userversion "${{ steps.version.outputs.version }}"

View File

@@ -0,0 +1,44 @@
#!/bin/bash
# butler manager command
# Uploads directories as builds to matching itch.io channels.
# HTML5 => html5
# Linux => linux
# Windows => win
# MacOS => osx
file=upload_destination.txt
directories=("HTML5" "Linux" "Windows" "MacOS")
channels=("html5" "linux" "win" "osx")
# Check if the file exists
if [ ! -e $file ]; then
# File doesn't exist, create an empty one
touch $file
fi
# File exists, read the first line into a variable
read -r destination < $file
if [ -z "$destination" ]; then
# File is empty, prompt the user for input
echo "Please enter the build destination (username/project-url-after-slash)."
read -r user_input
# Save user input to the file
echo "$user_input" > "$file"
echo "Destination saved to $file."
destination="$user_input"
fi
# Check for the existence of directories and upload contents
for ((i=0; i<${#directories[@]}; i++)); do
dir="${directories[i]}"
channel="${channels[i]}"
if [ -d "$dir" ]; then
echo butler push ./$dir/ $destination:$channel
butler push ./$dir/ $destination:$channel
else
echo "Directory '$dir' does not exist."
fi
done

View File

@@ -0,0 +1,8 @@
extends Control
## Control node that captures the mouse for games that require it.
##
## Used for games that use the mouse to move the camera (ex. FPS or third-person shooters).
func _gui_input(event):
if event is InputEventMouseButton and Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

View File

@@ -0,0 +1 @@
uid://dqdyrkm3jily6

View File

@@ -0,0 +1,51 @@
@tool
class_name LevelLoader
extends Node
## Loads scenes into a container.
signal level_load_started
signal level_loaded
signal level_ready
## Container where the level instance will be added.
@export var level_container : Node
## Optional reference to a loading screen in the scene.
## Requires Maaack's Scene Loader.
@export var level_loading_screen : Node
@export_group("Debugging")
@export var current_level : Node
## If Maaack's Scene Loader is installed, then it will be used to change scenes.
@onready var scene_loader_node = get_tree().root.get_node_or_null(^"SceneLoader")
var is_loading : bool = false
func _attach_level(level_resource : Resource):
assert(level_container != null, "level_container is null")
var instance = level_resource.instantiate()
level_container.call_deferred("add_child", instance)
return instance
func load_level(level_path : String):
if is_loading : return
if is_instance_valid(current_level):
current_level.queue_free()
await current_level.tree_exited
current_level = null
if scene_loader_node:
is_loading = true
scene_loader_node.load_scene(level_path, true)
if level_loading_screen:
level_loading_screen.reset()
level_load_started.emit()
await scene_loader_node.scene_loaded
is_loading = false
current_level = _attach_level(scene_loader_node.get_resource())
if level_loading_screen:
level_loading_screen.close()
else:
var level_scene = load(level_path)
current_level = _attach_level(level_scene)
level_loaded.emit()
await current_level.ready
level_ready.emit()

View File

@@ -0,0 +1 @@
uid://bbymrin0cm704

View File

@@ -0,0 +1,183 @@
class_name LevelManager
extends Node
## Manage level changes in games.
##
## A helper script to assign to a node in a scene.
## It works with a level loader and can open menus when players win or lose.
## It can either be assigned a starting level path or a scene lister.
## It can detect signals from levels to change levels in an open-world.
## With a scene lister, it will instead traverse through levels linearly.
## Required reference to a level loader in the scene.
@export var level_loader : LevelLoader
## Optional path to a starting level scene.
## Required if there is no scene lister.
@export_file var starting_level_path : String
## Optional reference to a scene lister in the scene.
## Required if there is no starting level path.
@export var scene_lister : SceneLister
## Whether to load the starting level when ready.
@export var auto_load : bool = true
@export_group("Scenes")
## Path to a main menu scene.
@export_file("*.tscn") var main_menu_scene_path : String
## Optional path to an ending scene.
@export_file("*.tscn") var ending_scene_path : String
## Optional screen to be shown after the game is won.
@export var game_won_scene : PackedScene
## Optional screen to be shown after the level is lost.
@export var level_lost_scene : PackedScene
## Optional screen to be shown after the level is won.
@export var level_won_scene : PackedScene
## If Maaack's Scene Loader is installed, then it will be used to change scenes.
@onready var scene_loader_node = get_tree().root.get_node_or_null(^"SceneLoader")
## Reference to the current level node.
var current_level : Node
var current_level_path : String : set = set_current_level_path
var checkpoint_level_path : String : set = set_checkpoint_level_path
func set_current_level_path(value : String) -> void:
current_level_path = value
func set_checkpoint_level_path(value : String) -> void:
checkpoint_level_path = value
func _try_connecting_signal_to_node(node : Node, signal_name : String, callable : Callable) -> void:
if node.has_signal(signal_name) and not node.is_connected(signal_name, callable):
node.connect(signal_name, callable)
func _try_connecting_signal_to_level(signal_name : String, callable : Callable) -> void:
_try_connecting_signal_to_node(current_level, signal_name, callable)
func get_main_menu_scene_path() -> String:
return main_menu_scene_path
func _load_main_menu() -> void:
if scene_loader_node:
scene_loader_node.load_scene(get_main_menu_scene_path())
else:
get_tree().change_scene_to_file(get_main_menu_scene_path())
func _find_in_scene_lister(level_path : String) -> int:
if not scene_lister: return -1
level_path = ResourceUID.ensure_path(level_path)
return scene_lister.files.find(level_path)
func is_on_last_level() -> bool:
var current_level_id = _find_in_scene_lister(current_level_path)
return current_level_id > -1 and current_level_id == scene_lister.files.size() - 1
func get_relative_level_path(offset : int = 1) -> String:
var current_level_id := _find_in_scene_lister(current_level_path)
if current_level_id > -1:
if current_level_id >= max(0, -(offset)) and current_level_id < scene_lister.files.size() - max(0, offset):
current_level_id += offset
return scene_lister.files[current_level_id]
return ""
func get_next_level_path() -> String:
return get_relative_level_path(1)
func get_prev_level_path() -> String:
return get_relative_level_path(-1)
func get_ending_scene_path() -> String:
return ending_scene_path
func _load_ending() -> void:
if not get_ending_scene_path().is_empty():
if scene_loader_node:
scene_loader_node.load_scene(get_ending_scene_path())
else:
get_tree().change_scene_to_file(get_ending_scene_path())
else:
_load_main_menu()
func _on_level_lost() -> void:
if level_lost_scene:
var instance = level_lost_scene.instantiate()
get_tree().current_scene.add_child(instance)
_try_connecting_signal_to_node(instance, &"restart_pressed", _reload_level)
_try_connecting_signal_to_node(instance, &"main_menu_pressed", _load_main_menu)
else:
_reload_level()
func get_checkpoint_level_path() -> String:
if checkpoint_level_path.is_empty():
if scene_lister:
return scene_lister.files.front()
if not starting_level_path.is_empty():
return starting_level_path
return checkpoint_level_path
func load_level(level_path : String) -> void:
current_level_path = level_path
level_loader.load_level(level_path)
func _load_checkpoint_level() -> void:
load_level(get_checkpoint_level_path())
func _reload_level() -> void:
load_level(current_level_path)
func _load_win_screen_or_ending() -> void:
if game_won_scene:
var instance = game_won_scene.instantiate()
get_tree().current_scene.add_child(instance)
_try_connecting_signal_to_node(instance, &"continue_pressed", _load_ending)
_try_connecting_signal_to_node(instance, &"restart_pressed", _reload_level)
_try_connecting_signal_to_node(instance, &"main_menu_pressed", _load_main_menu)
else:
_load_ending()
func _load_level_won_screen_or_checkpoint() -> void:
if level_won_scene:
var instance = level_won_scene.instantiate()
get_tree().current_scene.add_child(instance)
_try_connecting_signal_to_node(instance, &"continue_pressed", _load_checkpoint_level)
_try_connecting_signal_to_node(instance, &"restart_pressed", _reload_level)
_try_connecting_signal_to_node(instance, &"main_menu_pressed", _load_main_menu)
else:
_load_checkpoint_level()
func _on_level_won(next_level_path : String = ""):
if next_level_path.is_empty():
next_level_path = get_next_level_path()
if next_level_path.is_empty():
_load_win_screen_or_ending()
else:
checkpoint_level_path = next_level_path
_load_level_won_screen_or_checkpoint()
func _on_level_changed(next_level_path : String):
checkpoint_level_path = next_level_path
_load_checkpoint_level()
func _connect_level_signals() -> void:
_try_connecting_signal_to_level(&"level_lost", _on_level_lost)
_try_connecting_signal_to_level(&"level_won", _on_level_won)
_try_connecting_signal_to_level(&"level_changed", _on_level_changed)
func _on_level_loader_level_loaded() -> void:
current_level = level_loader.current_level
await current_level.ready
_connect_level_signals()
func _on_level_loader_level_load_started() -> void:
pass
func _on_level_loader_level_ready() -> void:
pass
func _auto_load() -> void:
if auto_load:
_load_checkpoint_level()
func _ready() -> void:
if Engine.is_editor_hint(): return
level_loader.level_loaded.connect(_on_level_loader_level_loaded)
level_loader.level_ready.connect(_on_level_loader_level_ready)
level_loader.level_load_started.connect(_on_level_loader_level_load_started)
_auto_load()

View File

@@ -0,0 +1 @@
uid://bllg4gg7v1tsr

View File

@@ -0,0 +1,23 @@
@tool
extends Node
class_name SceneLister
## Helper class for listing all the scenes in a directory.
## List of paths to scene files.
## Prefilled in the editor by selecting a directory.
@export var files : Array[String]
## Prefill files with any scenes in the directory.
@export_dir var directory : String :
set(value):
directory = value
_refresh_files()
func _refresh_files():
if not is_inside_tree() or directory.is_empty(): return
var dir_access = DirAccess.open(directory)
if dir_access:
files.clear()
for file in dir_access.get_files():
if not file.ends_with(".tscn"):
continue
files.append(directory + "/" + file)

View File

@@ -0,0 +1 @@
uid://wjq7li836lwj

View File

@@ -0,0 +1,73 @@
extends Node
## Path to a main menu scene.
@export_file("*.tscn") var main_menu_scene_path : String
## Optional path to an ending scene.
@export_file("*.tscn") var ending_scene_path : String
## Optional screen to be shown after the game is won.
@export var game_won_scene : PackedScene
## Optional screen to be shown after the game is lost.
@export var game_lost_scene : PackedScene
## If Maaack's Scene Loader is installed, then it will be used to change scenes.
@onready var scene_loader_node = get_tree().root.get_node_or_null(^"SceneLoader")
var has_lost_game : bool = false
var has_won_game : bool = false
func _try_connecting_signal_to_node(node : Node, signal_name : String, callable : Callable) -> void:
if node.has_signal(signal_name) and not node.is_connected(signal_name, callable):
node.connect(signal_name, callable)
func get_main_menu_scene_path() -> String:
return main_menu_scene_path
func _load_main_menu() -> void:
if scene_loader_node:
scene_loader_node.load_scene(get_main_menu_scene_path())
else:
get_tree().change_scene_to_file(get_main_menu_scene_path())
func get_ending_scene_path() -> String:
return ending_scene_path
func _load_ending() -> void:
if not get_ending_scene_path().is_empty():
if scene_loader_node:
scene_loader_node.load_scene(get_ending_scene_path())
else:
get_tree().change_scene_to_file(get_ending_scene_path())
else:
_load_main_menu()
func _load_lose_screen_or_reload() -> void:
if game_lost_scene:
var instance = game_lost_scene.instantiate()
get_tree().current_scene.add_child(instance)
_try_connecting_signal_to_node(instance, &"restart_pressed", _reload_level)
_try_connecting_signal_to_node(instance, &"main_menu_pressed", _load_main_menu)
else:
_reload_level()
func _reload_level() -> void:
get_tree().reload_current_scene()
func _load_win_screen_or_ending() -> void:
if game_won_scene:
var instance = game_won_scene.instantiate()
get_tree().current_scene.add_child(instance)
_try_connecting_signal_to_node(instance, &"continue_pressed", _load_ending)
_try_connecting_signal_to_node(instance, &"restart_pressed", _reload_level)
_try_connecting_signal_to_node(instance, &"main_menu_pressed", _load_main_menu)
else:
_load_ending()
func game_lost() -> void:
if has_won_game or has_lost_game: return
has_lost_game = true
_load_lose_screen_or_reload()
func game_won() -> void:
if has_won_game or has_lost_game: return
has_won_game = true
_load_win_screen_or_ending()

View File

@@ -0,0 +1 @@
uid://bmlwpkrav3q56