Add web-based save editor for "Reksio i Czarodzieje"
- Implemented a local web server using http.server to serve the save editor. - Created a user interface with HTML, CSS, and JavaScript for managing game slots and editing save files. - Added API endpoints for retrieving slot information, overview, and raw data for editing. - Implemented functionality for saving changes back to the game files. - Added tests for ARR and DTA formats to ensure data integrity during round-trip serialization. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
"""Schematy: nazwy kolumn .DTA i znaczenie indeksów w .ARR.
|
||||
|
||||
Kolumny .DTA nie są zapisane w pliku — pochodzą z definicji STRUCT w Arcade.cnv
|
||||
(linie 1455-1489). Znaczenia pól ARR z MAINMENU.class (DEFSETS, SAVEGAME).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
INT = "INTEGER"
|
||||
STR = "STRING"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Column:
|
||||
name: str
|
||||
type: str
|
||||
note: str = ""
|
||||
|
||||
|
||||
#: SOBJECT — model bazy obiektów sceny (<SCENA><slot>.DTA).
|
||||
SOBJECT: tuple[Column, ...] = (
|
||||
Column("NAME", STR, "ścieżka do .ANN/.IMG/.SEK względem katalogu sceny"),
|
||||
Column("IDNAME", STR, "identyfikator logiczny, używany przez skrypty sceny"),
|
||||
Column("TYPE", INT, "rola wiersza — patrz OBJECT_TYPES"),
|
||||
Column("SPARAM0", STR),
|
||||
Column("SPARAM1", STR),
|
||||
Column("SPARAM2", STR),
|
||||
Column("IPARAM0", INT, "dla TYPE==2: 1 = widoczny, 0 = zabrany"),
|
||||
Column("IPARAM1", INT),
|
||||
Column("IPARAM2", INT),
|
||||
)
|
||||
|
||||
#: SITEM — model ekwipunku (ITEMS<slot>.DTA).
|
||||
SITEM: tuple[Column, ...] = (
|
||||
Column("GUID", INT, "indeks slotu 0..9, równy numerowi wiersza"),
|
||||
Column("NAME", STR, "identyfikator przedmiotu, NULL gdy pusto"),
|
||||
Column("PARENT", INT, "indeks wiersza w .DTA lokacji, z której podniesiono (-1 = brak)"),
|
||||
Column("BASE", STR, "lokacja pochodzenia"),
|
||||
Column("EMPTY", INT, "MYLĄCA NAZWA: 1 = slot ZAJĘTY, 0 = wolny"),
|
||||
)
|
||||
|
||||
DTA_SCHEMAS: dict[str, tuple[Column, ...]] = {
|
||||
"scene": SOBJECT,
|
||||
"items": SITEM,
|
||||
}
|
||||
|
||||
#: Kolumna TYPE w SOBJECT — dekoder z Arcade.cnv:__LOAD_DB__.
|
||||
OBJECT_TYPES: dict[int, str] = {
|
||||
101: "tło statyczne (.IMG)",
|
||||
102: "maska ścieżek chodzenia (.SEK)",
|
||||
103: "aktywacja przejścia: WPATH^SETACTIVE(IPARAM0, IPARAM1, IPARAM2)",
|
||||
104: "Reksio; IPARAM0 = prędkość maks.",
|
||||
105: "punkt startowy, zestaw 0; IDNAME = scena skąd, IPARAM0/1 = X/Y",
|
||||
106: "punkt docelowy, zestaw 0; IPARAM0/1 = X/Y",
|
||||
107: "tło animowane; IPARAM0/1 = szerokość/wysokość",
|
||||
108: "muzyka lokacji; IDNAME = plik .WAV",
|
||||
140: "Kret",
|
||||
145: "punkt startowy, zestaw 1",
|
||||
146: "punkt docelowy, zestaw 1",
|
||||
}
|
||||
|
||||
|
||||
def describe_object_type(value: int) -> str:
|
||||
if value in OBJECT_TYPES:
|
||||
return OBJECT_TYPES[value]
|
||||
if value < 100:
|
||||
extra = " — IPARAM0 to flaga widoczności" if value == 2 else ""
|
||||
return f"obiekt sceny (ANNOBJECT), podtyp {value}{extra}"
|
||||
return f"nieznany typ {value}"
|
||||
|
||||
|
||||
#: GAME<slot>.ARR — 6 elementów, wszystkie zapisane jako STRING.
|
||||
GAMESETS: tuple[Column, ...] = (
|
||||
Column("G_SARCADEOBJECTS", STR, "bieżąca lokacja"),
|
||||
Column("G_SLASTOBJECTS", STR, "poprzednia lokacja — wyznacza punkt wejścia"),
|
||||
Column("G_SDIALOGCHARACTER", STR, "rozmówca"),
|
||||
Column("G_SDIALOGRETURN", STR, "dokąd wrócić po rozmowie"),
|
||||
Column("G_ITKTPM", INT, "losowe 0..7, ustalane raz przy nowej grze"),
|
||||
Column("G_IKRETACTIVE", INT, "czy Kret towarzyszy Reksiowi (0/1)"),
|
||||
)
|
||||
|
||||
#: Stan fabryczny GAME0.ARR z DEFSETS (indeks 4 jest losowany 0..7).
|
||||
GAMESETS_DEFAULT: list[str] = ["INTRO_1", "NULL", "BUREKTOR", "NULL", "0", "0"]
|
||||
|
||||
#: SPELLS<slot>.ARR — 17 intów. Stan fabryczny z NEWGAME.
|
||||
SPELLS_DEFAULT: list[int] = [0] * 12 + [64] + [0] * 4
|
||||
|
||||
#: Próg doświadczenia na jeden poziom (MAINMENU.class: LEVELUP, BFITMP57/59/61).
|
||||
EXP_PER_LEVEL = 150
|
||||
|
||||
#: Ile klatek animacji klepsydry — klatka = exp // 10 + 1 (BFITMP55).
|
||||
EXP_PER_HOURGLASS_FRAME = 10
|
||||
|
||||
#: Indeksy 0..11 to flagi „czar znany" (0/1). Sześć czarów w wersji zwykłej (0-5)
|
||||
#: i obronnej (6-11, sufiks _OBR); czar N ma obronę N+6. Nazwy z uchwytów klikania
|
||||
#: kul w gabinetach — Guldryk.cnv, Gabinetsnejka.cnv, Wmwnetrze.cnv.
|
||||
SPELL_NAMES: tuple[str, ...] = (
|
||||
"Sen", "Zamiana w żabę", "Plaga much", "Kula budyniu", "Koła i wiry", "Ciemność",
|
||||
"Obrona przed Snem", "Obrona przed Zamianą w żabę", "Obrona przed Plagą much", "Obrona przed Kulą budyniu",
|
||||
"Obrona przed Kołami i wirami", "Obrona przed Ciemnością",
|
||||
)
|
||||
|
||||
#: Indeksy 12..16 — liczniki i flagi stanu klepsydry.
|
||||
SPELLS_NOTES: dict[int, str] = {
|
||||
12: "startuje na 64, SUBAT(12,3) przy każdym awansie — nikt tego nie czyta",
|
||||
13: f"DOŚWIADCZENIE, 0..{EXP_PER_LEVEL - 1}; po przekroczeniu progu reszta przechodzi dalej",
|
||||
14: "licznik awansów (ile razy klepsydra się przesypała)",
|
||||
15: "1 → klepsydra/różdżka widoczna w menu (SHOWEXP, M_WAND)",
|
||||
16: "1 → klepsydra miga, można nauczyć się czaru",
|
||||
}
|
||||
|
||||
#: Indeks doświadczenia w SPELLS<slot>.ARR.
|
||||
EXP_INDEX = 13
|
||||
LEVEL_INDEX = 14
|
||||
HOURGLASS_VISIBLE_INDEX = 15
|
||||
HOURGLASS_BLINKING_INDEX = 16
|
||||
|
||||
|
||||
def describe_spell_index(index: int) -> str:
|
||||
if index < len(SPELL_NAMES):
|
||||
return f"{SPELL_NAMES[index]} (1 = znany)"
|
||||
return SPELLS_NOTES.get(index, "nieznane")
|
||||
Reference in New Issue
Block a user