afnm-types 0.6.49 → 0.6.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CharacterCraftingEncounter.d.ts +7 -0
- package/dist/CharacterCraftingEncounter.js +1 -0
- package/dist/avatarEffects.d.ts +23 -0
- package/dist/avatarEffects.js +98 -0
- package/dist/buff.d.ts +15 -1
- package/dist/cli/extract-mod-translations/index.js +367 -0
- package/dist/cli/extract-translations/config.js +632 -0
- package/dist/cli/extract-translations/extractors.js +1283 -0
- package/dist/cli/extract-translations/registries.js +40 -0
- package/dist/cli/extract-translations/registry-builders.js +661 -0
- package/dist/cli/extract-translations/reporters.js +1369 -0
- package/dist/cli/extract-translations/resolvers.js +180 -0
- package/dist/cli/extract-translations/template-processor.js +380 -0
- package/dist/cli/extract-translations/types.js +5 -0
- package/dist/craftingTechnique.d.ts +1 -0
- package/dist/event.d.ts +6 -2
- package/dist/event.js +1 -0
- package/dist/gameVersion.d.ts +1 -1
- package/dist/gameVersion.js +1 -1
- package/dist/herbField.d.ts +0 -1
- package/dist/mod.d.ts +73 -1
- package/dist/reduxState.d.ts +10 -0
- package/dist/stat.d.ts +1 -1
- package/dist/stat.js +4 -0
- package/package.json +11 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Avatar visual effect registry.
|
|
3
|
+
*
|
|
4
|
+
* Each effect maps to a WebGL fragment shader that replaces the default
|
|
5
|
+
* crossfade shader while the effect is active. Mods can extend
|
|
6
|
+
* AVATAR_EFFECT_REGISTRY with additional string keys before combat starts.
|
|
7
|
+
*
|
|
8
|
+
* Built-in effect IDs are defined in AvatarEffectId.
|
|
9
|
+
*/
|
|
10
|
+
export type AvatarEffectId = 'glitch';
|
|
11
|
+
export interface AvatarEffectShader {
|
|
12
|
+
/**
|
|
13
|
+
* Fragment shader source. Must accept the same base uniforms as the default
|
|
14
|
+
* fragment shader (u_texA, u_texB, u_mix, u_aspectA, u_aspectB,
|
|
15
|
+
* u_canvasAspect) plus u_time (float, seconds since the effect started).
|
|
16
|
+
*/
|
|
17
|
+
fragSrc: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Central record from effect ID → shader definition.
|
|
21
|
+
* Mods can add entries with arbitrary string keys before combat starts.
|
|
22
|
+
*/
|
|
23
|
+
export declare const AVATAR_EFFECT_REGISTRY: Record<string, AvatarEffectShader>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Avatar visual effect registry.
|
|
3
|
+
*
|
|
4
|
+
* Each effect maps to a WebGL fragment shader that replaces the default
|
|
5
|
+
* crossfade shader while the effect is active. Mods can extend
|
|
6
|
+
* AVATAR_EFFECT_REGISTRY with additional string keys before combat starts.
|
|
7
|
+
*
|
|
8
|
+
* Built-in effect IDs are defined in AvatarEffectId.
|
|
9
|
+
*/
|
|
10
|
+
// ─── Glitch shader ────────────────────────────────────────────────────────────
|
|
11
|
+
// Replicates the hand-authored glitch art style: strong persistent chromatic
|
|
12
|
+
// aberration (cyan/magenta fringing always visible), periodic brightness
|
|
13
|
+
// washout, and horizontal scanline tears during burst events.
|
|
14
|
+
const GLITCH_FRAG_SRC = `
|
|
15
|
+
precision mediump float;
|
|
16
|
+
varying vec2 v_uv;
|
|
17
|
+
uniform sampler2D u_texA;
|
|
18
|
+
uniform sampler2D u_texB;
|
|
19
|
+
uniform float u_mix;
|
|
20
|
+
uniform float u_aspectA;
|
|
21
|
+
uniform float u_aspectB;
|
|
22
|
+
uniform float u_canvasAspect;
|
|
23
|
+
uniform float u_time;
|
|
24
|
+
|
|
25
|
+
float hash(float n) {
|
|
26
|
+
return fract(sin(n) * 43758.5453);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
vec4 sampleContain(sampler2D tex, vec2 uv, float texAspect) {
|
|
30
|
+
float rel = texAspect / u_canvasAspect;
|
|
31
|
+
vec2 scale;
|
|
32
|
+
if (rel > 1.0) {
|
|
33
|
+
scale = vec2(1.0, 1.0 / rel);
|
|
34
|
+
} else {
|
|
35
|
+
scale = vec2(rel, 1.0);
|
|
36
|
+
}
|
|
37
|
+
vec2 offset = (1.0 - scale) * 0.5;
|
|
38
|
+
vec2 mapped = (uv - offset) / scale;
|
|
39
|
+
if (mapped.x < 0.0 || mapped.x > 1.0 || mapped.y < 0.0 || mapped.y > 1.0) {
|
|
40
|
+
return vec4(0.0);
|
|
41
|
+
}
|
|
42
|
+
return texture2D(tex, mapped);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
void main() {
|
|
46
|
+
vec2 uv = v_uv;
|
|
47
|
+
|
|
48
|
+
// Glitch burst timing — ~0.8 event slots per second, 35% chance each.
|
|
49
|
+
float eventT = floor(u_time * 0.8);
|
|
50
|
+
float isGlitching = step(0.65, hash(eventT * 1.618));
|
|
51
|
+
// Sharp onset, slow decay — the burst lingers before fading.
|
|
52
|
+
float slotFrac = fract(u_time * 0.8);
|
|
53
|
+
float decay = (1.0 - smoothstep(0.1, 0.9, slotFrac)) * isGlitching;
|
|
54
|
+
|
|
55
|
+
// Band-based horizontal displacement (scanline tearing) during bursts.
|
|
56
|
+
float band = floor(uv.y * 24.0);
|
|
57
|
+
float bandActive = step(0.55, hash(band + eventT * 23.7));
|
|
58
|
+
float bandShift = (hash(band * 5.2 + eventT * 7.1) * 2.0 - 1.0) * 0.025;
|
|
59
|
+
float shift = bandShift * bandActive * decay;
|
|
60
|
+
|
|
61
|
+
// Occasional large block tears.
|
|
62
|
+
float bigTear = step(0.92, hash(band * 3.7 + eventT * 11.3));
|
|
63
|
+
shift += (hash(band * 9.1 + eventT * 4.7) * 2.0 - 1.0) * 0.08 * bigTear * decay;
|
|
64
|
+
|
|
65
|
+
vec2 shiftedUv = vec2(uv.x + shift, uv.y);
|
|
66
|
+
|
|
67
|
+
// Chromatic aberration: strong baseline always present, amplified by bursts.
|
|
68
|
+
float aberr = 0.012 + 0.018 * decay;
|
|
69
|
+
|
|
70
|
+
float rA = sampleContain(u_texA, vec2(shiftedUv.x + aberr, shiftedUv.y), u_aspectA).r;
|
|
71
|
+
float gA = sampleContain(u_texA, shiftedUv, u_aspectA).g;
|
|
72
|
+
float bA = sampleContain(u_texA, vec2(shiftedUv.x - aberr, shiftedUv.y), u_aspectA).b;
|
|
73
|
+
float aA = sampleContain(u_texA, shiftedUv, u_aspectA).a;
|
|
74
|
+
vec4 colA = vec4(rA, gA, bA, aA);
|
|
75
|
+
|
|
76
|
+
float rB = sampleContain(u_texB, vec2(shiftedUv.x + aberr, shiftedUv.y), u_aspectB).r;
|
|
77
|
+
float gB = sampleContain(u_texB, shiftedUv, u_aspectB).g;
|
|
78
|
+
float bB = sampleContain(u_texB, vec2(shiftedUv.x - aberr, shiftedUv.y), u_aspectB).b;
|
|
79
|
+
float aB = sampleContain(u_texB, shiftedUv, u_aspectB).a;
|
|
80
|
+
vec4 colB = vec4(rB, gB, bB, aB);
|
|
81
|
+
|
|
82
|
+
vec4 col = mix(colA, colB, u_mix);
|
|
83
|
+
|
|
84
|
+
// Brightness washout during bursts — push toward overexposed white,
|
|
85
|
+
// scaled by alpha so transparent edges don't bloom.
|
|
86
|
+
float blowout = decay * 0.45;
|
|
87
|
+
col.rgb = mix(col.rgb, vec3(1.0), blowout * col.a);
|
|
88
|
+
|
|
89
|
+
gl_FragColor = col;
|
|
90
|
+
}`;
|
|
91
|
+
// ─── Registry ─────────────────────────────────────────────────────────────────
|
|
92
|
+
/**
|
|
93
|
+
* Central record from effect ID → shader definition.
|
|
94
|
+
* Mods can add entries with arbitrary string keys before combat starts.
|
|
95
|
+
*/
|
|
96
|
+
export const AVATAR_EFFECT_REGISTRY = {
|
|
97
|
+
glitch: { fragSrc: GLITCH_FRAG_SRC },
|
|
98
|
+
};
|
package/dist/buff.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { TechniqueElement } from './element';
|
|
|
3
3
|
import type { CombatStatistic, Scaling } from './stat';
|
|
4
4
|
import type { DamageType } from './DamageType';
|
|
5
5
|
import type { CombatEntity } from './entity';
|
|
6
|
+
import { AvatarEffectId } from './avatarEffects';
|
|
6
7
|
export type TechniqueCondition = ChanceTechniqueCondition | BuffTechniqueCondition | HpTechniqueCondition | ConditionTechniqueCondition | InventoryItemTechniqueCondition;
|
|
7
8
|
interface BaseTechniqueCondition {
|
|
8
9
|
/**
|
|
@@ -154,7 +155,7 @@ export interface Buff {
|
|
|
154
155
|
/** Runtime guardian max HP — set automatically from guardianIntercept.maxHp at buff creation */
|
|
155
156
|
guardianMaxHp?: number;
|
|
156
157
|
}
|
|
157
|
-
type BuffCombatImage = ScatterCombatImage | ArcCombatImage | FloatingCombatImage | OverlayCombatImage | CompanionCombatImage | GuardianCombatImage | GroundCombatImage | FormationCombatImage | ForegroundCombatImage | BackgroundCombatImage | TransformationCombatImage;
|
|
158
|
+
type BuffCombatImage = ScatterCombatImage | ArcCombatImage | FloatingCombatImage | OverlayCombatImage | CompanionCombatImage | GuardianCombatImage | GroundCombatImage | FormationCombatImage | ForegroundCombatImage | BackgroundCombatImage | TransformationCombatImage | AvatarEffectCombatImage;
|
|
158
159
|
interface BaseCombatImage {
|
|
159
160
|
image: string;
|
|
160
161
|
imageOverrides?: {
|
|
@@ -165,6 +166,19 @@ interface BaseCombatImage {
|
|
|
165
166
|
animations?: ('buff' | 'bump' | 'attack' | 'debuff')[];
|
|
166
167
|
animateOnEntity?: boolean;
|
|
167
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Avatar effect combat image — applies a visual shader effect to the character
|
|
171
|
+
* avatar sprite while this buff is active. Only the last active avatarEffect
|
|
172
|
+
* buff applies (same priority rule as transformation).
|
|
173
|
+
*/
|
|
174
|
+
export interface AvatarEffectCombatImage {
|
|
175
|
+
position: 'avatarEffect';
|
|
176
|
+
/** The shader effect to apply. Use AvatarEffectId for built-ins, or a
|
|
177
|
+
* custom string key registered in AVATAR_EFFECT_REGISTRY for mod effects. */
|
|
178
|
+
effect: AvatarEffectId | string;
|
|
179
|
+
animations?: ('buff' | 'bump' | 'attack' | 'debuff')[];
|
|
180
|
+
animateOnEntity?: boolean;
|
|
181
|
+
}
|
|
168
182
|
export interface ScatterCombatImage extends BaseCombatImage {
|
|
169
183
|
position: 'scatter';
|
|
170
184
|
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Mod Translation String Extraction Script
|
|
5
|
+
*
|
|
6
|
+
* Extracts translatable strings from a mod's TypeScript source code and generates
|
|
7
|
+
* a template.json file that translators can fill in to provide translations.
|
|
8
|
+
*
|
|
9
|
+
* Translators fill in template.json (or a copy of it named <lang>.json), then the
|
|
10
|
+
* mod registers those translations at runtime via:
|
|
11
|
+
* import ruTranslations from './translations/ru.json';
|
|
12
|
+
* api.addTranslation('ru', ruTranslations);
|
|
13
|
+
*
|
|
14
|
+
* Usage: WHEN RUNNING THIS, USE THE NPM COMMAND. DO NOT TRY TO RUN DIRECTLY
|
|
15
|
+
* npm run extract-mod-translations -- --src <path> [--output <path>]
|
|
16
|
+
*
|
|
17
|
+
* Options:
|
|
18
|
+
* --src <path> Path to the mod source directory (default: ./src)
|
|
19
|
+
* --output <path> Path to the translations output directory (default: ./translations)
|
|
20
|
+
*/
|
|
21
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
22
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
23
|
+
};
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
const fs_1 = __importDefault(require("fs"));
|
|
26
|
+
const path_1 = __importDefault(require("path"));
|
|
27
|
+
const glob_1 = require("glob");
|
|
28
|
+
const registries_js_1 = require("../extract-translations/registries.js");
|
|
29
|
+
const registry_builders_js_1 = require("../extract-translations/registry-builders.js");
|
|
30
|
+
const extractors_js_1 = require("../extract-translations/extractors.js");
|
|
31
|
+
const reporters_js_1 = require("../extract-translations/reporters.js");
|
|
32
|
+
// ─── CLI argument parsing ───────────────────────────────────────────────────
|
|
33
|
+
function parseArgs() {
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
let srcDir = './src';
|
|
36
|
+
let outputDir = './translations';
|
|
37
|
+
for (let i = 0; i < args.length; i++) {
|
|
38
|
+
if (args[i] === '--src' && args[i + 1]) {
|
|
39
|
+
srcDir = args[++i];
|
|
40
|
+
}
|
|
41
|
+
else if (args[i] === '--output' && args[i + 1]) {
|
|
42
|
+
outputDir = args[++i];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { srcDir, outputDir };
|
|
46
|
+
}
|
|
47
|
+
// ─── Translation migration helpers ─────────────────────────────────────────
|
|
48
|
+
/**
|
|
49
|
+
* Build a lookup structure from an existing language file so translations
|
|
50
|
+
* can be migrated to the new template structure.
|
|
51
|
+
*/
|
|
52
|
+
function buildTranslationLookup(lang) {
|
|
53
|
+
const flat = new Map();
|
|
54
|
+
const flatWithField = new Map();
|
|
55
|
+
const nested = new Map();
|
|
56
|
+
for (const [category, objects] of Object.entries(lang)) {
|
|
57
|
+
const categoryMap = new Map();
|
|
58
|
+
nested.set(category, categoryMap);
|
|
59
|
+
for (const [objectId, fields] of Object.entries(objects)) {
|
|
60
|
+
const objectMap = new Map();
|
|
61
|
+
categoryMap.set(objectId, objectMap);
|
|
62
|
+
for (const [key, translation] of Object.entries(fields)) {
|
|
63
|
+
if (typeof translation !== 'string' || !translation.trim())
|
|
64
|
+
continue;
|
|
65
|
+
// Key format: "[field] English text" or "[field:context] English text"
|
|
66
|
+
const match = key.match(/^\[([^\]]+)\]\s*(.+)$/s);
|
|
67
|
+
if (!match)
|
|
68
|
+
continue;
|
|
69
|
+
const field = match[1];
|
|
70
|
+
const englishText = match[2].trim();
|
|
71
|
+
objectMap.set(englishText, translation);
|
|
72
|
+
flat.set(englishText, translation);
|
|
73
|
+
flatWithField.set(`${field}|${englishText}`, translation);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { flat, flatWithField, nested };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Migrate an existing language file to the new template structure.
|
|
81
|
+
*/
|
|
82
|
+
function migrateTranslationsToTemplate(template, existingLang) {
|
|
83
|
+
const lookup = buildTranslationLookup(existingLang);
|
|
84
|
+
const migrated = {};
|
|
85
|
+
let matched = 0;
|
|
86
|
+
let unmatched = 0;
|
|
87
|
+
for (const [category, objects] of Object.entries(template)) {
|
|
88
|
+
migrated[category] = {};
|
|
89
|
+
for (const [objectId, fields] of Object.entries(objects)) {
|
|
90
|
+
migrated[category][objectId] = {};
|
|
91
|
+
for (const key of Object.keys(fields)) {
|
|
92
|
+
const match = key.match(/^\[([^\]]+)\]\s*(.+)$/s);
|
|
93
|
+
if (!match) {
|
|
94
|
+
migrated[category][objectId][key] = '';
|
|
95
|
+
unmatched++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const field = match[1];
|
|
99
|
+
const englishText = match[2].trim();
|
|
100
|
+
let translation;
|
|
101
|
+
// Strategy 1: Exact match at same category/objectId
|
|
102
|
+
translation = lookup.nested.get(category)?.get(objectId)?.get(englishText);
|
|
103
|
+
// Strategy 1.5: Context migration — look for same key without context
|
|
104
|
+
if (!translation && field.includes(':')) {
|
|
105
|
+
const baseField = field.split(':')[0];
|
|
106
|
+
const existingObject = existingLang[category]?.[objectId];
|
|
107
|
+
if (existingObject) {
|
|
108
|
+
for (const [oldKey, oldTranslation] of Object.entries(existingObject)) {
|
|
109
|
+
if (typeof oldTranslation !== 'string' || !oldTranslation.trim())
|
|
110
|
+
continue;
|
|
111
|
+
const oldMatch = oldKey.match(/^\[([^\]]+)\]\s*(.+)$/s);
|
|
112
|
+
if (!oldMatch)
|
|
113
|
+
continue;
|
|
114
|
+
if (oldMatch[2].trim() === englishText && oldMatch[1] === baseField) {
|
|
115
|
+
translation = oldTranslation;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Strategy 2: Match by field + English text across all locations
|
|
122
|
+
if (!translation) {
|
|
123
|
+
translation = lookup.flatWithField.get(`${field}|${englishText}`);
|
|
124
|
+
}
|
|
125
|
+
// Strategy 3: Match by base field (ignore context) + English text
|
|
126
|
+
if (!translation && field.includes(':')) {
|
|
127
|
+
const baseField = field.split(':')[0];
|
|
128
|
+
translation = lookup.flatWithField.get(`${baseField}|${englishText}`);
|
|
129
|
+
}
|
|
130
|
+
// Strategy 4: Fallback — match English text only
|
|
131
|
+
if (!translation) {
|
|
132
|
+
translation = lookup.flat.get(englishText);
|
|
133
|
+
}
|
|
134
|
+
if (translation) {
|
|
135
|
+
migrated[category][objectId][key] = translation;
|
|
136
|
+
matched++;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
migrated[category][objectId][key] = '';
|
|
140
|
+
unmatched++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { migrated, stats: { matched, unmatched } };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Deep sort an object's keys alphabetically (recursive) for consistent output.
|
|
149
|
+
*/
|
|
150
|
+
function deepSortObject(obj) {
|
|
151
|
+
if (typeof obj !== 'object' || Array.isArray(obj)) {
|
|
152
|
+
return obj;
|
|
153
|
+
}
|
|
154
|
+
const result = {};
|
|
155
|
+
for (const key of Object.keys(obj).sort((a, b) => a.localeCompare(b))) {
|
|
156
|
+
const value = obj[key];
|
|
157
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
158
|
+
result[key] = deepSortObject(value);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
result[key] = value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Load existing language files from the output directory.
|
|
168
|
+
* These will be migrated to the new template structure.
|
|
169
|
+
*/
|
|
170
|
+
function loadExistingModLanguageFiles(outputDir) {
|
|
171
|
+
const languageFiles = new Map();
|
|
172
|
+
if (!fs_1.default.existsSync(outputDir)) {
|
|
173
|
+
return languageFiles;
|
|
174
|
+
}
|
|
175
|
+
const entries = fs_1.default.readdirSync(outputDir);
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
if (!entry.endsWith('.json') || entry === 'template.json')
|
|
178
|
+
continue;
|
|
179
|
+
const lang = entry.replace('.json', '');
|
|
180
|
+
const langPath = path_1.default.join(outputDir, entry);
|
|
181
|
+
try {
|
|
182
|
+
const data = JSON.parse(fs_1.default.readFileSync(langPath, 'utf-8'));
|
|
183
|
+
languageFiles.set(lang, data);
|
|
184
|
+
console.log(` Loaded existing translations from ${entry}`);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
188
|
+
console.warn(` Warning: Failed to parse ${entry}: ${message}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return languageFiles;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Count total translatable entries in a hierarchical template.
|
|
195
|
+
*/
|
|
196
|
+
function countHierarchicalStrings(template) {
|
|
197
|
+
let count = 0;
|
|
198
|
+
for (const category of Object.values(template)) {
|
|
199
|
+
for (const objectStrings of Object.values(category)) {
|
|
200
|
+
count += Object.keys(objectStrings).length;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return count;
|
|
204
|
+
}
|
|
205
|
+
// ─── Main extraction function ───────────────────────────────────────────────
|
|
206
|
+
async function main() {
|
|
207
|
+
const { srcDir, outputDir } = parseArgs();
|
|
208
|
+
const resolvedSrc = path_1.default.resolve(srcDir);
|
|
209
|
+
const resolvedOutput = path_1.default.resolve(outputDir);
|
|
210
|
+
console.log('Extracting translatable strings from mod...\n');
|
|
211
|
+
console.log(` Source directory : ${resolvedSrc}`);
|
|
212
|
+
console.log(` Output directory : ${resolvedOutput}\n`);
|
|
213
|
+
if (!fs_1.default.existsSync(resolvedSrc)) {
|
|
214
|
+
console.error(`Error: Source directory does not exist: ${resolvedSrc}`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
// Clear all registries so this run is isolated (important when running multiple times)
|
|
218
|
+
(0, registries_js_1.clearRegistries)();
|
|
219
|
+
// Find all TypeScript/TSX files in the mod source
|
|
220
|
+
const files = await (0, glob_1.glob)('**/*.{ts,tsx}', {
|
|
221
|
+
cwd: resolvedSrc,
|
|
222
|
+
ignore: ['**/*.test.ts', '**/*.test.tsx', '**/*.d.ts'],
|
|
223
|
+
absolute: true,
|
|
224
|
+
});
|
|
225
|
+
if (files.length === 0) {
|
|
226
|
+
console.warn('Warning: No TypeScript files found in the source directory.');
|
|
227
|
+
console.warn(` Searched in: ${resolvedSrc}`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(`Found ${files.length} source file(s) to scan.\n`);
|
|
231
|
+
}
|
|
232
|
+
// PASS 1: Build string export registry
|
|
233
|
+
console.log('Pass 1: Building string export registry...');
|
|
234
|
+
for (const file of files) {
|
|
235
|
+
try {
|
|
236
|
+
(0, registry_builders_js_1.buildStringExportRegistry)(file);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
240
|
+
console.warn(` Warning: Failed to parse ${file}: ${message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// PASS 2: Build import registry
|
|
244
|
+
console.log('Pass 2: Building import registry...');
|
|
245
|
+
for (const file of files) {
|
|
246
|
+
try {
|
|
247
|
+
(0, registry_builders_js_1.buildImportRegistry)(file);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
251
|
+
console.warn(` Warning: Failed to parse ${file}: ${message}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// PASS 3: Build name export registry
|
|
255
|
+
console.log('Pass 3: Building name export registry...');
|
|
256
|
+
for (const file of files) {
|
|
257
|
+
try {
|
|
258
|
+
(0, registry_builders_js_1.buildNameExportRegistry)(file);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
262
|
+
console.warn(` Warning: Failed to parse ${file}: ${message}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// PASS 4: Build realm map registry
|
|
266
|
+
console.log('Pass 4: Building realm map registry...');
|
|
267
|
+
for (const file of files) {
|
|
268
|
+
try {
|
|
269
|
+
(0, registry_builders_js_1.buildRealmMapRegistry)(file);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
273
|
+
console.warn(` Warning: Failed to parse ${file}: ${message}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
(0, registry_builders_js_1.computeRealmMapNames)();
|
|
277
|
+
// PASS 5: Extract strings
|
|
278
|
+
console.log('Pass 5: Extracting strings...');
|
|
279
|
+
const allStrings = [];
|
|
280
|
+
for (const file of files) {
|
|
281
|
+
try {
|
|
282
|
+
const extracted = (0, extractors_js_1.extractFromFile)(file);
|
|
283
|
+
allStrings.push(...extracted);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
287
|
+
console.error(` Error processing ${file}: ${message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// PASS 5.5: Extract from xxxToName maps defined in the mod source
|
|
291
|
+
console.log('Pass 5.5: Extracting from xxxToName maps...');
|
|
292
|
+
try {
|
|
293
|
+
const toNameStrings = await (0, extractors_js_1.extractFromToNameMaps)(resolvedSrc);
|
|
294
|
+
allStrings.push(...toNameStrings);
|
|
295
|
+
console.log(` Found ${toNameStrings.length} strings in xxxToName maps.`);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
299
|
+
console.error(` Error extracting from xxxToName maps: ${message}`);
|
|
300
|
+
}
|
|
301
|
+
// PASS 5.6: Add realm-indexed map names
|
|
302
|
+
let realmMapNameCount = 0;
|
|
303
|
+
for (const [mapKey, realmNames] of registries_js_1.realmMapResolvedNames) {
|
|
304
|
+
const [filePath] = mapKey.split(':');
|
|
305
|
+
for (const [, name] of realmNames) {
|
|
306
|
+
if (name && name.trim().length > 0) {
|
|
307
|
+
allStrings.push({
|
|
308
|
+
text: name,
|
|
309
|
+
file: filePath,
|
|
310
|
+
line: 0,
|
|
311
|
+
context: 'data-name-static',
|
|
312
|
+
objectType: 'item',
|
|
313
|
+
});
|
|
314
|
+
realmMapNameCount++;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (realmMapNameCount > 0) {
|
|
319
|
+
console.log(` Found ${realmMapNameCount} realm-indexed map names.`);
|
|
320
|
+
}
|
|
321
|
+
// Collect simple and static strings for template generation
|
|
322
|
+
const templateStrings = allStrings.filter((s) => s.context !== 'INVALID_TEMPLATE_LITERAL' &&
|
|
323
|
+
!s.context.startsWith('NEEDS_MANUAL_REVIEW') &&
|
|
324
|
+
!s.context.includes('-dynamic'));
|
|
325
|
+
console.log(`\nTotal extracted strings: ${allStrings.length}`);
|
|
326
|
+
console.log(`Template-ready strings : ${templateStrings.length}`);
|
|
327
|
+
// Generate hierarchical template
|
|
328
|
+
console.log('\nGenerating translation template...');
|
|
329
|
+
const template = (0, reporters_js_1.generateHierarchicalTemplate)(templateStrings);
|
|
330
|
+
const sortedTemplate = deepSortObject(template);
|
|
331
|
+
const stringCount = countHierarchicalStrings(sortedTemplate);
|
|
332
|
+
console.log(` ${stringCount} unique translatable entries found.`);
|
|
333
|
+
// Ensure output directory exists
|
|
334
|
+
fs_1.default.mkdirSync(resolvedOutput, { recursive: true });
|
|
335
|
+
// Load existing language files for migration
|
|
336
|
+
console.log('\nLoading existing language files...');
|
|
337
|
+
const existingLanguages = loadExistingModLanguageFiles(resolvedOutput);
|
|
338
|
+
// Write template.json
|
|
339
|
+
const templatePath = path_1.default.join(resolvedOutput, 'template.json');
|
|
340
|
+
fs_1.default.writeFileSync(templatePath, JSON.stringify(sortedTemplate, null, 2), 'utf-8');
|
|
341
|
+
console.log(`\nWrote template: ${templatePath}`);
|
|
342
|
+
// Migrate existing language files
|
|
343
|
+
if (existingLanguages.size > 0) {
|
|
344
|
+
console.log('\nMigrating existing translations to new template structure...');
|
|
345
|
+
for (const [lang, existingData] of existingLanguages) {
|
|
346
|
+
const { migrated, stats } = migrateTranslationsToTemplate(sortedTemplate, existingData);
|
|
347
|
+
const sortedMigrated = deepSortObject(migrated);
|
|
348
|
+
const langPath = path_1.default.join(resolvedOutput, `${lang}.json`);
|
|
349
|
+
fs_1.default.writeFileSync(langPath, JSON.stringify(sortedMigrated, null, 2), 'utf-8');
|
|
350
|
+
const total = stats.matched + stats.unmatched;
|
|
351
|
+
const pct = total > 0 ? Math.round((stats.matched / total) * 100) : 0;
|
|
352
|
+
console.log(` ${lang}.json: ${stats.matched}/${total} strings preserved (${pct}%)`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
console.log('\n✓ Extraction complete!\n');
|
|
356
|
+
console.log('Next steps:');
|
|
357
|
+
console.log(` 1. Open ${templatePath}`);
|
|
358
|
+
console.log(' 2. Copy it to a new language file, e.g. translations/ru.json');
|
|
359
|
+
console.log(' 3. Fill in the translated values for each key');
|
|
360
|
+
console.log(' 4. Register the translations in your mod:');
|
|
361
|
+
console.log(" import ruTranslations from './translations/ru.json';");
|
|
362
|
+
console.log(" api.addTranslation('ru', ruTranslations);");
|
|
363
|
+
}
|
|
364
|
+
main().catch((error) => {
|
|
365
|
+
console.error('Fatal error:', error);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
});
|