Replace M* table-wrapper classes with typed Resources; add type hints

Two related cleanups from the best-practice audit:

Task 3 — typed Resources instead of MBase / MScene / MLevel / MSetting

The old model classes wrapped the godot_db_manager Table API: each row
read went through table.get_data_at_row_idx(int), Cell.get_data(),
and 'as int' / 'as String' casts that don't actually parse anything
in Godot 4. m_value, m_lock, m_label, ... members shadowed the cell
indirection. Setters round-tripped through table.edit_data() +
Global.database.save_db(). That's a lot of plumbing for what is, in
the end, three flat tables of static strings.

Introduce three @export-typed Resources:
  db/scene_entry.gd     class_name SceneEntry
  db/level_entry.gd     class_name LevelEntry
  db/settings_data.gd   class_name SettingsData

Rewrite scripts/Database.gd so Database.DB holds:
  settings: SettingsData
  levels:   Array[LevelEntry]
  scenes:   Array[SceneEntry]

Build them once at startup from ahog.json, and serialise back to the
same JSON shape on save() so existing progress files keep working.
LevelEntry carries its own object_to_find / object_finding / reset
methods (talking to Global.database for cross-table lookups), and
SceneEntry carries its own mesh_path / audio_sound. Per-scene
dissolve state (value, tick_reference, dissolved) lives on
SceneEntry as non-exported runtime fields.

Delete db/MBase.gd / db/MScene.gd / db/MLevel.gd / db/MSetting.gd.

Update consumers:
- scripts/Setting.gd: read/write Global.database.settings directly,
  call Global.database.save() after each setter.
- scenes/levels/Levels.gd: iterate Global.database.scenes_for_level(
  current_scene_int) instead of mscene.new(i) for every row; scene
  state reads (scene.lock, scene.mesh, scene.counter, ...) replace
  scene.lock() / scene.mesh() / scene.counter() method calls; runtime
  dissolve state lives on the SceneEntry instance instead of mutable
  m_value / m_tick_reference members on MScene; 'dissolved' flag
  replaces set_mesh(null) signalling.
- scenes/UI/choose_scenes/ChooseScene.gd: iterate Global.database
  .levels; level.name / level.thumb property access in place of
  level.name() / level.thumbnail(). configure_reset() loses its
  redundant index argument (LevelEntry knows its own index).
- scripts/event.gd: _on_reset_level signature now takes LevelEntry,
  reset path drops index forwarding.

Task 2 — type hints across the remaining scripts

scripts/Global.gd, scenes/Main.gd, scenes/UI/ending/Ending.gd,
scenes/UI/loading/Loading.gd, scenes/UI/settings/Settings.gd: add
typed parameters and -> return annotations. current_scene_int is now
'int = -1' (sentinel) so callers don't fall into Variant comparisons;
event.gd:_on_reset_level resets it to -1 instead of null.
Settings.gd no longer wraps button_pressed in int() before passing to
the now-typed bool setters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vaillant Jeremy
2026-05-16 21:58:11 +02:00
parent 60d9f614ee
commit c17769246f
24 changed files with 462 additions and 659 deletions
+132 -105
View File
@@ -1,128 +1,155 @@
extends Node
# Lightweight replacement for the godot_db_manager plugin (incompatible with
# Godot 4). Keeps the ahog.json format used by the rest of the project and
# exposes the same surface API the M* model classes expect:
# Loads ahog.json once into typed Resource arrays:
# Global.database.settings -> SettingsData
# Global.database.levels -> Array[LevelEntry]
# Global.database.scenes -> Array[SceneEntry]
#
# Global.database.get_table_by_name(name) -> Table
# Global.database.save_db()
# table.get_data_at_row_idx(row_id) -> [Cell, Cell, ...]
# table.edit_data(prop_id, row_id, value)
# table.m_rows_count
# table.get_data_by_prop_name_and_data(prop_name, value) -> Array
# table.get_dictionary_by_prop_name_and_data(prop_name, value) -> Array[Dict]
# cell.get_data() -> String
# save() round-trips back to the same JSON format so game progress
# (per-scene lock state) persists across runs.
class Cell:
var _value
func _init(value):
_value = value
func get_data():
return _value
class Table:
var name: String
var props: Array
var data: Array
var m_rows_count: int
func _init(table_dict: Dictionary):
name = table_dict["table_name"]
props = table_dict["props"]
data = table_dict["data"]
m_rows_count = data.size() / max(1, props.size())
func _col_count() -> int:
return props.size()
func _find_prop_idx(prop_name: String) -> int:
for i in range(props.size()):
if props[i]["name"] == prop_name:
return i
return -1
func get_data_at_row_idx(row_idx: int) -> Array:
var start = row_idx * _col_count()
var cells = []
for i in range(_col_count()):
cells.append(Cell.new(data[start + i]))
return cells
func edit_data(prop_id: int, row_id: int, value) -> void:
data[row_id * _col_count() + prop_id] = str(value)
func get_data_by_prop_name_and_data(prop_name: String, value) -> Array:
var prop_idx = _find_prop_idx(prop_name)
var matches = []
if prop_idx == -1:
return matches
for row in range(m_rows_count):
if data[row * _col_count() + prop_idx] == str(value):
matches.append(row)
return matches
func get_dictionary_by_prop_name_and_data(prop_name: String, value) -> Array:
var prop_idx = _find_prop_idx(prop_name)
var results = []
if prop_idx == -1:
return results
for row in range(m_rows_count):
if data[row * _col_count() + prop_idx] == str(value):
var d = {}
for i in range(_col_count()):
d[props[i]["name"]] = data[row * _col_count() + i]
results.append(d)
return results
func to_dict() -> Dictionary:
return {
"table_name": name,
"props": props,
"data": data,
}
class DB:
var version: String
var db_name: String
var tables: Dictionary = {}
class DB extends RefCounted:
var settings: SettingsData
var levels: Array[LevelEntry] = []
var scenes: Array[SceneEntry] = []
var _path: String
func _init(path: String):
const _SETTINGS_PROPS := [
{"name": "langue", "type": "1", "auto_increment": "0"},
{"name": "gyroscope", "type": "0", "auto_increment": "0"},
{"name": "ambient_sound", "type": "0", "auto_increment": "0"},
{"name": "resolution", "type": "3", "auto_increment": "0"},
{"name": "fullscreen", "type": "0", "auto_increment": "0"},
{"name": "version", "type": "0", "auto_increment": "0"},
]
const _LEVELS_PROPS := [
{"name": "name", "type": "3", "auto_increment": "0"},
{"name": "thumb", "type": "4", "auto_increment": "0"},
]
const _SCENES_PROPS := [
{"name": "lock", "type": "0", "auto_increment": "0"},
{"name": "label", "type": "3", "auto_increment": "0"},
{"name": "key", "type": "3", "auto_increment": "0"},
{"name": "level", "type": "table", "table_name": "levels", "auto_increment": "0"},
{"name": "mesh", "type": "3", "auto_increment": "0"},
{"name": "label_counter", "type": "1", "auto_increment": "0"},
{"name": "counter", "type": "1", "auto_increment": "0"},
]
func _init(path: String) -> void:
_path = path
var f = FileAccess.open(path, FileAccess.READ)
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
push_error("[Database] Cannot open " + path)
return
var raw = f.get_as_text()
var doc = JSON.parse_string(f.get_as_text())
f.close()
var doc = JSON.parse_string(raw)
if doc == null:
if not (doc is Dictionary):
push_error("[Database] Invalid JSON in " + path)
return
version = doc.get("GDDB_ver", "")
db_name = doc.get("db_name", "")
for t in doc.get("tables", []):
var table = Table.new(t)
tables[table.name] = table
for table in doc.get("tables", []):
match table.get("table_name", ""):
"settings": settings = _load_settings(table.get("data", []))
"levels": levels = _load_levels(table.get("data", []))
"scenes": scenes = _load_scenes(table.get("data", []))
func get_table_by_name(name: String):
return tables.get(name)
func _load_settings(data: Array) -> SettingsData:
var s := SettingsData.new()
if data.size() >= 6:
s.langue = int(data[0])
s.gyroscope = bool(int(data[1]))
s.ambient_sound = bool(int(data[2]))
s.resolution = str(data[3])
s.fullscreen = bool(int(data[4]))
s.version = str(data[5])
return s
func save_db() -> void:
var doc = {
"GDDB_ver": version,
"db_name": db_name,
"tables": [],
func _load_levels(data: Array) -> Array[LevelEntry]:
var result: Array[LevelEntry] = []
const W := 2
for i in range(data.size() / W):
var l := LevelEntry.new()
l.index = i
l.name = str(data[i * W + 0])
l.thumb = str(data[i * W + 1])
result.append(l)
return result
func _load_scenes(data: Array) -> Array[SceneEntry]:
var result: Array[SceneEntry] = []
const W := 7
for i in range(data.size() / W):
var s := SceneEntry.new()
s.lock = bool(int(data[i * W + 0]))
s.label = str(data[i * W + 1])
s.key = str(data[i * W + 2])
s.level = int(data[i * W + 3])
s.mesh = str(data[i * W + 4])
s.label_counter = str(data[i * W + 5])
s.counter = int(data[i * W + 6])
result.append(s)
return result
func scenes_for_level(level_idx: int) -> Array[SceneEntry]:
var result: Array[SceneEntry] = []
for s in scenes:
if s.level == level_idx:
result.append(s)
return result
func level_by_index(idx: int) -> LevelEntry:
for l in levels:
if l.index == idx:
return l
return null
func save() -> void:
var doc := {
"GDDB_ver": "2.0",
"db_name": "ahog",
"tables": [
{"table_name": "settings", "props": _SETTINGS_PROPS, "data": _dump_settings()},
{"table_name": "levels", "props": _LEVELS_PROPS, "data": _dump_levels()},
{"table_name": "scenes", "props": _SCENES_PROPS, "data": _dump_scenes()},
],
}
for tname in tables:
doc["tables"].append(tables[tname].to_dict())
var f = FileAccess.open(_path, FileAccess.WRITE)
var f := FileAccess.open(_path, FileAccess.WRITE)
if f == null:
push_error("[Database] Cannot write " + _path)
return
f.store_string(JSON.stringify(doc))
f.close()
func _dump_settings() -> Array:
return [
str(settings.langue),
str(int(settings.gyroscope)),
str(int(settings.ambient_sound)),
settings.resolution,
str(int(settings.fullscreen)),
settings.version,
]
func _dump_levels() -> Array:
var out: Array = []
for l in levels:
out.append_array([l.name, l.thumb])
return out
func _dump_scenes() -> Array:
var out: Array = []
for s in scenes:
out.append_array([
str(int(s.lock)),
s.label,
s.key,
str(s.level),
s.mesh,
s.label_counter,
str(s.counter),
])
return out
func initialize() -> DB:
return DB.new(save_database_in())
@@ -141,8 +168,8 @@ func normal_path() -> String:
func copy_database() -> void:
if FileAccess.file_exists(android_path()):
return
var src = FileAccess.open(normal_path(), FileAccess.READ)
var dst = FileAccess.open(android_path(), FileAccess.WRITE)
var src := FileAccess.open(normal_path(), FileAccess.READ)
var dst := FileAccess.open(android_path(), FileAccess.WRITE)
dst.store_string(src.get_as_text())
dst.close()
src.close()