lpc-forge 1.0.0
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/CREDITS.csv +22985 -0
- package/LICENSE +674 -0
- package/README.md +281 -0
- package/assets/music/.gitkeep +0 -0
- package/dist/character/batch.d.ts +17 -0
- package/dist/character/batch.js +48 -0
- package/dist/character/batch.js.map +1 -0
- package/dist/character/composer.d.ts +3 -0
- package/dist/character/composer.js +164 -0
- package/dist/character/composer.js.map +1 -0
- package/dist/character/definitions.d.ts +16 -0
- package/dist/character/definitions.js +116 -0
- package/dist/character/definitions.js.map +1 -0
- package/dist/character/presets.d.ts +6 -0
- package/dist/character/presets.js +246 -0
- package/dist/character/presets.js.map +1 -0
- package/dist/character/slicer.d.ts +8 -0
- package/dist/character/slicer.js +66 -0
- package/dist/character/slicer.js.map +1 -0
- package/dist/character/types.d.ts +48 -0
- package/dist/character/types.js +32 -0
- package/dist/character/types.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +938 -0
- package/dist/export/frames.d.ts +5 -0
- package/dist/export/frames.js +15 -0
- package/dist/export/frames.js.map +1 -0
- package/dist/export/godot.d.ts +17 -0
- package/dist/export/godot.js +464 -0
- package/dist/export/godot.js.map +1 -0
- package/dist/export/types.d.ts +11 -0
- package/dist/export/types.js +2 -0
- package/dist/export/types.js.map +1 -0
- package/dist/license.d.ts +49 -0
- package/dist/license.js +271 -0
- package/dist/map/cellular.d.ts +3 -0
- package/dist/map/cellular.js +191 -0
- package/dist/map/cellular.js.map +1 -0
- package/dist/map/dungeon.d.ts +3 -0
- package/dist/map/dungeon.js +238 -0
- package/dist/map/dungeon.js.map +1 -0
- package/dist/map/multifloor.d.ts +20 -0
- package/dist/map/multifloor.js +57 -0
- package/dist/map/multifloor.js.map +1 -0
- package/dist/map/overworld.d.ts +3 -0
- package/dist/map/overworld.js +205 -0
- package/dist/map/overworld.js.map +1 -0
- package/dist/map/town.d.ts +7 -0
- package/dist/map/town.js +181 -0
- package/dist/map/town.js.map +1 -0
- package/dist/map/types.d.ts +65 -0
- package/dist/map/types.js +16 -0
- package/dist/map/types.js.map +1 -0
- package/dist/map/wfc.d.ts +18 -0
- package/dist/map/wfc.js +192 -0
- package/dist/map/wfc.js.map +1 -0
- package/dist/tileset/atlas.d.ts +15 -0
- package/dist/tileset/atlas.js +55 -0
- package/dist/tileset/atlas.js.map +1 -0
- package/dist/tileset/registry.d.ts +12 -0
- package/dist/tileset/registry.js +71 -0
- package/dist/tileset/registry.js.map +1 -0
- package/dist/tileset/terrain.d.ts +3 -0
- package/dist/tileset/terrain.js +110 -0
- package/dist/tileset/terrain.js.map +1 -0
- package/dist/utils/credits.d.ts +11 -0
- package/dist/utils/credits.js +74 -0
- package/dist/utils/credits.js.map +1 -0
- package/dist/utils/image.d.ts +17 -0
- package/dist/utils/image.js +94 -0
- package/dist/utils/image.js.map +1 -0
- package/dist/utils/rng.d.ts +18 -0
- package/dist/utils/rng.js +48 -0
- package/dist/utils/rng.js.map +1 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# ⚔️ LPC Forge
|
|
4
|
+
|
|
5
|
+
**Generate a complete 2D RPG from one command.**
|
|
6
|
+
|
|
7
|
+
Characters · Maps · Enemy AI · Inventory · Dialog · Menus · SFX · Lighting · Particles · Godot 4.6
|
|
8
|
+
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](https://nodejs.org)
|
|
11
|
+
[](https://godotengine.org)
|
|
12
|
+
[](https://github.com/LaunchDay-Studio-Inc/lpc-forge/actions)
|
|
13
|
+
[](https://discord.gg/bJDGXc4DvW)
|
|
14
|
+
|
|
15
|
+
[Website](https://blueth.online) · [Get Premium](https://launchday.gumroad.com/l/lpc-forge-premium) · [Discord](https://discord.gg/bJDGXc4DvW) · [Report Bug](https://github.com/LaunchDay-Studio-Inc/lpc-forge/issues)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What is LPC Forge?
|
|
22
|
+
|
|
23
|
+
LPC Forge generates **playable 2D RPG projects** for Godot 4.6 using [Liberated Pixel Cup](https://lpc.opengameart.org/) assets. Not just sprites — complete game projects with working systems you can customize.
|
|
24
|
+
|
|
25
|
+
**Free tier** gives you characters, maps, and a basic playable project.
|
|
26
|
+
**Premium** ($10) gives you the full RPG game kit — inventory, dialog, enemy AI, menus, save/load, sound effects, lighting, particles, and more. All from one command.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Free — character + map + basic project
|
|
30
|
+
lpc-forge init my-rpg
|
|
31
|
+
|
|
32
|
+
# Premium — complete playable RPG
|
|
33
|
+
lpc-forge init my-rpg --full
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Open in Godot → Press F5 → **Play.**
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Clone and install
|
|
44
|
+
git clone https://github.com/LaunchDay-Studio-Inc/lpc-forge.git
|
|
45
|
+
cd lpc-forge
|
|
46
|
+
npm install && npm run build
|
|
47
|
+
|
|
48
|
+
# Generate a free project
|
|
49
|
+
npx lpc-forge init my-rpg
|
|
50
|
+
|
|
51
|
+
# Open in Godot 4.6 → Import → Select my-rpg/project.godot → Press F5
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Free vs Premium
|
|
57
|
+
|
|
58
|
+
| Feature | Free | Premium ($10) |
|
|
59
|
+
|---------|:----:|:-------------:|
|
|
60
|
+
| **Character Compositor** (17 presets, custom specs) | ✅ | ✅ |
|
|
61
|
+
| **Map Generation** (dungeon, cave, overworld, town, WFC, multifloor) | ✅ | ✅ |
|
|
62
|
+
| **Godot Project Scaffold** (player controller, hitbox, camera, HUD) | ✅ | ✅ |
|
|
63
|
+
| **Batch Character Generation** | ✅ | ✅ |
|
|
64
|
+
| **Enemy AI** (patrol, chase, attack, flee, boss patterns) | — | ✅ |
|
|
65
|
+
| **Inventory System** (grid UI, equip, stack, drag-drop, tooltips) | — | ✅ |
|
|
66
|
+
| **Dialog System** (text box, choices, portraits, typing effect) | — | ✅ |
|
|
67
|
+
| **Menu System** (main, pause, settings, game over, credits) | — | ✅ |
|
|
68
|
+
| **Save/Load** (JSON, 3 slots, auto-save) | — | ✅ |
|
|
69
|
+
| **Scene Transitions** (doors, stairs, fade, area loading) | — | ✅ |
|
|
70
|
+
| **Loot & Drop System** (drop tables, item pickup, XP/gold drops) | — | ✅ |
|
|
71
|
+
| **Quest Tracker** (objectives, markers, completion) | — | ✅ |
|
|
72
|
+
| **Day/Night Cycle** (time-of-day modulation, lamp auto-on) | — | ✅ |
|
|
73
|
+
| **Full HUD** (HP, MP, XP, gold, minimap, hotbar, buffs) | — | ✅ |
|
|
74
|
+
| **Sound Effects** (45 procedural SFX: combat, UI, movement, magic) | — | ✅ |
|
|
75
|
+
| **Music Catalog** (curated CC0 BGM tracks for every scene type) | — | ✅ |
|
|
76
|
+
| **UI Kit** (panels, buttons, frames, tooltips, medieval theme) | — | ✅ |
|
|
77
|
+
| **Item Icons** (swords, potions, scrolls, armor, food, keys) | — | ✅ |
|
|
78
|
+
| **Props** (chests, barrels, torches, signs, wells, fences) | — | ✅ |
|
|
79
|
+
| **Character Portraits** (auto-cropped, 3 sizes) | — | ✅ |
|
|
80
|
+
| **Lighting Presets** (8 presets: dungeon, overworld, cave, boss arena) | — | ✅ |
|
|
81
|
+
| **Particle Effects** (8 effects: rain, snow, fireflies, fire, magic) | — | ✅ |
|
|
82
|
+
| **Enemy Characters** (skeleton, guard, thief — full spritesheets) | — | ✅ |
|
|
83
|
+
| **NPC Characters** (merchant, healer, peasant — full spritesheets) | — | ✅ |
|
|
84
|
+
| **Autoload Wiring** (systems auto-registered in project.godot) | — | ✅ |
|
|
85
|
+
| **Input Actions** (inventory, interact, pause, quest log pre-configured) | — | ✅ |
|
|
86
|
+
|
|
87
|
+
### Get Premium
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# 1. Purchase at https://launchday.gumroad.com/l/lpc-forge-premium
|
|
91
|
+
# 2. Activate your license key
|
|
92
|
+
lpc-forge activate <your-license-key>
|
|
93
|
+
|
|
94
|
+
# 3. Generate a complete RPG
|
|
95
|
+
lpc-forge init my-rpg --full
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
$10. One-time purchase. Unlimited projects. No subscription.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Commands
|
|
103
|
+
|
|
104
|
+
### Free Commands
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Character compositor
|
|
108
|
+
lpc-forge character --preset paladin -o ./output
|
|
109
|
+
lpc-forge character --body female --hair plain:blonde --armor plate:gold
|
|
110
|
+
lpc-forge character --list-layers
|
|
111
|
+
|
|
112
|
+
# Batch generation
|
|
113
|
+
lpc-forge batch --presets warrior,mage,rogue -o ./output
|
|
114
|
+
|
|
115
|
+
# Map generation
|
|
116
|
+
lpc-forge map --type dungeon --width 50 --height 50 -o ./output
|
|
117
|
+
lpc-forge map --type overworld --seed "my-world" -o ./output
|
|
118
|
+
|
|
119
|
+
# List all presets
|
|
120
|
+
lpc-forge list
|
|
121
|
+
|
|
122
|
+
# Project scaffold (free tier)
|
|
123
|
+
lpc-forge init my-rpg --character warrior --map dungeon
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Premium Commands
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Full RPG project (all systems, all assets)
|
|
130
|
+
lpc-forge init my-rpg --full
|
|
131
|
+
|
|
132
|
+
# Individual premium generators
|
|
133
|
+
lpc-forge systems --list # Preview (free)
|
|
134
|
+
lpc-forge systems -o ./output # Generate (premium)
|
|
135
|
+
lpc-forge sfx --list # Preview (free)
|
|
136
|
+
lpc-forge sfx -o ./output # Generate (premium)
|
|
137
|
+
lpc-forge ui --list-themes # Preview (free)
|
|
138
|
+
lpc-forge ui -o ./output # Generate (premium)
|
|
139
|
+
lpc-forge lighting --list # Preview (free)
|
|
140
|
+
lpc-forge lighting -o ./output # Generate (premium)
|
|
141
|
+
lpc-forge particles --list # Preview (free)
|
|
142
|
+
lpc-forge particles -o ./output # Generate (premium)
|
|
143
|
+
lpc-forge icons -o ./output # Generate (premium)
|
|
144
|
+
lpc-forge props -o ./output # Generate (premium)
|
|
145
|
+
lpc-forge portrait --character warrior -o ./output # Generate (premium)
|
|
146
|
+
|
|
147
|
+
# License management
|
|
148
|
+
lpc-forge activate <key> # Activate license
|
|
149
|
+
lpc-forge activate --status # Check license status
|
|
150
|
+
lpc-forge activate --deactivate # Remove license
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## What `init --full` Generates
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
my-rpg/
|
|
159
|
+
├── project.godot # Pre-configured with autoloads + input actions
|
|
160
|
+
├── sprites/
|
|
161
|
+
│ ├── warrior/ # Player character (8-dir, all animations)
|
|
162
|
+
│ ├── skeleton/ # Enemy character
|
|
163
|
+
│ ├── guard/ # Enemy character
|
|
164
|
+
│ ├── thief/ # Enemy character
|
|
165
|
+
│ ├── npc_merchant/ # NPC character
|
|
166
|
+
│ ├── npc_healer/ # NPC character
|
|
167
|
+
│ └── npc_peasant/ # NPC character
|
|
168
|
+
├── scripts/
|
|
169
|
+
│ ├── player.gd # State machine player controller
|
|
170
|
+
│ ├── enemy_ai.gd # Patrol/chase/attack FSM
|
|
171
|
+
│ ├── inventory_manager.gd # Grid inventory system (autoload)
|
|
172
|
+
│ ├── inventory_ui.gd # Drag-drop inventory UI
|
|
173
|
+
│ ├── dialog_manager.gd # Dialog system (autoload)
|
|
174
|
+
│ ├── dialog_box.gd # Text box with typing effect
|
|
175
|
+
│ ├── save_manager.gd # Save/load system (autoload)
|
|
176
|
+
│ ├── scene_manager.gd # Scene transitions (autoload)
|
|
177
|
+
│ ├── loot_manager.gd # Drop tables and item pickup (autoload)
|
|
178
|
+
│ ├── quest_manager.gd # Quest tracker (autoload)
|
|
179
|
+
│ ├── day_night.gd # Day/night cycle (autoload)
|
|
180
|
+
│ ├── menu_manager.gd # Menu system (autoload)
|
|
181
|
+
│ ├── hud.gd # Full HUD (autoload)
|
|
182
|
+
│ └── game_config.gd # Global game constants
|
|
183
|
+
├── dungeon.tscn # Generated dungeon map
|
|
184
|
+
├── tileset/ # Terrain tiles
|
|
185
|
+
├── ui/ # UI kit (panels, buttons, frames)
|
|
186
|
+
├── icons/ # Item icon sprites
|
|
187
|
+
├── props/ # Prop sprites
|
|
188
|
+
├── portraits/ # Character portraits (3 sizes)
|
|
189
|
+
├── lighting/ # 8 lighting preset scenes
|
|
190
|
+
├── particles/ # 8 particle effect scenes
|
|
191
|
+
├── sfx/ # 45 sound effects (.wav)
|
|
192
|
+
├── music/ # BGM tracks catalog
|
|
193
|
+
└── map_preview.png # Map overview image
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Open in Godot → Press F5 → Walk around, fight enemies, open inventory, talk to NPCs.**
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Character Presets
|
|
201
|
+
|
|
202
|
+
| Preset | Layers | Style |
|
|
203
|
+
|--------|--------|-------|
|
|
204
|
+
| `warrior` | Plate armor, longsword, brown hair | Classic RPG fighter |
|
|
205
|
+
| `mage` | Robe, staff, white hair | Spellcaster |
|
|
206
|
+
| `rogue` | Leather armor, dagger, black hair | Stealth class |
|
|
207
|
+
| `ranger` | Leather, bow, green hood | Ranged fighter |
|
|
208
|
+
| `paladin` | Gold plate, greatsword, blonde hair | Holy knight |
|
|
209
|
+
| `necromancer` | Dark robe, skull staff, bald | Dark magic |
|
|
210
|
+
| `cleric` | White robe, mace, brown hair | Healer |
|
|
211
|
+
| `barbarian` | Fur armor, battleaxe, red hair | Berserker |
|
|
212
|
+
| `monk` | Simple clothes, bo staff, shaved head | Martial arts |
|
|
213
|
+
| `bard` | Fancy clothes, lute, curly hair | Support class |
|
|
214
|
+
| ...and 7 more | | |
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# See all presets
|
|
218
|
+
lpc-forge list
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Map Types
|
|
224
|
+
|
|
225
|
+
| Type | Algorithm | Features |
|
|
226
|
+
|------|-----------|----------|
|
|
227
|
+
| `dungeon` | BSP (Binary Space Partition) | Rooms, corridors, doors, spawn/treasure/boss POIs |
|
|
228
|
+
| `cave` | Cellular Automata | Organic caverns, varied openness |
|
|
229
|
+
| `overworld` | Multi-octave noise | Biomes, rivers, mountains, forests |
|
|
230
|
+
| `town` | District-based | Houses, shops, roads, town square |
|
|
231
|
+
| `wfc` | Wave Function Collapse | Pattern-based, highly varied |
|
|
232
|
+
| `multifloor` | Stacked BSP | Multi-level dungeons with stairs |
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
lpc-forge map --type dungeon --width 60 --height 60 --seed "my-dungeon" -o ./output
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## License & Credits
|
|
241
|
+
|
|
242
|
+
### Tool License
|
|
243
|
+
|
|
244
|
+
LPC Forge (the CLI tool) is licensed under **GPL-3.0-or-later**. See [LICENSE](LICENSE).
|
|
245
|
+
|
|
246
|
+
### Asset Licenses
|
|
247
|
+
|
|
248
|
+
Character sprites use [Liberated Pixel Cup](https://lpc.opengameart.org/) assets under **CC-BY-SA 3.0** and **CC-BY-SA 4.0**. Full artist credits in [CREDITS.csv](CREDITS.csv).
|
|
249
|
+
|
|
250
|
+
Premium assets include curated content from OpenGameArt.org under CC0/CC-BY-SA licenses. When distributing games made with LPC Forge, include the generated `CREDITS.md` file.
|
|
251
|
+
|
|
252
|
+
### Premium License
|
|
253
|
+
|
|
254
|
+
LPC Forge Premium (the `--full` content pack, game systems, SFX presets, and curated assets) is sold separately at [blueth.online](https://launchday.gumroad.com/l/lpc-forge-premium). One-time purchase, unlimited projects, no subscription.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Contributing
|
|
259
|
+
|
|
260
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). PRs welcome for the open-source core — character presets, map algorithms, export improvements.
|
|
261
|
+
|
|
262
|
+
Premium features (systems, SFX, UI, lighting, particles) are maintained by [LaunchDay Studio](https://launchdaystudio.com).
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Links
|
|
267
|
+
|
|
268
|
+
- 🌐 [blueth.online](https://blueth.online) — Blueth Plugin Marketplace
|
|
269
|
+
- 💬 [Discord](https://discord.gg/bJDGXc4DvW) — Community & Support
|
|
270
|
+
- 🐛 [Issues](https://github.com/LaunchDay-Studio-Inc/lpc-forge/issues) — Bug Reports
|
|
271
|
+
- 📖 [LPC Wiki](https://lpc.opengameart.org/) — Liberated Pixel Cup
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
<div align="center">
|
|
276
|
+
|
|
277
|
+
**Made by [LaunchDay Studio](https://launchdaystudio.com) 🚀**
|
|
278
|
+
|
|
279
|
+
*Stop spending weeks on placeholder art. Start building your game.*
|
|
280
|
+
|
|
281
|
+
</div>
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CharacterSpec } from './types.js';
|
|
2
|
+
export interface BatchEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
preset?: string;
|
|
5
|
+
spec?: CharacterSpec;
|
|
6
|
+
slice?: boolean;
|
|
7
|
+
godot?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface BatchConfig {
|
|
10
|
+
characters: BatchEntry[];
|
|
11
|
+
outputDir?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function runBatch(configPath: string, repoRoot: string, outputBase: string): Promise<{
|
|
14
|
+
name: string;
|
|
15
|
+
success: boolean;
|
|
16
|
+
error?: string;
|
|
17
|
+
}[]>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { composeCharacter } from './composer.js';
|
|
2
|
+
import { PRESETS } from './presets.js';
|
|
3
|
+
import { sliceCharacter } from './slicer.js';
|
|
4
|
+
import { exportCharacterToGodot } from '../export/godot.js';
|
|
5
|
+
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
export async function runBatch(configPath, repoRoot, outputBase) {
|
|
8
|
+
const raw = await readFile(configPath, 'utf-8');
|
|
9
|
+
const config = JSON.parse(raw);
|
|
10
|
+
const results = [];
|
|
11
|
+
for (const entry of config.characters) {
|
|
12
|
+
const outDir = resolve(outputBase, entry.name);
|
|
13
|
+
await mkdir(outDir, { recursive: true });
|
|
14
|
+
try {
|
|
15
|
+
let spec;
|
|
16
|
+
if (entry.preset) {
|
|
17
|
+
const preset = PRESETS[entry.preset];
|
|
18
|
+
if (!preset)
|
|
19
|
+
throw new Error(`Unknown preset: ${entry.preset}`);
|
|
20
|
+
spec = preset.spec;
|
|
21
|
+
}
|
|
22
|
+
else if (entry.spec) {
|
|
23
|
+
spec = entry.spec;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
throw new Error('Each entry needs either "preset" or "spec"');
|
|
27
|
+
}
|
|
28
|
+
const buffer = await composeCharacter(spec, repoRoot);
|
|
29
|
+
await writeFile(join(outDir, 'spritesheet.png'), buffer);
|
|
30
|
+
if (entry.slice) {
|
|
31
|
+
await sliceCharacter(buffer, join(outDir, 'frames'));
|
|
32
|
+
}
|
|
33
|
+
if (entry.godot) {
|
|
34
|
+
await exportCharacterToGodot(buffer, outDir, entry.name);
|
|
35
|
+
}
|
|
36
|
+
results.push({ name: entry.name, success: true });
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
results.push({
|
|
40
|
+
name: entry.name,
|
|
41
|
+
success: false,
|
|
42
|
+
error: err instanceof Error ? err.message : String(err),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=batch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.js","sourceRoot":"","sources":["../../src/character/batch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAe1C,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,UAAkB,EAClB,QAAgB,EAChB,UAAkB;IAElB,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAChD,MAAM,MAAM,GAAgB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAyD,EAAE,CAAC;IAEzE,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACtC,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC/C,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEzC,IAAI,CAAC;YACH,IAAI,IAAmB,CAAC;YACxB,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBACjB,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;gBACrC,IAAI,CAAC,MAAM;oBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;gBAChE,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;YACrB,CAAC;iBAAM,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;gBACtB,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;YACpB,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;YAChE,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YACtD,MAAM,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM,CAAC,CAAC;YAEzD,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;gBAChB,MAAM,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;YACvD,CAAC;YAED,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;gBAChB,MAAM,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAC3D,CAAC;YAED,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;aACxD,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { ANIMATIONS, FRAME_SIZE, SHEET_WIDTH, SHEET_HEIGHT, ANIMATION_FOLDER_MAP, } from './types.js';
|
|
5
|
+
import { loadDefinitions, findDefinition } from './definitions.js';
|
|
6
|
+
/** Compose a complete character spritesheet from a CharacterSpec */
|
|
7
|
+
export async function composeCharacter(spec, repoRoot) {
|
|
8
|
+
const registry = await loadDefinitions(repoRoot);
|
|
9
|
+
const spritesDir = join(repoRoot, 'spritesheets');
|
|
10
|
+
// Resolve all layers to concrete sprite paths
|
|
11
|
+
const resolvedLayers = [];
|
|
12
|
+
for (const layer of spec.layers) {
|
|
13
|
+
const def = findDefinition(registry, layer.category, layer.subcategory);
|
|
14
|
+
if (!def) {
|
|
15
|
+
console.warn(`Warning: No definition found for ${layer.category}/${layer.subcategory}, skipping`);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
for (const entry of def.layers) {
|
|
19
|
+
// Skip custom animation layers for now (oversize attacks, etc.)
|
|
20
|
+
if (entry.customAnimation)
|
|
21
|
+
continue;
|
|
22
|
+
const basePath = entry.paths[spec.bodyType];
|
|
23
|
+
if (!basePath) {
|
|
24
|
+
// Try to find a fallback body type
|
|
25
|
+
const fallback = findFallbackBodyType(entry.paths, spec.bodyType);
|
|
26
|
+
if (!fallback) {
|
|
27
|
+
console.warn(`Warning: ${def.name} layer has no path for body type "${spec.bodyType}", skipping`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
resolvedLayers.push({
|
|
31
|
+
zPos: entry.zPos,
|
|
32
|
+
basePath: fallback,
|
|
33
|
+
variant: layer.variant,
|
|
34
|
+
animations: def.animations,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
resolvedLayers.push({
|
|
39
|
+
zPos: entry.zPos,
|
|
40
|
+
basePath,
|
|
41
|
+
variant: layer.variant,
|
|
42
|
+
animations: def.animations,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Sort by zPos ascending (back to front)
|
|
48
|
+
resolvedLayers.sort((a, b) => a.zPos - b.zPos);
|
|
49
|
+
// Build universal sheets for each resolved layer
|
|
50
|
+
const layerBuffers = [];
|
|
51
|
+
for (const layer of resolvedLayers) {
|
|
52
|
+
const buffer = await buildUniversalSheet(layer, spritesDir);
|
|
53
|
+
if (buffer) {
|
|
54
|
+
layerBuffers.push(buffer);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (layerBuffers.length === 0) {
|
|
58
|
+
throw new Error('No valid layers could be loaded. Check your CharacterSpec.');
|
|
59
|
+
}
|
|
60
|
+
// Composite all layers together
|
|
61
|
+
const composites = layerBuffers.map((input) => ({
|
|
62
|
+
input,
|
|
63
|
+
top: 0,
|
|
64
|
+
left: 0,
|
|
65
|
+
blend: 'over',
|
|
66
|
+
}));
|
|
67
|
+
return sharp({
|
|
68
|
+
create: {
|
|
69
|
+
width: SHEET_WIDTH,
|
|
70
|
+
height: SHEET_HEIGHT,
|
|
71
|
+
channels: 4,
|
|
72
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
.png()
|
|
76
|
+
.composite(composites)
|
|
77
|
+
.toBuffer();
|
|
78
|
+
}
|
|
79
|
+
function findFallbackBodyType(paths, bodyType) {
|
|
80
|
+
// Fallback chain
|
|
81
|
+
const fallbacks = {
|
|
82
|
+
muscular: ['male'],
|
|
83
|
+
pregnant: ['female'],
|
|
84
|
+
teen: ['male', 'female'],
|
|
85
|
+
child: ['teen', 'male'],
|
|
86
|
+
};
|
|
87
|
+
const chain = fallbacks[bodyType] ?? [];
|
|
88
|
+
for (const fb of chain) {
|
|
89
|
+
if (paths[fb])
|
|
90
|
+
return paths[fb];
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/** Build a universal 832×3456 sheet from individual animation PNGs */
|
|
95
|
+
async function buildUniversalSheet(layer, spritesDir) {
|
|
96
|
+
const composites = [];
|
|
97
|
+
// Map animation names used in definitions to folder names
|
|
98
|
+
const animationList = Object.keys(ANIMATIONS);
|
|
99
|
+
for (const animName of animationList) {
|
|
100
|
+
const animInfo = ANIMATIONS[animName];
|
|
101
|
+
// Check if this layer supports this animation
|
|
102
|
+
const defAnimNames = layer.animations.map((a) => {
|
|
103
|
+
const mapped = ANIMATION_FOLDER_MAP[a];
|
|
104
|
+
return mapped ?? a;
|
|
105
|
+
});
|
|
106
|
+
// The folder name might differ from the animation name
|
|
107
|
+
const folderName = ANIMATION_FOLDER_MAP[animName] ?? animName;
|
|
108
|
+
// Check if the definition lists this animation (using either name)
|
|
109
|
+
const supportsAnim = layer.animations.length === 0 || // no filter = all
|
|
110
|
+
layer.animations.some((a) => {
|
|
111
|
+
const mapped = ANIMATION_FOLDER_MAP[a] ?? a;
|
|
112
|
+
return mapped === animName || mapped === folderName || a === animName;
|
|
113
|
+
});
|
|
114
|
+
if (!supportsAnim)
|
|
115
|
+
continue;
|
|
116
|
+
// Convert variant name: spaces to underscores for filenames
|
|
117
|
+
const variantFile = layer.variant.replace(/ /g, '_');
|
|
118
|
+
// Try to find the animation PNG
|
|
119
|
+
const animPath = join(spritesDir, layer.basePath, folderName, `${variantFile}.png`);
|
|
120
|
+
if (!existsSync(animPath)) {
|
|
121
|
+
// Also try without folder mapping
|
|
122
|
+
const altPath = join(spritesDir, layer.basePath, animName, `${variantFile}.png`);
|
|
123
|
+
if (existsSync(altPath)) {
|
|
124
|
+
try {
|
|
125
|
+
const buf = await sharp(altPath).png().toBuffer();
|
|
126
|
+
composites.push({
|
|
127
|
+
input: buf,
|
|
128
|
+
top: animInfo.row * FRAME_SIZE,
|
|
129
|
+
left: 0,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Skip unreadable files
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const buf = await sharp(animPath).png().toBuffer();
|
|
140
|
+
composites.push({
|
|
141
|
+
input: buf,
|
|
142
|
+
top: animInfo.row * FRAME_SIZE,
|
|
143
|
+
left: 0,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Skip unreadable files
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (composites.length === 0)
|
|
151
|
+
return null;
|
|
152
|
+
return sharp({
|
|
153
|
+
create: {
|
|
154
|
+
width: SHEET_WIDTH,
|
|
155
|
+
height: SHEET_HEIGHT,
|
|
156
|
+
channels: 4,
|
|
157
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
.png()
|
|
161
|
+
.composite(composites)
|
|
162
|
+
.toBuffer();
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=composer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composer.js","sourceRoot":"","sources":["../../src/character/composer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LayerDefinition } from './types.js';
|
|
2
|
+
export interface DefinitionRegistry {
|
|
3
|
+
/** All parsed definitions keyed by "category/subcategory" */
|
|
4
|
+
definitions: Map<string, LayerDefinition>;
|
|
5
|
+
/** All categories (body, hair, torso, etc.) */
|
|
6
|
+
categories: string[];
|
|
7
|
+
}
|
|
8
|
+
/** Recursively read all JSON definitions from sheet_definitions/ */
|
|
9
|
+
export declare function loadDefinitions(repoRoot: string): Promise<DefinitionRegistry>;
|
|
10
|
+
/** List all available layers grouped by category */
|
|
11
|
+
export declare function listLayers(registry: DefinitionRegistry): Record<string, {
|
|
12
|
+
name: string;
|
|
13
|
+
variants: string[];
|
|
14
|
+
}[]>;
|
|
15
|
+
/** Find a definition by searching category and subcategory patterns */
|
|
16
|
+
export declare function findDefinition(registry: DefinitionRegistry, category: string, subcategory: string): LayerDefinition | undefined;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/** Recursively read all JSON definitions from sheet_definitions/ */
|
|
4
|
+
export async function loadDefinitions(repoRoot) {
|
|
5
|
+
const defsDir = join(repoRoot, 'sheet_definitions');
|
|
6
|
+
const definitions = new Map();
|
|
7
|
+
const categories = await readdir(defsDir);
|
|
8
|
+
for (const category of categories) {
|
|
9
|
+
const categoryPath = join(defsDir, category);
|
|
10
|
+
const s = await stat(categoryPath);
|
|
11
|
+
if (!s.isDirectory())
|
|
12
|
+
continue;
|
|
13
|
+
await loadCategoryDefinitions(categoryPath, category, definitions);
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
definitions,
|
|
17
|
+
categories: [...new Set([...definitions.values()].map((d) => d.name))],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function loadCategoryDefinitions(dirPath, categoryPrefix, definitions) {
|
|
21
|
+
const entries = await readdir(dirPath);
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const fullPath = join(dirPath, entry);
|
|
24
|
+
const s = await stat(fullPath);
|
|
25
|
+
if (s.isDirectory()) {
|
|
26
|
+
// Recurse into subdirectories
|
|
27
|
+
await loadCategoryDefinitions(fullPath, categoryPrefix, definitions);
|
|
28
|
+
}
|
|
29
|
+
else if (entry.endsWith('.json') && !entry.startsWith('meta_')) {
|
|
30
|
+
try {
|
|
31
|
+
const def = await parseDefinitionFile(fullPath, categoryPrefix);
|
|
32
|
+
if (def) {
|
|
33
|
+
definitions.set(def.name, def);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Skip invalid JSON files
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function parseDefinitionFile(filePath, category) {
|
|
43
|
+
const content = await readFile(filePath, 'utf-8');
|
|
44
|
+
const json = JSON.parse(content);
|
|
45
|
+
if (!json.name)
|
|
46
|
+
return null;
|
|
47
|
+
// Extract layer entries (layer_1, layer_2, etc.)
|
|
48
|
+
const layers = [];
|
|
49
|
+
for (let i = 1; i <= 20; i++) {
|
|
50
|
+
const layerKey = `layer_${i}`;
|
|
51
|
+
const layerData = json[layerKey];
|
|
52
|
+
if (!layerData)
|
|
53
|
+
break;
|
|
54
|
+
const paths = {};
|
|
55
|
+
const bodyTypes = ['male', 'muscular', 'female', 'pregnant', 'teen', 'child'];
|
|
56
|
+
for (const bt of bodyTypes) {
|
|
57
|
+
if (layerData[bt]) {
|
|
58
|
+
paths[bt] = layerData[bt];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
layers.push({
|
|
62
|
+
zPos: layerData.zPos ?? 0,
|
|
63
|
+
paths,
|
|
64
|
+
customAnimation: layerData.custom_animation,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (layers.length === 0)
|
|
68
|
+
return null;
|
|
69
|
+
// Derive a subcategory key from the filename
|
|
70
|
+
const fileName = filePath.split('/').pop().replace('.json', '');
|
|
71
|
+
return {
|
|
72
|
+
name: `${category}/${fileName}`,
|
|
73
|
+
priority: json.priority ?? 0,
|
|
74
|
+
layers,
|
|
75
|
+
variants: json.variants ?? [],
|
|
76
|
+
animations: json.animations ?? [],
|
|
77
|
+
matchBodyColor: json.match_body_color ?? false,
|
|
78
|
+
typeName: json.type_name,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** List all available layers grouped by category */
|
|
82
|
+
export function listLayers(registry) {
|
|
83
|
+
const result = {};
|
|
84
|
+
for (const [key, def] of registry.definitions) {
|
|
85
|
+
const category = key.split('/')[0];
|
|
86
|
+
if (!result[category])
|
|
87
|
+
result[category] = [];
|
|
88
|
+
result[category].push({
|
|
89
|
+
name: key,
|
|
90
|
+
variants: def.variants,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
/** Find a definition by searching category and subcategory patterns */
|
|
96
|
+
export function findDefinition(registry, category, subcategory) {
|
|
97
|
+
// Try exact key match first
|
|
98
|
+
const exactKey = `${category}/${subcategory}`;
|
|
99
|
+
if (registry.definitions.has(exactKey)) {
|
|
100
|
+
return registry.definitions.get(exactKey);
|
|
101
|
+
}
|
|
102
|
+
// Try partial matching
|
|
103
|
+
for (const [key, def] of registry.definitions) {
|
|
104
|
+
if (key.startsWith(category + '/') && key.includes(subcategory)) {
|
|
105
|
+
return def;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Try matching by type_name
|
|
109
|
+
for (const [, def] of registry.definitions) {
|
|
110
|
+
if (def.typeName === subcategory) {
|
|
111
|
+
return def;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=definitions.js.map
|