This commit is contained in:
+5
-1
@@ -1 +1,5 @@
|
|||||||
files/
|
files/*
|
||||||
|
!files/config
|
||||||
|
!files/config/**
|
||||||
|
!files/custom
|
||||||
|
!files/custom/**
|
||||||
|
|||||||
@@ -58,6 +58,14 @@ COPY --chown=node:node docker/attribution/de.json /app/languages/de/attribution.
|
|||||||
COPY docker/patch-attribution.js /tmp/patch-attribution.js
|
COPY docker/patch-attribution.js /tmp/patch-attribution.js
|
||||||
RUN node /tmp/patch-attribution.js /app/htmls/contents/account_help.html
|
RUN node /tmp/patch-attribution.js /app/htmls/contents/account_help.html
|
||||||
|
|
||||||
|
# Local patch: custom game values and elements (docker/patch-custom-game.js).
|
||||||
|
# Hooks the loader into main.js and the game server, makes the Lone Wolf rates
|
||||||
|
# data-driven, and merges phrase overrides into the language catalogs. The
|
||||||
|
# loader itself is copied to /app/docker/ in the runtime stage; the custom file
|
||||||
|
# is mounted at runtime from ./files/custom.
|
||||||
|
COPY docker/patch-custom-game.js /tmp/patch-custom-game.js
|
||||||
|
RUN node /tmp/patch-custom-game.js /app
|
||||||
|
|
||||||
|
|
||||||
FROM ${NODE_IMAGE} AS runtime
|
FROM ${NODE_IMAGE} AS runtime
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,52 @@ https://adventure.land. Die Texte liegen in `docker/attribution/en.js` und
|
|||||||
Auch dieser Patch bricht den Build ab, wenn die Upstream-Vorlage nicht mehr
|
Auch dieser Patch bricht den Build ab, wenn die Upstream-Vorlage nicht mehr
|
||||||
passt.
|
passt.
|
||||||
|
|
||||||
|
`docker/patch-custom-game.js` hakt den Loader für `files/custom/game.js` in
|
||||||
|
Backend und Game-Server ein und macht die Lone-Wolf-Raten datengetrieben (siehe
|
||||||
|
„Spieldaten anpassen“). Fehlt ein Anker, bricht der Build ebenfalls ab.
|
||||||
|
|
||||||
|
## Spieldaten anpassen (`files/custom/game.js`)
|
||||||
|
|
||||||
|
Werte bestehender Elemente und neue Elemente lassen sich ohne Quelltextänderung
|
||||||
|
über `files/custom/game.js` setzen. Die Datei wird in beide Spielcontainer als
|
||||||
|
`/app/custom/game.js` gemountet und beim Start gelesen; Änderungen brauchen nur
|
||||||
|
einen Neustart, keinen Rebuild:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose restart al-backend al-game
|
||||||
|
```
|
||||||
|
|
||||||
|
Aufbau (alle Angaben optional):
|
||||||
|
|
||||||
|
- `items`, `monsters`, `npcs`, `maps`, `sets`, `craft`, `skills`, `conditions`,
|
||||||
|
`classes`, `levels`, `upgrades`, `compounds`, `multipliers`, `titles`,
|
||||||
|
`tokens`, `dismantle`: Objekte werden tief zusammengeführt, Arrays ersetzt.
|
||||||
|
Neue IDs sind erlaubt, wenn sie eine vorhandene Grafik aus
|
||||||
|
`design/dimensions.js` nutzen (Items und Monster brauchen ein `skin`).
|
||||||
|
Neue Karten gehen nicht: ihre Geometrie liegt in MongoDB (`MP_*`).
|
||||||
|
- `drops`: `{ main: [[0.001, "item_id"]] }` hängt Zeilen an die Tabelle an.
|
||||||
|
- `gold`: überschreibt `drops.gold`, z. B. `{ base: 0.8 }`.
|
||||||
|
- `phrases`: überschreibt Anzeigetexte. `"*"` gilt für alle Sprachen, ein
|
||||||
|
Sprachcode (`de`, `en`, ...) hat Vorrang. Nötig, wenn sich Zahlen in einem
|
||||||
|
Tooltip ändern.
|
||||||
|
- `apply(game)`: Funktion für Sonderfälle, läuft nach dem Zusammenführen.
|
||||||
|
|
||||||
|
Beim Start loggen beide Container `custom-game: loaded ...`; neue IDs stehen
|
||||||
|
als `(new: ...)` dabei. Unbekannte Top-Level-Schlüssel, unbekannte Drop-Tabellen,
|
||||||
|
fehlende Items und fehlende Grafiken brechen den Start mit einer klaren Meldung
|
||||||
|
ab, statt das Spiel halb gepatcht zu starten.
|
||||||
|
|
||||||
|
Enthaltenes Beispiel: Der Buff „Lone Wolf“ gibt 10× XP statt 3×. Die
|
||||||
|
Multiplikatoren liest der Game-Server aus der Condition-Definition (Patch in
|
||||||
|
`docker/patch-custom-game.js`), der Tooltip-Text kommt aus `phrases`. Die Datei
|
||||||
|
ist versioniert (`files/custom/`), `files/db` bleibt von Git ausgenommen.
|
||||||
|
|
||||||
|
Grenzen: Alles, was in `design/*.js` als Datum steht, ist überschreibbar.
|
||||||
|
Konstanten, die im Code stehen (z. B. andere Encouragement-Raten), brauchen
|
||||||
|
weiterhin einen Patch wie den für Lone Wolf. Endet ein Season-Event zur
|
||||||
|
Laufzeit, können eventmodifizierte Monsterfelder auf den Stand vor der
|
||||||
|
Custom-Datei zurückgesetzt werden; dann den Game-Server neu starten.
|
||||||
|
|
||||||
## Gut zu wissen
|
## Gut zu wissen
|
||||||
|
|
||||||
- **E-Mail-Verifizierung:** AWS SES ist nicht konfiguriert. Beim Registrieren
|
- **E-Mail-Verifizierung:** AWS SES ist nicht konfiguriert. Beim Registrieren
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ x-app: &app
|
|||||||
volumes:
|
volumes:
|
||||||
# Editable keys/options from the host (replaces the cloned template).
|
# Editable keys/options from the host (replaces the cloned template).
|
||||||
- ./files/config:/app/secretsandconfig:ro
|
- ./files/config:/app/secretsandconfig:ro
|
||||||
|
# Editable game values and new elements; see files/custom/game.js.
|
||||||
|
- ./files/custom:/app/custom:ro
|
||||||
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
"use strict";
|
||||||
|
// Local loader for values and new elements from ./custom/game.js.
|
||||||
|
//
|
||||||
|
// The file is mounted read-only at /app/custom and read whenever the backend or
|
||||||
|
// game server starts. It is not part of the image, so edits need only a restart:
|
||||||
|
//
|
||||||
|
// docker compose restart al-backend al-game
|
||||||
|
//
|
||||||
|
// The loader is deliberately fail-loud: a malformed custom file aborts startup
|
||||||
|
// instead of silently producing a half-patched game. Unknown top-level keys,
|
||||||
|
// unknown drop tables, references to missing items and new maps (their geometry
|
||||||
|
// lives in MongoDB) are errors. New items/monsters are allowed when they use a
|
||||||
|
// skin that already exists in design/dimensions.js.
|
||||||
|
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
const file = path.resolve(__dirname, "..", "custom", "game.js");
|
||||||
|
|
||||||
|
// Sections that are deep-merged into the design globals. Maps are the exception
|
||||||
|
// handled below: only existing maps can be changed.
|
||||||
|
const sections = [
|
||||||
|
"items",
|
||||||
|
"monsters",
|
||||||
|
"npcs",
|
||||||
|
"maps",
|
||||||
|
"sets",
|
||||||
|
"craft",
|
||||||
|
"skills",
|
||||||
|
"conditions",
|
||||||
|
"classes",
|
||||||
|
"levels",
|
||||||
|
"upgrades",
|
||||||
|
"compounds",
|
||||||
|
"multipliers",
|
||||||
|
"titles",
|
||||||
|
"tokens",
|
||||||
|
"dismantle",
|
||||||
|
];
|
||||||
|
|
||||||
|
const allowed = new Set([...sections, "drops", "gold", "phrases", "apply"]);
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
throw new Error("custom-game: " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_plain(value) {
|
||||||
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clone(value) {
|
||||||
|
return structuredClone(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
if (!fs.existsSync(file)) return null;
|
||||||
|
const resolved = require.resolve(file);
|
||||||
|
// Re-read on every call: a restart of the game server reloads the mounted file.
|
||||||
|
delete require.cache[resolved];
|
||||||
|
const custom = require(resolved);
|
||||||
|
if (!is_plain(custom)) fail(file + " must export an object");
|
||||||
|
return custom;
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge(target, source, where) {
|
||||||
|
for (const key of Object.keys(source)) {
|
||||||
|
const value = source[key];
|
||||||
|
if (value === undefined) continue;
|
||||||
|
if (is_plain(value)) {
|
||||||
|
if (!is_plain(target[key])) target[key] = {};
|
||||||
|
merge(target[key], value, where + "." + key);
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
target[key] = clone(value);
|
||||||
|
} else if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
|
||||||
|
fail(where + "." + key + " must be a plain value");
|
||||||
|
} else {
|
||||||
|
target[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A new element may only reuse an existing graphic, never introduce a new one.
|
||||||
|
// Monsters are drawn from design/dimensions.js (`dimensions`); items and
|
||||||
|
// condition icons are drawn from the sprite atlas (`positions`).
|
||||||
|
function check_graphics(section, id, entry, game) {
|
||||||
|
const dimensions = is_plain(game.dimensions) ? game.dimensions : {};
|
||||||
|
const positions = is_plain(game.positions) ? game.positions : {};
|
||||||
|
const exists = (section === "monsters" ? dimensions : positions);
|
||||||
|
if (section === "items") {
|
||||||
|
if (typeof entry.skin !== "string") fail("items." + id + ": new items need a skin (see design/dimensions.js)");
|
||||||
|
if (!exists[entry.skin]) fail("items." + id + ": skin \"" + entry.skin + "\" does not exist in design/dimensions.js");
|
||||||
|
}
|
||||||
|
if (section === "monsters") {
|
||||||
|
const skin = typeof entry.skin === "string" ? entry.skin : id;
|
||||||
|
if (!exists[skin]) fail("monsters." + id + ": skin \"" + skin + "\" does not exist in design/dimensions.js");
|
||||||
|
}
|
||||||
|
if (section === "conditions" && entry.skin !== undefined && !exists[entry.skin]) {
|
||||||
|
fail("conditions." + id + ": skin \"" + entry.skin + "\" does not exist in design/dimensions.js");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge_drops(game, drops) {
|
||||||
|
if (!is_plain(drops)) fail("drops must be an object of map -> rows");
|
||||||
|
for (const map of Object.keys(drops)) {
|
||||||
|
const rows = drops[map];
|
||||||
|
if (!Array.isArray(rows)) fail("drops." + map + " must be an array");
|
||||||
|
if (!Array.isArray(game.drops.maps[map])) fail("drops." + map + ": unknown drop table (see design/drops.js)");
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!Array.isArray(row) || row.length < 2) fail("drops." + map + ": each row is [chance, item]");
|
||||||
|
if (typeof row[0] !== "number" || row[0] < 0) fail("drops." + map + ": chance must be a non-negative number");
|
||||||
|
if (typeof row[1] !== "string") fail("drops." + map + ": item must be a string");
|
||||||
|
if (!is_plain(game.items[row[1]])) fail("drops." + map + ": unknown item \"" + row[1] + "\"");
|
||||||
|
game.drops.maps[map].push(clone(row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply(game) {
|
||||||
|
const custom = load();
|
||||||
|
if (!custom) return;
|
||||||
|
if (!is_plain(game)) fail("internal: game data is missing");
|
||||||
|
for (const key of Object.keys(custom)) {
|
||||||
|
if (!allowed.has(key)) fail("unknown top-level key \"" + key + "\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = [];
|
||||||
|
for (const section of sections) {
|
||||||
|
const entries = custom[section];
|
||||||
|
if (entries === undefined) continue;
|
||||||
|
if (!is_plain(entries)) fail(section + " must be an object of id -> values");
|
||||||
|
if (!is_plain(game[section])) fail("internal: " + section + " data is missing");
|
||||||
|
for (const id of Object.keys(entries)) {
|
||||||
|
const patch = entries[id];
|
||||||
|
if (!is_plain(patch)) fail(section + "." + id + " must be an object");
|
||||||
|
if (section === "maps" && !is_plain(game.maps[id])) {
|
||||||
|
fail("maps." + id + ": a new map needs geometry in MongoDB; only existing maps can be changed");
|
||||||
|
}
|
||||||
|
const is_new = !is_plain(game[section][id]);
|
||||||
|
if (is_new) {
|
||||||
|
game[section][id] = {};
|
||||||
|
created.push(section + "." + id);
|
||||||
|
}
|
||||||
|
merge(game[section][id], patch, section + "." + id);
|
||||||
|
if (is_new) check_graphics(section, id, game[section][id], game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (custom.drops !== undefined) merge_drops(game, custom.drops);
|
||||||
|
|
||||||
|
if (custom.gold !== undefined) {
|
||||||
|
if (!is_plain(game.drops.gold)) fail("internal: drops.gold data is missing");
|
||||||
|
if (!is_plain(custom.gold)) fail("gold must be an object");
|
||||||
|
merge(game.drops.gold, custom.gold, "gold");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (custom.apply !== undefined) {
|
||||||
|
if (typeof custom.apply !== "function") fail("apply must be a function");
|
||||||
|
custom.apply(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("custom-game: loaded " + file + (created.length ? " (new: " + created.join(", ") + ")" : ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate_phrases(where, map) {
|
||||||
|
if (map === undefined) return {};
|
||||||
|
if (!is_plain(map)) fail(where + " must be an object of phrase id -> text");
|
||||||
|
for (const id of Object.keys(map)) {
|
||||||
|
if (typeof map[id] !== "string") fail(where + "." + id + " must be a string");
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phrase overrides reach both the server-rendered pages and the browser bundle.
|
||||||
|
// "*" applies to every language; a language entry takes precedence. This keeps
|
||||||
|
// numbers in a tooltip correct in all languages unless translated per language.
|
||||||
|
function phrases(language) {
|
||||||
|
const custom = load();
|
||||||
|
if (!custom || custom.phrases === undefined) return {};
|
||||||
|
if (!is_plain(custom.phrases)) fail("phrases must be an object of language -> phrase map");
|
||||||
|
const all = validate_phrases("phrases.*", custom.phrases["*"]);
|
||||||
|
const own = validate_phrases("phrases." + language, custom.phrases[language]);
|
||||||
|
return Object.assign({}, all, own);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { apply: apply, phrases: phrases, file: file };
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Local patch: custom game values and elements from custom/game.js.
|
||||||
|
//
|
||||||
|
// The loader (docker/custom_game.js) itself needs no source changes, but the
|
||||||
|
// game data is built inside eval() scope, so three small hooks are inserted at
|
||||||
|
// image build time:
|
||||||
|
//
|
||||||
|
// 1. main.js and node/server.js call the loader after the seasonal flags are
|
||||||
|
// applied, so custom values win over the shipped design and over events.
|
||||||
|
// 2. node/logic/encouragement.js reads the Lone Wolf rates from the condition
|
||||||
|
// definition instead of hardcoded 3s, so custom/game.js can tune them.
|
||||||
|
// 3. languages/index.js merges custom phrase overrides into the catalogs, so
|
||||||
|
// tooltips such as the Lone Wolf explanation match tuned numbers.
|
||||||
|
//
|
||||||
|
// Every edit is idempotent and fails loudly if the upstream anchor is gone, so
|
||||||
|
// an upstream update cannot silently drop the hooks.
|
||||||
|
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
const app = path.resolve(process.argv[2] || "/app");
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
console.error("patch-custom-game: " + message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(file, marker, from, to, count) {
|
||||||
|
const full = path.join(app, file);
|
||||||
|
const source = fs.readFileSync(full, "utf8");
|
||||||
|
if (source.includes(marker)) {
|
||||||
|
console.log("patch-custom-game: " + file + " already patched");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const occurrences = source.split(from).length - 1;
|
||||||
|
if (occurrences !== count) {
|
||||||
|
fail(file + ": expected " + count + " occurrence(s) of the anchor, found " + occurrences);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(full, source.split(from).join(to));
|
||||||
|
console.log("patch-custom-game: patched " + file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1a. Backend: design globals are module-scoped in main.js.
|
||||||
|
edit(
|
||||||
|
"main.js",
|
||||||
|
'require("./docker/custom_game.js").apply',
|
||||||
|
`apply_event_flags({ maps: maps, items: items, monsters: monsters, drops: drops }, active_event_flags);
|
||||||
|
|
||||||
|
// docs`,
|
||||||
|
`apply_event_flags({ maps: maps, items: items, monsters: monsters, drops: drops }, active_event_flags);
|
||||||
|
|
||||||
|
// Local: values and new elements from the mounted custom/game.js.
|
||||||
|
require("./docker/custom_game.js").apply({
|
||||||
|
items: items, monsters: monsters, npcs: npcs, maps: maps, sets: sets, craft: craft,
|
||||||
|
skills: skills, conditions: conditions, classes: classes, levels: levels, upgrades: upgrades,
|
||||||
|
compounds: compounds, multipliers: multipliers, titles: titles, tokens: tokens, dismantle: dismantle,
|
||||||
|
drops: drops, dimensions: dimensions, positions: positions,
|
||||||
|
});
|
||||||
|
|
||||||
|
// docs`,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1b. Game server: the same globals are local to init_game() and reload_server().
|
||||||
|
edit(
|
||||||
|
"node/server.js",
|
||||||
|
'require("../docker/custom_game.js").apply',
|
||||||
|
`\t\tvar season_data = { maps: maps, items: items, monsters: monsters, drops: drops };
|
||||||
|
\t\tapply_event_flags(season_data, active_event_flags);
|
||||||
|
\t\tseason_controls = { restore: season_data.event_restore || {}, end: end_event_flag };`,
|
||||||
|
`\t\tvar season_data = { maps: maps, items: items, monsters: monsters, drops: drops };
|
||||||
|
\t\tapply_event_flags(season_data, active_event_flags);
|
||||||
|
\t\t// Local: values and new elements from the mounted custom/game.js.
|
||||||
|
\t\trequire("../docker/custom_game.js").apply({
|
||||||
|
\t\t\titems: items, monsters: monsters, npcs: npcs, maps: maps, sets: sets, craft: craft,
|
||||||
|
\t\t\tskills: skills, conditions: conditions, classes: classes, levels: levels, upgrades: upgrades,
|
||||||
|
\t\t\tcompounds: compounds, multipliers: multipliers, titles: titles, tokens: tokens, dismantle: dismantle,
|
||||||
|
\t\t\tdrops: drops, dimensions: dimensions, positions: positions,
|
||||||
|
\t\t});
|
||||||
|
\t\tseason_controls = { restore: season_data.event_restore || {}, end: end_event_flag };`,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Lone Wolf rates become data-driven; the defaults keep upstream behaviour.
|
||||||
|
edit(
|
||||||
|
"node/logic/encouragement.js",
|
||||||
|
"lonewolf_definition",
|
||||||
|
`\t\t} else if (!blocked && name === "encouragement_lonewolf" && lonewolf) {
|
||||||
|
\t\t\tcondition = { gold_multiplier: 3, xp_multiplier: 3, luck_multiplier: 3 };`,
|
||||||
|
`\t\t} else if (!blocked && name === "encouragement_lonewolf" && lonewolf) {
|
||||||
|
\t\t\t// Local: read the rates from design/conditions.js so custom/game.js can tune them.
|
||||||
|
\t\t\tvar lonewolf_definition = (G.conditions && G.conditions[name]) || {};
|
||||||
|
\t\t\tcondition = {
|
||||||
|
\t\t\t\tgold_multiplier: lonewolf_definition.gold_multiplier === undefined ? 3 : lonewolf_definition.gold_multiplier,
|
||||||
|
\t\t\t\txp_multiplier: lonewolf_definition.xp_multiplier === undefined ? 3 : lonewolf_definition.xp_multiplier,
|
||||||
|
\t\t\t\tluck_multiplier: lonewolf_definition.luck_multiplier === undefined ? 3 : lonewolf_definition.luck_multiplier,
|
||||||
|
\t\t\t};`,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3a. Phrase lookup helper, next to the catalog loader.
|
||||||
|
edit(
|
||||||
|
"languages/index.js",
|
||||||
|
"function custom_phrases(language)",
|
||||||
|
`function create_catalog_loader(directory, domains) {`,
|
||||||
|
`// Local: phrase overrides from the mounted custom/game.js.
|
||||||
|
function custom_phrases(language) {
|
||||||
|
try {
|
||||||
|
return require("../docker/custom_game.js").phrases(language);
|
||||||
|
} catch (e) {
|
||||||
|
if (e && e.code === "MODULE_NOT_FOUND") return {};
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_catalog_loader(directory, domains) {`,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3b. Apply the overrides after each language's files have been read.
|
||||||
|
edit(
|
||||||
|
"languages/index.js",
|
||||||
|
"var custom = custom_phrases(language);",
|
||||||
|
`\t\t}
|
||||||
|
\t\treturn result;
|
||||||
|
\t}
|
||||||
|
\treturn function catalog(language) {`,
|
||||||
|
`\t\t}
|
||||||
|
\t\t// Local: custom names and explanations win over the shipped text.
|
||||||
|
\t\tvar custom = custom_phrases(language);
|
||||||
|
\t\tfor (var custom_id of Object.keys(custom)) result[custom_id] = custom[custom_id];
|
||||||
|
\t\treturn result;
|
||||||
|
\t}
|
||||||
|
\treturn function catalog(language) {`,
|
||||||
|
1,
|
||||||
|
);
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// Eigene Spieldaten für diese Instanz.
|
||||||
|
//
|
||||||
|
// Die Datei liegt auf dem Host unter files/custom/game.js und wird in die
|
||||||
|
// Container als /app/custom/game.js gemountet. Sie ist kein Teil des Images:
|
||||||
|
// Änderungen brauchen nur einen Neustart, keinen Rebuild.
|
||||||
|
//
|
||||||
|
// docker compose restart al-backend al-game
|
||||||
|
//
|
||||||
|
// Regeln:
|
||||||
|
// - Objekte werden tief zusammengeführt; Arrays ersetzen den bisherigen Wert.
|
||||||
|
// - Neue IDs sind erlaubt (Items, Monster, NPCs, Conditions, ...), solange
|
||||||
|
// sie eine vorhandene Grafik aus design/dimensions.js verwenden.
|
||||||
|
// - Neue Karten sind nicht möglich: ihre Geometrie liegt in MongoDB.
|
||||||
|
// - `drops` wird an die vorhandene Tabelle angehängt.
|
||||||
|
// - `phrases` überschreibt Anzeigetexte. "*" gilt für alle Sprachen,
|
||||||
|
// ein Sprachcode (de, en, ...) hat Vorrang. Das ist nötig, wenn sich
|
||||||
|
// Zahlen in einem Tooltip ändern.
|
||||||
|
// - `apply(game)` ist ein Notausgang für alles, was sich nicht als Daten
|
||||||
|
// ausdrücken lässt; `game` enthält die Design-Objekte und `drops`.
|
||||||
|
//
|
||||||
|
// Aktuelles Beispiel: "Lone Wolf" gibt 10× XP statt 3×.
|
||||||
|
// Der Game-Server liest die Multiplikatoren aus der Condition-Definition
|
||||||
|
// (siehe docker/patch-custom-game.js); der Tooltip-Text kommt aus `phrases`.
|
||||||
|
module.exports = {
|
||||||
|
conditions: {
|
||||||
|
encouragement_lonewolf: {
|
||||||
|
gold_multiplier: 3,
|
||||||
|
xp_multiplier: 10,
|
||||||
|
luck_multiplier: 3,
|
||||||
|
// Fallback für Stellen, die die Rohdaten ohne Phrase anzeigen.
|
||||||
|
explanation:
|
||||||
|
"3× Gold, 10× XP and 3× Luck while you run one non-merchant character. Merchants may stay online. Extra rewards follow your contribution.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
phrases: {
|
||||||
|
"*": {
|
||||||
|
"condition.encouragement_lonewolf.explanation":
|
||||||
|
"3× Gold, 10× XP and 3× Luck while you run one non-merchant character. Merchants may stay online. Extra rewards follow your contribution.",
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
"condition.encouragement_lonewolf.explanation":
|
||||||
|
"3× Gold, 10× XP und 3× Glück, solange du nur einen Nicht-Händler-Charakter spielst. Händler dürfen online bleiben. Zusätzliche Belohnungen richten sich nach deinem Beitrag.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Weitere Beispiele – einkommentieren und anpassen:
|
||||||
|
//
|
||||||
|
// items: {
|
||||||
|
// "training_sword": {
|
||||||
|
// type: "weapon",
|
||||||
|
// wtype: "short_sword",
|
||||||
|
// skin: "sword", // muss in design/dimensions.js existieren
|
||||||
|
// name: "Training Sword",
|
||||||
|
// g: 10,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// monsters: {
|
||||||
|
// "training_goo": {
|
||||||
|
// skin: "goo",
|
||||||
|
// name: "Training Goo",
|
||||||
|
// speed: 6, charge: 12, hp: 100, xp: 100, attack: 5,
|
||||||
|
// damage_type: "physical", respawn: 1, gold: 20, range: 15,
|
||||||
|
// frequency: 0.4, aggro: 0, aa: 1,
|
||||||
|
// // Zusätzlich muss das Monster in einer Karte auftauchen, z. B.:
|
||||||
|
// // maps: { main: { monsters: [{ type: "training_goo", position: [0, 0], radius: 100, count: 1 }] } },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// drops: {
|
||||||
|
// main: [[0.001, "training_sword"]], // [Chance, Item-ID], wird angehängt
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// gold: {
|
||||||
|
// base: 0.8, // Standard 0.64
|
||||||
|
// },
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user