create-asciitorium 0.1.34 → 0.1.36
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/README.md +4 -4
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/templates/base/index.html +1 -1
- package/dist/templates/base/package.json +1 -1
- package/dist/templates/base/public/art/ART-DESIGN-SPEC.md +375 -0
- package/dist/templates/base/public/art/README.md +115 -0
- package/dist/templates/base/public/art/asciitorium.art +111 -0
- package/dist/templates/base/public/art/borders/bubbles.art +16 -0
- package/dist/templates/base/public/art/borders/dna.art +9 -0
- package/dist/templates/base/public/art/castle.art +4 -0
- package/dist/templates/base/public/art/computer.art +17 -0
- package/dist/templates/base/public/art/font/pencil.art +168 -0
- package/dist/templates/base/public/art/maps/README.md +251 -0
- package/dist/templates/base/public/art/maps/example/README.md +62 -0
- package/dist/templates/base/public/art/maps/example/legend.json +29 -0
- package/dist/templates/base/public/art/maps/example/map.art +21 -0
- package/dist/templates/base/public/art/materials/README.md +279 -0
- package/dist/templates/base/public/art/materials/bone.art +11 -0
- package/dist/templates/base/public/art/materials/door-wooden.art +154 -0
- package/dist/templates/base/public/art/materials/wall-brick.art +128 -0
- package/dist/templates/base/public/art/materials/wall-wireframe.art +128 -0
- package/dist/templates/base/public/art/mazes/example.art +21 -0
- package/dist/templates/base/public/art/sounds/door-close.mp3 +0 -0
- package/dist/templates/base/public/art/sounds/door-open.mp3 +0 -0
- package/dist/templates/base/public/art/sounds/taps.mp3 +0 -0
- package/dist/templates/base/public/art/sprites/asciitorium.art +6 -0
- package/dist/templates/base/public/art/sprites/balloon.art +126 -0
- package/dist/templates/base/public/art/sprites/beating-heart.art +49 -0
- package/dist/templates/base/public/art/sprites/castle.art +4 -0
- package/dist/templates/base/public/art/sprites/component-icon.art +6 -0
- package/dist/templates/base/public/art/sprites/eyes.art +61 -0
- package/dist/templates/base/public/art/sprites/firework.art +58 -0
- package/dist/templates/base/public/art/sprites/heart.art +5 -0
- package/dist/templates/base/public/art/sprites/multi-select-test.art +6 -0
- package/dist/templates/base/public/art/sprites/nav-basics.art +5 -0
- package/dist/templates/base/public/art/sprites/pyramid.art +5 -0
- package/dist/templates/base/public/art/sprites/tatooene.art +5 -0
- package/dist/templates/base/public/art/sprites/welcome.art +7 -0
- package/dist/templates/base/public/art/tatooene.txt +5 -0
- package/dist/templates/base/public/index.css +41 -0
- package/dist/templates/base/public/logo.png +0 -0
- package/dist/templates/base/scripts/gen-figlet-art.js +2 -2
- package/dist/templates/base/scripts/map-builder.js +294 -0
- package/dist/templates/base/src/main.tsx +22 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# create-asciitorium
|
|
2
2
|
|
|
3
|
-
`create-asciitorium` is a command-line tool that helps you quickly scaffold a new [asciitorium](https://github.com/iroknee/asciitorium) project. It sets up all the necessary files, folders, and dependencies so you can start building
|
|
3
|
+
`create-asciitorium` is a command-line tool that helps you quickly scaffold a new [asciitorium](https://github.com/iroknee/asciitorium) project. It sets up all the necessary files, folders, and dependencies so you can start building CLUI applications with minimal setup.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
@@ -41,7 +41,7 @@ npm run build
|
|
|
41
41
|
|
|
42
42
|
### Other Scripts
|
|
43
43
|
|
|
44
|
-
Asciitorium supports viewing ascii art files. There are a few easy ways to create some specific ascii art, including FIGlet fonts and
|
|
44
|
+
Asciitorium supports viewing ascii art files. There are a few easy ways to create some specific ascii art, including FIGlet fonts and maps.
|
|
45
45
|
|
|
46
46
|
To generate FIGlet ASCII art assets (automatically placed in public/art):
|
|
47
47
|
|
|
@@ -94,8 +94,8 @@ To generate ASCII maze files (placed in public/art/mazes):
|
|
|
94
94
|
node scripts/maze-builder.js <width> <height> <filename> [--smooth]
|
|
95
95
|
|
|
96
96
|
# Examples:
|
|
97
|
-
node scripts/maze-builder.js 10 10 dungeon-level-1.
|
|
98
|
-
node scripts/maze-builder.js 15 20 castle-maze.
|
|
97
|
+
node scripts/maze-builder.js 10 10 dungeon-level-1.art
|
|
98
|
+
node scripts/maze-builder.js 15 20 castle-maze.art --smooth
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
The `--smooth` flag uses Unicode box drawing characters for improved visual appearance.
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,8 @@ async function main() {
|
|
|
34
34
|
console.log(red(`✖ Directory "${target}" already exists.`));
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
// Always use npm as the package manager
|
|
38
|
+
const pm = argv.pm ?? 'npm';
|
|
38
39
|
const template = argv.template ?? 'base';
|
|
39
40
|
const useGit = Boolean(argv.git);
|
|
40
41
|
const doInstall = Boolean(argv.install);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,aAAa;AACb,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,QAAQ,MAAM,UAAU,CAAC;AAChC,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QAC3C,MAAM,EAAE,CAAC,UAAU,EAAE,IAAI,CAAC;QAC1B,OAAO,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC;QAC3B,OAAO,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;KACtC,CAAC,CAAC;IAEH,IAAI,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAuB,CAAC;IAC7C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC;YACxB,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,MAAM;YACZ,OAAO,EAAE,eAAe;YACxB,OAAO,EAAE,oBAAoB;SAC9B,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC;IACpB,CAAC;IACD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,sCAAsC,CAAC,CAAC;QAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;IACvD,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,gBAAgB,MAAM,mBAAmB,CAAC,CAAC,CAAC;QAC5D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"cli": "tsx src/main.tsx",
|
|
11
11
|
"figlet": "node scripts/gen-figlet-art.js",
|
|
12
12
|
"figlet:fonts": "node -e 'require(\"figlet\").fonts((_, f) => console.log(f.join(\"\\n\")))\"",
|
|
13
|
-
"
|
|
13
|
+
"map-builder": "node scripts/map-builder.js"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"typescript": "^5.6.0",
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# Art Asset Format Specification
|
|
2
|
+
|
|
3
|
+
This document defines the metadata format for ASCII art assets in asciitorium, including fonts, materials, and sprites.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Art assets use a text-based format that combines:
|
|
8
|
+
|
|
9
|
+
- **Section headers** (`§`) defining global asset properties
|
|
10
|
+
- **Layer/frame separators** (`¶`) with metadata for each section
|
|
11
|
+
- **ASCII art content** following each separator
|
|
12
|
+
|
|
13
|
+
## Design Goals
|
|
14
|
+
|
|
15
|
+
1. **Human-readable**: Artists should be able to read and write metadata without tooling
|
|
16
|
+
2. **Simple**: Use standard JSON format with flat key-value structure
|
|
17
|
+
3. **No nesting**: Flat data structures only - no nested objects or arrays
|
|
18
|
+
4. **Consistent**: Same format works for materials, sprites, and font asset types
|
|
19
|
+
5. **Extensible**: Easy to add new properties without breaking existing assets
|
|
20
|
+
|
|
21
|
+
## Format Syntax
|
|
22
|
+
|
|
23
|
+
### File Header (`§`)
|
|
24
|
+
|
|
25
|
+
The file header appears once at the beginning of the file and defines the asset type and global properties.
|
|
26
|
+
|
|
27
|
+
**Format:**
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
§ {"key":"value","key":"value"}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Example:**
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
§ {"kind":"material","usage":"first-person","placement":"scenery"}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Layer/Frame Separator (`¶`)
|
|
40
|
+
|
|
41
|
+
Separates different layers (materials), characters (fonts) or frames (sprites) with section-specific metadata.
|
|
42
|
+
|
|
43
|
+
**Format:**
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
¶ {"key":"value","key":"value"}
|
|
47
|
+
[ASCII art content follows]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Example:**
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
¶ {"layer":"here","pos":"center"}
|
|
54
|
+
|‽‽‽‽‽‽‽|
|
|
55
|
+
|‽‽‽‽‽‽‽|
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Parsing Rules
|
|
59
|
+
|
|
60
|
+
### JSON Format
|
|
61
|
+
|
|
62
|
+
1. **Parse as JSON**: Standard JSON.parse() after removing separator character
|
|
63
|
+
2. **Primitive values only**: All values must be strings, numbers, or booleans
|
|
64
|
+
3. **No nesting**: Objects must be flat - no nested objects or arrays
|
|
65
|
+
4. **Empty metadata**: Empty separator `¶` or `§` is valid (inherits defaults)
|
|
66
|
+
|
|
67
|
+
### Single-Frame Sprites
|
|
68
|
+
|
|
69
|
+
If a file does not start with a `§` file header, it is assumed to be a single-frame sprite with no metadata:
|
|
70
|
+
|
|
71
|
+
```txt
|
|
72
|
+
___
|
|
73
|
+
/ \
|
|
74
|
+
| o |
|
|
75
|
+
\___/
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This is equivalent to:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
§ {"kind":"sprite","loop":"false"}
|
|
82
|
+
¶
|
|
83
|
+
___
|
|
84
|
+
/ \
|
|
85
|
+
| o |
|
|
86
|
+
\___/
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Empty Separators
|
|
90
|
+
|
|
91
|
+
A separator with no metadata is valid for sprites:
|
|
92
|
+
|
|
93
|
+
```txt
|
|
94
|
+
¶
|
|
95
|
+
[ASCII art content]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This inherits defaults or indicates a frame with no special properties.
|
|
99
|
+
|
|
100
|
+
## Font Assets
|
|
101
|
+
|
|
102
|
+
Fonts define ASCII art representations of characters for rendering text in stylized formats.
|
|
103
|
+
|
|
104
|
+
### File Header Properties
|
|
105
|
+
|
|
106
|
+
| Key | Values | Required | Description |
|
|
107
|
+
| ------ | ------ | -------- | --------------------- |
|
|
108
|
+
| `kind` | `font` | Yes | Asset type identifier |
|
|
109
|
+
|
|
110
|
+
### Character Separator Properties
|
|
111
|
+
|
|
112
|
+
| Key | Values | Required | Description |
|
|
113
|
+
| ----------- | ------ | -------- | ----------------------------------- |
|
|
114
|
+
| `character` | string | Yes | The character this glyph represents |
|
|
115
|
+
|
|
116
|
+
### Font Example
|
|
117
|
+
|
|
118
|
+
``` json
|
|
119
|
+
§ {"kind":"font"}
|
|
120
|
+
¶ {"character":"a"}
|
|
121
|
+
|
|
122
|
+
╭─╮
|
|
123
|
+
╭─┤
|
|
124
|
+
╰─╰
|
|
125
|
+
|
|
126
|
+
¶ {"character":"b"}
|
|
127
|
+
│
|
|
128
|
+
├─╮
|
|
129
|
+
│ │
|
|
130
|
+
╯─╯
|
|
131
|
+
|
|
132
|
+
¶ {"character":"c"}
|
|
133
|
+
|
|
134
|
+
╭─╮
|
|
135
|
+
│
|
|
136
|
+
╰─╯
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Font Design Rules
|
|
140
|
+
|
|
141
|
+
- Each character glyph should have consistent height (including leading/trailing blank lines)
|
|
142
|
+
- Characters can have variable width
|
|
143
|
+
- Blank lines before/after the glyph art create vertical spacing
|
|
144
|
+
- The `character` property must be a single character (letter, number, punctuation, etc.)
|
|
145
|
+
- Fonts are typically loaded by name and used to render styled text
|
|
146
|
+
|
|
147
|
+
## Sprite Assets
|
|
148
|
+
|
|
149
|
+
Sprites define animated ASCII art with multiple frames.
|
|
150
|
+
|
|
151
|
+
### File Header Properties
|
|
152
|
+
|
|
153
|
+
| Key | Values | Required | Description |
|
|
154
|
+
| -------------------- | --------------- | -------- | ------------------------------------------ |
|
|
155
|
+
| `kind` | `sprite` | Yes | Asset type identifier |
|
|
156
|
+
| `loop` | `true`, `false` | No | Whether animation loops (default: `false`) |
|
|
157
|
+
| `default-frame-rate` | number (ms) | No | Default frame duration in milliseconds (default is 250 if none is supplied) |
|
|
158
|
+
|
|
159
|
+
### Frame Separator Properties
|
|
160
|
+
|
|
161
|
+
| Key | Values | Required | Description |
|
|
162
|
+
| ---------- | ----------- | -------- | ---------------------------------- |
|
|
163
|
+
| `duration` | number (ms) | No | Frame duration (overrides default) |
|
|
164
|
+
| `sound` | filename | No | Sound to play when frame displays |
|
|
165
|
+
| `event` | string | No | Custom event name to trigger |
|
|
166
|
+
|
|
167
|
+
### Sprite Example
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
§ {"kind":"sprite","loop":true,"default-frame-rate":120}
|
|
171
|
+
¶ {"duration":1000}
|
|
172
|
+
_ _ _ _
|
|
173
|
+
__ _ ___ ___(_|_) |_ ___ _ __(_)_ _ _ __ ___
|
|
174
|
+
/ _` / __|/ __| | | __/ _ \| '__| | | | | '_ ` _ \
|
|
175
|
+
| (_| \__ \ (__| | | || (_) | | | | |_| | | | | | |
|
|
176
|
+
\__,_|___/\___|_|_|\__\___/|_| |_|\__,_|_| |_| |_|
|
|
177
|
+
¶ {"duration":500}
|
|
178
|
+
. _ * _ * .
|
|
179
|
+
__ _ ___ ___(_|_).|_ * _ __(_)_ _ * __
|
|
180
|
+
/ _` / __|/ _ . | __/ . \ '__| | | | ' ` \ .
|
|
181
|
+
| (_| \__ (__| *|.| || (_) | * | | |_| *| | | │ .
|
|
182
|
+
\__,_|___/\__ ._|_|\__\ * /|_| .|\__ |_| |_│
|
|
183
|
+
¶
|
|
184
|
+
* _ * . * . *
|
|
185
|
+
__ * (_|_) . * _ __ . . __
|
|
186
|
+
/ ` __| | __/ * \ '__| * ` \
|
|
187
|
+
| * * \__ | | || (_) | . | * * | | │
|
|
188
|
+
. .|___/ |_| \__\___/|_| . . |_| . |_│
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Animation Control
|
|
192
|
+
|
|
193
|
+
- **loop:true**: Animation repeats from first frame after last frame
|
|
194
|
+
- **loop:false**: Animation plays once and stops on last frame
|
|
195
|
+
|
|
196
|
+
## Material Assets
|
|
197
|
+
|
|
198
|
+
Materials define visual representations at different distances in first-person view.
|
|
199
|
+
|
|
200
|
+
### File Header Properties
|
|
201
|
+
|
|
202
|
+
| Key | Values | Required | Description |
|
|
203
|
+
| -------------- | ------------------------------------------- | -------- | --------------------------------- |
|
|
204
|
+
| `kind` | `material` | Yes | Asset type identifier |
|
|
205
|
+
| `usage` | `first-person`, `top-down`, `side-scroller` | Yes | Rendering context |
|
|
206
|
+
| `placement` | `scenery`, `ground`, `ceiling` | No | Surface type (default: `scenery`) |
|
|
207
|
+
| `onEnterSound` | filename | No | Sound when player enters tile |
|
|
208
|
+
| `onExitSound` | filename | No | Sound when player exits tile |
|
|
209
|
+
| `ambientSound` | filename | No | Looping sound near this material |
|
|
210
|
+
|
|
211
|
+
### Layer Separator Properties
|
|
212
|
+
|
|
213
|
+
| Key | Values | Required | Description |
|
|
214
|
+
| ------- | ------------------------------- | -------- | ------------------------------- |
|
|
215
|
+
| `layer` | `here`, `near`, `middle`, `far` | Yes | Distance layer |
|
|
216
|
+
| `pos` | `left`, `center`, `right` | Yes | Horizontal position |
|
|
217
|
+
| `x` | number | No | Horizontal offset for alignment |
|
|
218
|
+
|
|
219
|
+
### Material Example
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
§ {"kind":"material","usage":"first-person","placement":"scenery","onEnterSound":"door-open.mp3","onExitSound":"door-close.mp3"}
|
|
223
|
+
¶ {"layer":"here","pos":"left"}
|
|
224
|
+
╲
|
|
225
|
+
┊╲
|
|
226
|
+
╲┊ ╲
|
|
227
|
+
¶ {"layer":"here","pos":"center"}
|
|
228
|
+
|‽‽‽‽‽‽‽‽‽‽|
|
|
229
|
+
|‽‽‽‽‽‽‽‽‽‽|
|
|
230
|
+
|‽‽‽‽‽‽‽‽‽‽|
|
|
231
|
+
¶ {"layer":"here","pos":"right"}
|
|
232
|
+
╱
|
|
233
|
+
╱┊
|
|
234
|
+
╱ ┊╱
|
|
235
|
+
¶ {"layer":"near","pos":"center"}
|
|
236
|
+
…… ……… ……… …
|
|
237
|
+
┊……┊………┊………┊…┊
|
|
238
|
+
┊┊…╭───────╮…┊
|
|
239
|
+
┊……|┋┋┋┋┋┋┋|…┊
|
|
240
|
+
¶ {"layer":"middle","pos":"center"}
|
|
241
|
+
… … ……
|
|
242
|
+
┊…┊…┊……┊
|
|
243
|
+
┊…╭──╮…┊
|
|
244
|
+
┊…|┋┋|…┊
|
|
245
|
+
¶ {"layer":"far","pos":"center"}
|
|
246
|
+
……
|
|
247
|
+
┊……┊
|
|
248
|
+
┊||┊
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Layer System
|
|
252
|
+
|
|
253
|
+
Materials use a layered perspective system that adapts to different usage contexts:
|
|
254
|
+
|
|
255
|
+
#### First-Person Usage
|
|
256
|
+
|
|
257
|
+
- **here**: Immediate foreground (player is on this tile)
|
|
258
|
+
- **near**: Close distance (1-2 tiles away)
|
|
259
|
+
- **middle**: Mid distance (3-5 tiles away)
|
|
260
|
+
- **far**: Far distance (6+ tiles away)
|
|
261
|
+
|
|
262
|
+
#### Side-Scroller Usage (Parallax Layers)
|
|
263
|
+
|
|
264
|
+
- **here**: Interactive elements (platforms player stands on, walls player collides with)
|
|
265
|
+
- **near**: Foreground elements (close platforms, foreground decorations)
|
|
266
|
+
- **middle**: Midground elements (trees, buildings, mid-distance scenery)
|
|
267
|
+
- **far**: Background elements (mountains, clouds, distant scenery)
|
|
268
|
+
|
|
269
|
+
#### Top-Down Usage
|
|
270
|
+
|
|
271
|
+
- **here**: Ground level elements (floors, paths the player walks on)
|
|
272
|
+
- **near**: Low obstacles and ground decorations
|
|
273
|
+
- **middle**: Medium height elements (furniture, props)
|
|
274
|
+
- **far**: Tall elements and ceiling details
|
|
275
|
+
|
|
276
|
+
Each layer can have `left`, `center`, and `right` positioned elements for additional composition control.
|
|
277
|
+
|
|
278
|
+
### Sound Triggers
|
|
279
|
+
|
|
280
|
+
Sound properties are defined once in the file header and apply to the entire material:
|
|
281
|
+
|
|
282
|
+
- **onEnterSound**: Plays once when player steps onto the tile
|
|
283
|
+
- **onExitSound**: Plays once when player leaves the tile
|
|
284
|
+
- **ambientSound**: Looping sound while material is visible (future feature)
|
|
285
|
+
|
|
286
|
+
Sound files must be placed in `art/sounds/` directory.
|
|
287
|
+
|
|
288
|
+
### Side-Scroller Material Example
|
|
289
|
+
|
|
290
|
+
Here's an example of materials for a side-scrolling platformer game:
|
|
291
|
+
|
|
292
|
+
**Ground Platform:**
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
§ {"kind":"material","usage":"side-scroller","placement":"ground","onEnterSound":"step.mp3"}
|
|
296
|
+
¶ {"layer":"here","pos":"center"}
|
|
297
|
+
═══════════════════════
|
|
298
|
+
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Brick Wall:**
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
§ {"kind":"material","usage":"side-scroller","placement":"scenery"}
|
|
305
|
+
¶ {"layer":"here","pos":"center"}
|
|
306
|
+
█▓▒░█▓▒░█▓▒░
|
|
307
|
+
░▒▓█░▒▓█░▒▓█
|
|
308
|
+
█▓▒░█▓▒░█▓▒░
|
|
309
|
+
░▒▓█░▒▓█░▒▓█
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Background Mountain (Parallax):**
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
§ {"kind":"material","usage":"side-scroller","placement":"scenery"}
|
|
316
|
+
¶ {"layer":"far","pos":"center"}
|
|
317
|
+
/\
|
|
318
|
+
/ \
|
|
319
|
+
/ \
|
|
320
|
+
/ \
|
|
321
|
+
/ \
|
|
322
|
+
¶ {"layer":"middle","pos":"center"}
|
|
323
|
+
/\ /\
|
|
324
|
+
/ \/ \
|
|
325
|
+
/ \
|
|
326
|
+
¶ {"layer":"near","pos":"center"}
|
|
327
|
+
/\/\
|
|
328
|
+
/ \
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
In side-scrollers, the layer system naturally supports parallax scrolling where `far` layers move slower than `here` layers, creating depth perception.
|
|
332
|
+
|
|
333
|
+
## Validation Rules
|
|
334
|
+
|
|
335
|
+
### Required Properties
|
|
336
|
+
|
|
337
|
+
- File header MUST have `kind` property
|
|
338
|
+
- Material layers MUST have `layer` and `pos` properties
|
|
339
|
+
- Sprite frames MAY omit all properties (inherits defaults)
|
|
340
|
+
|
|
341
|
+
### No Nesting Rule
|
|
342
|
+
|
|
343
|
+
- All values MUST be primitives (string, number, or boolean)
|
|
344
|
+
- Nested objects are NOT allowed
|
|
345
|
+
- Arrays are NOT allowed
|
|
346
|
+
- This keeps the format simple and predictable
|
|
347
|
+
|
|
348
|
+
**Invalid (nested object):**
|
|
349
|
+
|
|
350
|
+
```json
|
|
351
|
+
{ "onEnter": { "sound": "door.mp3" } }
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Valid (flat structure with primitives):**
|
|
355
|
+
|
|
356
|
+
```json
|
|
357
|
+
{ "onEnterSound": "door.mp3", "duration": 1000, "loop": true }
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Value Constraints
|
|
361
|
+
|
|
362
|
+
- **layer**: Must be one of `here`, `near`, `middle`, `far`
|
|
363
|
+
- **pos**: Must be one of `left`, `center`, `right`
|
|
364
|
+
- **placement**: Must be one of `scenery`, `ground`, `ceiling`
|
|
365
|
+
- **usage**: Must be one of `first-person`, `top-down`, `side-scroller`
|
|
366
|
+
- **loop**: Must be boolean `true` or `false`
|
|
367
|
+
- **duration**: Must be positive number (milliseconds)
|
|
368
|
+
- **default-frame-rate**: Must be positive number (milliseconds)
|
|
369
|
+
- **x**: Must be number (integer)
|
|
370
|
+
|
|
371
|
+
### Sound File References
|
|
372
|
+
|
|
373
|
+
- Sound properties contain filename only (no path)
|
|
374
|
+
- Files resolved relative to `art/sounds/` directory
|
|
375
|
+
- Format: `"onEnterSound":"door-open.mp3"` not `"onEnterSound":"art/sounds/door-open.mp3"`
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Art Directory
|
|
2
|
+
|
|
3
|
+
This directory contains ASCII art assets for a project. It is organized into subfolders for maps, materials, and sprites. Each file uses plain text or JSON for easy editing and integration.
|
|
4
|
+
|
|
5
|
+
## Asset Loading and Caching
|
|
6
|
+
|
|
7
|
+
**IMPORTANT:** All assets are automatically loaded and cached by `AssetManager` using reactive `State` objects. When developing components that use assets:
|
|
8
|
+
|
|
9
|
+
### For Component Developers
|
|
10
|
+
|
|
11
|
+
**✅ DO:**
|
|
12
|
+
- Use `AssetManager.getMapState(name)` to get a reactive State for maps
|
|
13
|
+
- Use `AssetManager.getMaterialState(name)` to get a reactive State for materials
|
|
14
|
+
- Subscribe to the returned State to react to loading completion or changes
|
|
15
|
+
- Use `GameWorld.getMapState()` when working with GameWorld instances
|
|
16
|
+
- Share State references across components - the cache handles deduplication
|
|
17
|
+
|
|
18
|
+
**❌ DON'T:**
|
|
19
|
+
- Create local caches in components (Map, Set, plain objects)
|
|
20
|
+
- Use the legacy non-reactive methods (`getMap()`, `getMaterial()`) for new code
|
|
21
|
+
- Load the same asset multiple times - AssetManager caches automatically
|
|
22
|
+
- Store asset data in component properties - subscribe to State instead
|
|
23
|
+
|
|
24
|
+
### Example Usage
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// ✅ Correct: Use reactive State from AssetManager
|
|
28
|
+
const materialState = AssetManager.getMaterialState('brick-wall');
|
|
29
|
+
|
|
30
|
+
// Subscribe to get the value when loaded
|
|
31
|
+
materialState.subscribe((material) => {
|
|
32
|
+
if (material) {
|
|
33
|
+
// Material is loaded, use it
|
|
34
|
+
console.log('Material loaded:', material);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Check current value (may be null if still loading)
|
|
39
|
+
if (materialState.value) {
|
|
40
|
+
// Material already loaded from cache
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ✅ Correct: GameWorld automatically uses AssetManager's State cache
|
|
44
|
+
const gameWorld = new GameWorld({ mapName: 'dungeon' });
|
|
45
|
+
gameWorld.getMapState().subscribe((mapAsset) => {
|
|
46
|
+
// Automatically updates when map loads
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ❌ Wrong: Don't create your own cache
|
|
50
|
+
class MyComponent {
|
|
51
|
+
private myCache: Map<string, Material> = new Map(); // DON'T DO THIS!
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Architecture Benefits
|
|
56
|
+
|
|
57
|
+
- **Single source of truth**: AssetManager is the only cache
|
|
58
|
+
- **Automatic updates**: Components react when assets load or change
|
|
59
|
+
- **Memory efficient**: One copy of each asset shared across all components
|
|
60
|
+
- **Hot-reload ready**: Asset changes propagate automatically to all subscribers
|
|
61
|
+
- **No manual cache management**: State handles all invalidation and updates
|
|
62
|
+
|
|
63
|
+
## Directory Structure
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
public/art/
|
|
67
|
+
├─ maps/
|
|
68
|
+
│ └─ example/
|
|
69
|
+
│ ├─ map.art
|
|
70
|
+
│ └─ legend.json
|
|
71
|
+
│
|
|
72
|
+
├─ materials/
|
|
73
|
+
│ ├─ brick-wall.art
|
|
74
|
+
│ └─ wooden-door.art
|
|
75
|
+
│
|
|
76
|
+
├─ sprites/
|
|
77
|
+
├─ giant-rat.art
|
|
78
|
+
└─ wolf.art
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## maps/
|
|
82
|
+
|
|
83
|
+
Contains map layouts and legends for game environments. Maps are typically stored as text files with ASCII characters representing different terrain types, while legend files (JSON format) define what each character represents, including visual assets, collision properties, and gameplay entity types.
|
|
84
|
+
|
|
85
|
+
**Contents:**
|
|
86
|
+
|
|
87
|
+
- `example/` - Sample map directory containing:
|
|
88
|
+
- `map.art` - ASCII representation of the map layout
|
|
89
|
+
- `legend.json` - Character-to-entity mapping with visual assets, collision, and behavior definitions
|
|
90
|
+
|
|
91
|
+
See [maps/README.md](maps/README.md) for detailed information on map file format, legend properties, and the entity/variant system.
|
|
92
|
+
|
|
93
|
+
## materials/
|
|
94
|
+
|
|
95
|
+
Includes ASCII representations of various materials and textures that can be used in game environments. Materials define layered visual representations at different distances (here, near, middle, far) and can include proximity-based sound effects and ambient events.
|
|
96
|
+
|
|
97
|
+
**Contents:**
|
|
98
|
+
|
|
99
|
+
- `brick-wall.art` - ASCII pattern for brick wall textures
|
|
100
|
+
- `bone.art` - Ground-placed bone material with placement metadata
|
|
101
|
+
- `door-on-brick.art` - Door material overlaid on brick wall background
|
|
102
|
+
- `wireframe-wall.art` - Complex wireframe demonstrating layered perspective rendering
|
|
103
|
+
|
|
104
|
+
See [materials/README.md](materials/README.md) for detailed information on material file format, layer system, placement properties, and the relationship between materials and legend entities.
|
|
105
|
+
|
|
106
|
+
## sprites/
|
|
107
|
+
|
|
108
|
+
Stores animated sprite files for characters, creatures, and other game entities. Sprites support multiple frames with configurable timing and can be referenced by legend entities for dynamic visual representation.
|
|
109
|
+
|
|
110
|
+
**Contents:**
|
|
111
|
+
|
|
112
|
+
- `giant-rat.art` - ASCII sprite for giant rat creature
|
|
113
|
+
- `wolf.art` - ASCII sprite for wolf creature
|
|
114
|
+
|
|
115
|
+
Sprites are referenced in map legends via the `asset` property and can be combined with entity types to define interactive game objects.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
§ {"loop":true,"duration":120}
|
|
2
|
+
_ _ _ _
|
|
3
|
+
__ _ ___ ___(_|_) |_ ___ _ __(_)_ _ _ __ ___
|
|
4
|
+
/ _` / __|/ __| | | __/ _ \| '__| | | | | '_ ` _ \
|
|
5
|
+
| (_| \__ \ (__| | | || (_) | | | | |_| | | | | | |
|
|
6
|
+
\__,_|___/\___|_|_|\__\___/|_| |_|\__,_|_| |_| |_|
|
|
7
|
+
¶ {"duration":8000}
|
|
8
|
+
_ _ _
|
|
9
|
+
__ _ ___ ___(_| )| _ ___ _ __(_)_ _ _ __ .
|
|
10
|
+
/ _` / __|/ __| | | __/ _ \| '__| | | | | '_ ` _ \.
|
|
11
|
+
| (_| \__ \ (__| | | || (_) | | | | |_| | | | | | |
|
|
12
|
+
\__,_|___/\___|_|_|\__\___/|_| |_|\__,_|_| |_| |_|
|
|
13
|
+
¶ {"duration":1000}
|
|
14
|
+
. _ * _ * .
|
|
15
|
+
__ _ ___ ___(_|_).|_ * _ __(_)_ _ * __
|
|
16
|
+
/ _` / __|/ _ . | __/ . \ '__| | | | ' ` \ .
|
|
17
|
+
| (_| \__ (__| *|.| || (_) | * | | |_| *| | | │ .
|
|
18
|
+
\__,_|___/\__ ._|_|\__\ * /|_| .|\__ |_| |_│
|
|
19
|
+
¶ {"duration":140}
|
|
20
|
+
* _ * . * . *
|
|
21
|
+
__ * (_|_) . * _ __ . . __
|
|
22
|
+
/ ` __| | __/ * \ '__| * ` \
|
|
23
|
+
| * * \__ | | || (_) | . | * * | | │
|
|
24
|
+
. .|___/ |_| \__\___/|_| . . |_| . |_│
|
|
25
|
+
¶ {"duration":140}
|
|
26
|
+
. * . _ * . . .
|
|
27
|
+
__ * * (_|_). . * _ * . __
|
|
28
|
+
/ _` * __| | __/ * * \ '__| . * _ \
|
|
29
|
+
| (_| * \__ | || (_) . | * . * | | │
|
|
30
|
+
. ._|___/ |_| \__\ * |_| . * |_| |_│
|
|
31
|
+
¶ {"duration":160}
|
|
32
|
+
. * . _ * . * . .
|
|
33
|
+
* . (_|_) . . * . * .
|
|
34
|
+
/ ` * __| __/ * . * . * . \
|
|
35
|
+
| * * \__ | (_)* . * . * . | │
|
|
36
|
+
. .|___/ |_| * . * . . |_|
|
|
37
|
+
¶ {"duration":140}
|
|
38
|
+
. * . . * . .
|
|
39
|
+
. . . * . * .
|
|
40
|
+
* . . . * .
|
|
41
|
+
. * . . . *
|
|
42
|
+
. . * . .
|
|
43
|
+
¶ {"duration":240}
|
|
44
|
+
. * . . * .
|
|
45
|
+
. . . * . *
|
|
46
|
+
. * . . . *
|
|
47
|
+
. . * . . .
|
|
48
|
+
* . . * .
|
|
49
|
+
¶ {"duration":240}
|
|
50
|
+
. * . . *
|
|
51
|
+
. . . * .
|
|
52
|
+
. * . . .
|
|
53
|
+
. . * . .
|
|
54
|
+
* . . . *
|
|
55
|
+
¶ {"duration":240}
|
|
56
|
+
|
|
57
|
+
. . * . .
|
|
58
|
+
. * . .
|
|
59
|
+
. . * .
|
|
60
|
+
* . . .
|
|
61
|
+
¶ {"duration":240}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
. . * .
|
|
65
|
+
. * . .
|
|
66
|
+
* . . .
|
|
67
|
+
¶ {"duration":240}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
. * .
|
|
72
|
+
* . .
|
|
73
|
+
¶ {"duration":260}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
. .
|
|
79
|
+
¶ {"duration":280}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
¶ {"duration":1000}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
_ _ _ _
|
|
91
|
+
¶ {"duration":140}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
_ _ _ _
|
|
97
|
+
__ _ ___ ___(_|_) |_ ___ _ __(_)_ _ _ __ ___
|
|
98
|
+
¶ {"duration":140}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
_ _ _ _
|
|
103
|
+
__ _ ___ ___(_|_) |_ ___ _ __(_)_ _ _ __ ___
|
|
104
|
+
╱ _` ╱ __|╱ __| | | __╱ _ ╲| '__| | | | | '_ ` _ ╲
|
|
105
|
+
¶ {"duration":140}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
_ _ _ _
|
|
109
|
+
__ _ ___ ___(_|_) |_ ___ _ __(_)_ _ _ __ ___
|
|
110
|
+
/ _` / __|/ __| | | __/ _ \| '__| | | | | '_ ` _ \
|
|
111
|
+
| (_| \__ \ (__| | | || (_) | | | | |_| | | | | | |
|