sci2-ts 0.2.2 → 0.3.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/CHANGELOG.md +18 -5
- package/lib/993.sc +129 -0
- package/lib/995.sc +58 -5
- package/lib/996.sc +84 -26
- package/lib/997.sc +26 -0
- package/lib/998.sc +4 -2
- package/lib/999.sc +4 -1
- package/lib/resources.js +33 -3
- package/lib/resources.ts +34 -3
- package/lib/system.sh +22 -1
- package/package.json +1 -1
- package/packages/content/src/room.js +13 -1
- package/packages/content/src/room.ts +11 -1
- package/packages/content/src/target.d.ts +5 -0
- package/packages/content/src/target.ts +5 -0
- package/packages/sci/src/vm/kernels/text.js +27 -2
- package/packages/sci/src/vm/kernels/text.ts +23 -2
- package/tools/art/build.d.ts +10 -0
- package/tools/art/build.js +32 -2
- package/tools/art/build.ts +28 -2
- package/tools/art/font.d.ts +24 -0
- package/tools/art/font.js +40 -0
- package/tools/art/font.ts +56 -0
- package/tools/game/build.d.ts +4 -0
- package/tools/game/build.js +27 -1
- package/tools/game/build.ts +24 -1
- package/tools/game/kit.d.ts +1 -0
- package/tools/game/kit.js +1 -0
- package/tools/game/kit.ts +1 -0
- package/tools/game/target.d.ts +1 -1
- package/tools/game/target.js +5 -3
- package/tools/game/target.ts +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to this project.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
All notable changes to this project. Versions follow [Semantic Versioning](https://semver.org/),
|
|
4
|
+
and the entries are generated from commit messages
|
|
5
|
+
([Conventional Commits](https://www.conventionalcommits.org/)) when a release is made.
|
|
6
|
+
|
|
7
|
+
## [0.3.0](https://github.com/rlueder/sci-ts/compare/v0.2.2...v0.3.0) (2026-10-01)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* **content:** close-ups from Yarn ([0546752](https://github.com/rlueder/sci-ts/commit/054675216342747bc5565301aff3d39d27a900c2))
|
|
12
|
+
* **lib:** a text style for every box and menu ([0350a4e](https://github.com/rlueder/sci-ts/commit/0350a4e28a57d951277372ff6144bd8e9e534bee))
|
|
13
|
+
* **lib:** an inventory and close-ups ([6d401d8](https://github.com/rlueder/sci-ts/commit/6d401d8f92d33a4b694dcc617adbb1d5878da246))
|
|
14
|
+
* **lib:** cursors a game can replace, and a wait cursor ([aa55d11](https://github.com/rlueder/sci-ts/commit/aa55d1142568aeb4b14681e44881a4223192d5d2))
|
|
15
|
+
* **lib:** portraits at the top left, with blinking eyes ([8fd6aa1](https://github.com/rlueder/sci-ts/commit/8fd6aa16e83f9608666f67fd998ac852be1a5a99))
|
|
16
|
+
* **tools:** fonts from sheets of glyphs drawn in a paint program ([cb13669](https://github.com/rlueder/sci-ts/commit/cb13669363fb419425b0a54f0a5afb9ca1576dcf))
|
|
17
|
+
|
|
18
|
+
### Maintenance
|
|
19
|
+
|
|
20
|
+
* release from CI with semantic-release ([1757412](https://github.com/rlueder/sci-ts/commit/17574126c257961dd1058f596c6174445f3d9f16))
|
|
6
21
|
|
|
7
22
|
## [0.2.2] - 2026-10-01
|
|
8
23
|
|
|
@@ -74,5 +89,3 @@ from commit messages ([Conventional Commits](https://www.conventionalcommits.org
|
|
|
74
89
|
|
|
75
90
|
- Workspace, typecheck, tests and CI
|
|
76
91
|
- Build every game, and publish the site to GitHub Pages by hand
|
|
77
|
-
|
|
78
|
-
|
package/lib/993.sc
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
;;; Things: what the hero carries and the window to choose one from, and close looks at
|
|
2
|
+
;;; things.
|
|
3
|
+
(script 993)
|
|
4
|
+
(include "system.sh")
|
|
5
|
+
|
|
6
|
+
;; Something the hero can carry. Its view's loop 0 is its icon in the inventory window and
|
|
7
|
+
;; loop 1 the cursor while it's being used (the cel's anchor is the hotspot); both anchored
|
|
8
|
+
;; at the top-left corner. Using it on something sends that thing doVerb: with the item's
|
|
9
|
+
;; own verb, 10 and up, so the room answers with noun and that verb (in Yarn, a node like
|
|
10
|
+
;; filings.lens: the game names its items' verbs in items.yaml).
|
|
11
|
+
(class InvItem of Obj
|
|
12
|
+
(properties
|
|
13
|
+
view -1
|
|
14
|
+
verb 0
|
|
15
|
+
description 0) ; what looking at it in the window says
|
|
16
|
+
|
|
17
|
+
;; Looked at in the inventory window.
|
|
18
|
+
(method (doVerb v)
|
|
19
|
+
(if (and (== v V_LOOK) description) (narrator say: description))))
|
|
20
|
+
|
|
21
|
+
;; What the hero carries (the `inventory` global): (inventory add: lens), (inventory
|
|
22
|
+
;; contains: lens). showSelf opens the window: the items in a row, one click picks one to
|
|
23
|
+
;; use (it becomes the cursor), a right-click on one looks at it, a click elsewhere closes.
|
|
24
|
+
(class Inventory of Set
|
|
25
|
+
(properties
|
|
26
|
+
window 0
|
|
27
|
+
icons 0
|
|
28
|
+
y 24)
|
|
29
|
+
|
|
30
|
+
(method (showSelf &tmp node item icon x)
|
|
31
|
+
(if (not size)
|
|
32
|
+
(narrator say: "You aren't carrying anything.")
|
|
33
|
+
(return))
|
|
34
|
+
(= window
|
|
35
|
+
((TextItem new:)
|
|
36
|
+
text: ""
|
|
37
|
+
width: (+ (* size (+ INV_ICON INV_GAP)) INV_GAP (* 2 (textStyle margin?)))
|
|
38
|
+
height: (+ INV_ICON (* 2 INV_GAP) (* 2 (textStyle margin?)))
|
|
39
|
+
yourself:))
|
|
40
|
+
(if (!= (textStyle frame?) -1)
|
|
41
|
+
(window
|
|
42
|
+
width: (+ (window width?) (CelWide (textStyle frame?) 0 6) (CelWide (textStyle frame?) 0 7))
|
|
43
|
+
height: (+ (window height?) (CelHigh (textStyle frame?) 0 4) (CelHigh (textStyle frame?) 0 5))))
|
|
44
|
+
(window x: (/ (- SCREEN_WIDTH (window width?)) 2) y: y init:)
|
|
45
|
+
(= icons (List new:))
|
|
46
|
+
(= x (+ (window x?) (window textLeft?) INV_GAP))
|
|
47
|
+
(for ((= node (FirstNode elements))) node ((= node (NextNode node)))
|
|
48
|
+
(= item (NodeValue node))
|
|
49
|
+
;; The icon keeps its item in `actions`.
|
|
50
|
+
(= icon ((View new:) view: (item view?) loop: 0 cel: 0 actions: item x: x y: (+ (window y?) (window textTop?) INV_GAP) yourself:))
|
|
51
|
+
(icon init:)
|
|
52
|
+
(icon plane: uiPlane setPri: (+ (window priority?) 1))
|
|
53
|
+
(icons add: icon)
|
|
54
|
+
(+= x (+ INV_ICON INV_GAP)))
|
|
55
|
+
(= dialog self))
|
|
56
|
+
|
|
57
|
+
;; The item whose icon is at (x, y), or 0.
|
|
58
|
+
(method (itemAt ex ey &tmp icon)
|
|
59
|
+
(= icon (icons firstTrue: #onMe ex ey))
|
|
60
|
+
(return (if icon (icon actions?) else 0)))
|
|
61
|
+
|
|
62
|
+
(method (handleEvent event &tmp item)
|
|
63
|
+
(event claimed: TRUE)
|
|
64
|
+
(if (!= (event type?) EV_MOUSE_DOWN) (return TRUE))
|
|
65
|
+
(= item (self itemAt: (event x?) (event y?)))
|
|
66
|
+
(cond
|
|
67
|
+
((not item) (self hide:))
|
|
68
|
+
((& (event modifiers?) MOD_RIGHT) (item doVerb: V_LOOK))
|
|
69
|
+
(else
|
|
70
|
+
(self hide:)
|
|
71
|
+
(user useItem: item)))
|
|
72
|
+
(return TRUE))
|
|
73
|
+
|
|
74
|
+
(method (hide)
|
|
75
|
+
(if (== dialog self) (= dialog 0))
|
|
76
|
+
(if icons
|
|
77
|
+
(icons eachElementDo: #dispose)
|
|
78
|
+
(icons dispose:)
|
|
79
|
+
(= icons 0))
|
|
80
|
+
(if window (window dispose:) (= window 0)))
|
|
81
|
+
|
|
82
|
+
(method (delete item)
|
|
83
|
+
(if (== theItem item) (user useItem: 0))
|
|
84
|
+
(super delete: item &rest)))
|
|
85
|
+
|
|
86
|
+
;; A close look at something: a view shown in the middle of the screen with the room dimmed
|
|
87
|
+
;; behind it, until a click; then it goes, and whoever asked is cued.
|
|
88
|
+
;; ((CloseUp new:) show: view [loop [cel [whoCares]]])
|
|
89
|
+
;; Its cels are anchored at their top-left corner. The dimming is the library's view 996,
|
|
90
|
+
;; drawn in remap colour 253 (which the art palette keeps free) at `dim` percent.
|
|
91
|
+
(class CloseUp of Obj
|
|
92
|
+
(properties
|
|
93
|
+
shade 0
|
|
94
|
+
picture 0
|
|
95
|
+
caller 0
|
|
96
|
+
dim 50)
|
|
97
|
+
|
|
98
|
+
(method (show v l c whoCares &tmp lp cl)
|
|
99
|
+
(= lp (if (> argc 1) l else 0))
|
|
100
|
+
(= cl (if (> argc 2) c else 0))
|
|
101
|
+
(= caller (if (> argc 3) whoCares else 0))
|
|
102
|
+
(RemapColors REMAP_BY_PERCENT 253 dim)
|
|
103
|
+
(= shade ((View new:) view: CLOSE_UP_SHADE x: 0 y: 0 yourself:))
|
|
104
|
+
(shade init:)
|
|
105
|
+
(shade plane: uiPlane setPri: 10)
|
|
106
|
+
(= picture
|
|
107
|
+
((View new:)
|
|
108
|
+
view: v loop: lp cel: cl
|
|
109
|
+
x: (/ (- SCREEN_WIDTH (CelWide v lp cl)) 2)
|
|
110
|
+
y: (/ (- SCREEN_HEIGHT (CelHigh v lp cl)) 2)
|
|
111
|
+
yourself:))
|
|
112
|
+
(picture init:)
|
|
113
|
+
(picture plane: uiPlane setPri: 20)
|
|
114
|
+
(= dialog self))
|
|
115
|
+
|
|
116
|
+
(method (handleEvent event)
|
|
117
|
+
(event claimed: TRUE)
|
|
118
|
+
(if (== (event type?) EV_MOUSE_DOWN)
|
|
119
|
+
(self close:))
|
|
120
|
+
(return TRUE))
|
|
121
|
+
|
|
122
|
+
(method (close &tmp c)
|
|
123
|
+
(if (== dialog self) (= dialog 0))
|
|
124
|
+
(if picture (picture dispose:) (= picture 0))
|
|
125
|
+
(if shade (shade dispose:) (= shade 0))
|
|
126
|
+
(RemapColors REMAP_OFF 253)
|
|
127
|
+
(= c caller)
|
|
128
|
+
(self dispose:)
|
|
129
|
+
(if c (c cue:))))
|
package/lib/995.sc
CHANGED
|
@@ -15,22 +15,69 @@
|
|
|
15
15
|
;; a click walks the hero there, or does the verb to what's under the cursor: an actor or
|
|
16
16
|
;; prop, a feature, or else the room. A click while a line is shown dismisses it; while a
|
|
17
17
|
;; menu is open, it goes to the menu.
|
|
18
|
+
;;
|
|
19
|
+
;; The cursor is a view for each verb, and another while the player can't act (handsOff).
|
|
20
|
+
;; A game with its own sets them in its init: (user walkCursor: 261 waitCursor: 265 ...).
|
|
21
|
+
;; I or Tab opens the inventory; an item picked there is used like a verb (V_ITEM), with
|
|
22
|
+
;; its own cursor, until right-click goes back to walking.
|
|
18
23
|
(class User of Obj
|
|
19
24
|
(properties
|
|
20
25
|
verb V_WALK
|
|
21
|
-
canInput TRUE
|
|
26
|
+
canInput TRUE
|
|
27
|
+
lookCursor 991 ; the library's: CURSOR_BASE + verb
|
|
28
|
+
talkCursor 992
|
|
29
|
+
walkCursor 993
|
|
30
|
+
doCursor 994
|
|
31
|
+
waitCursor 995 ; CURSOR_WAIT
|
|
32
|
+
cursor -1 ; the view and loop showing now
|
|
33
|
+
cursorLoop 0)
|
|
22
34
|
|
|
23
35
|
(method (init)
|
|
24
36
|
(self setVerb: verb))
|
|
25
37
|
|
|
26
38
|
(method (setVerb v)
|
|
27
39
|
(= verb v)
|
|
28
|
-
(
|
|
40
|
+
(self showCursor:))
|
|
41
|
+
|
|
42
|
+
;; Picks an item to use (0: none).
|
|
43
|
+
(method (useItem item)
|
|
44
|
+
(= theItem item)
|
|
45
|
+
(cond
|
|
46
|
+
(item (self setVerb: V_ITEM))
|
|
47
|
+
((== verb V_ITEM) (self setVerb: V_WALK))))
|
|
48
|
+
|
|
49
|
+
;; The cursor for a verb (0: waiting).
|
|
50
|
+
(method (cursorFor v)
|
|
51
|
+
(switch v
|
|
52
|
+
(V_LOOK (return lookCursor))
|
|
53
|
+
(V_TALK (return talkCursor))
|
|
54
|
+
(V_WALK (return walkCursor))
|
|
55
|
+
(V_DO (return doCursor))
|
|
56
|
+
(else (return waitCursor))))
|
|
57
|
+
|
|
58
|
+
(method (showCursor &tmp v l)
|
|
59
|
+
(= l 0)
|
|
60
|
+
(cond
|
|
61
|
+
((not canInput) (= v waitCursor))
|
|
62
|
+
((and (== verb V_ITEM) theItem) (= v (theItem view?)) (= l 1))
|
|
63
|
+
(else (= v (self cursorFor: verb))))
|
|
64
|
+
(if (or (!= v cursor) (!= l cursorLoop))
|
|
65
|
+
(= cursor v)
|
|
66
|
+
(= cursorLoop l)
|
|
67
|
+
(SetCursor v l 0)))
|
|
29
68
|
|
|
30
69
|
(method (doit)
|
|
70
|
+
(self showCursor:)
|
|
31
71
|
(while (GetEvent EV_ALL theEvent)
|
|
32
72
|
(self handleEvent: theEvent)))
|
|
33
73
|
|
|
74
|
+
;; The verb right-click goes to: walk, do, look, talk, the item picked (if any), walk.
|
|
75
|
+
(method (nextVerb)
|
|
76
|
+
(cond
|
|
77
|
+
((== verb V_ITEM) (return V_WALK))
|
|
78
|
+
((and (== verb V_TALK) theItem) (return V_ITEM))
|
|
79
|
+
(else (return (+ (mod verb VERB_COUNT) 1)))))
|
|
80
|
+
|
|
34
81
|
(method (handleEvent event &tmp obj ex ey)
|
|
35
82
|
(event claimed: FALSE)
|
|
36
83
|
;; A line being shown, then an open menu, get clicks first.
|
|
@@ -38,9 +85,15 @@
|
|
|
38
85
|
(if dialog
|
|
39
86
|
(dialog handleEvent: event)
|
|
40
87
|
(return))
|
|
41
|
-
(if (
|
|
88
|
+
(if (not canInput) (return))
|
|
89
|
+
(if (and (== (event type?) EV_KEY_DOWN)
|
|
90
|
+
(or (== (event message?) KEY_TAB) (== (event message?) KEY_i) (== (event message?) KEY_I)))
|
|
91
|
+
(event claimed: TRUE)
|
|
92
|
+
(inventory showSelf:)
|
|
93
|
+
(return))
|
|
94
|
+
(if (!= (event type?) EV_MOUSE_DOWN) (return))
|
|
42
95
|
(if (& (event modifiers?) MOD_RIGHT)
|
|
43
|
-
(self setVerb: (
|
|
96
|
+
(self setVerb: (self nextVerb:))
|
|
44
97
|
(return))
|
|
45
98
|
(= ex (event x?))
|
|
46
99
|
(= ey (event y?))
|
|
@@ -50,4 +103,4 @@
|
|
|
50
103
|
(= obj (cast firstTrue: #onMe ex ey))
|
|
51
104
|
(if (not obj) (= obj (features firstTrue: #onMe ex ey)))
|
|
52
105
|
(if (not obj) (= obj curRoom))
|
|
53
|
-
(if obj (obj doVerb: verb))))
|
|
106
|
+
(if obj (obj doVerb: (if (== verb V_ITEM) (theItem verb?) else verb)))))
|
package/lib/996.sc
CHANGED
|
@@ -3,8 +3,25 @@
|
|
|
3
3
|
(script 996)
|
|
4
4
|
(include "system.sh")
|
|
5
5
|
|
|
6
|
+
;; How text boxes and menus look, for the whole game (the `textStyle` global): the font, the
|
|
7
|
+
;; ink, paper and border colours, and a frame drawn around each box instead of the border.
|
|
8
|
+
;; The game sets it in its init, after (super init:):
|
|
9
|
+
;; (textStyle font: 1 fore: 3 back: 12 frame: 260)
|
|
10
|
+
;; A frame is a view whose loop 0 has eight cels, each anchored at its top-left corner: the
|
|
11
|
+
;; corners (top-left, top-right, bottom-left, bottom-right), then the edges (top, bottom, left,
|
|
12
|
+
;; right). The edges are tiled between the corners; transparent pixels show the paper.
|
|
13
|
+
(class TextStyle of Obj
|
|
14
|
+
(properties
|
|
15
|
+
font 0
|
|
16
|
+
fore 0 ; ink
|
|
17
|
+
back 255 ; paper
|
|
18
|
+
border 0 ; a one-pixel border's colour; -1: none
|
|
19
|
+
frame -1 ; a frame view, or -1
|
|
20
|
+
margin 4)) ; between the text and the border or frame
|
|
21
|
+
|
|
6
22
|
;; A box of text over the room: drawn by the interpreter into a bitmap, shown as a screen
|
|
7
|
-
;; item in the UI plane. Set text (and
|
|
23
|
+
;; item in the UI plane. Set text (and width, x, y), then init; it's as tall as the text
|
|
24
|
+
;; unless height is set. Font, colours and frame come from the textStyle unless set here.
|
|
8
25
|
(class TextItem of Obj
|
|
9
26
|
(properties
|
|
10
27
|
x 0 y 0 z 0
|
|
@@ -14,36 +31,66 @@
|
|
|
14
31
|
bitmap 0
|
|
15
32
|
scaleSignal 0 scaleX 128 scaleY 128
|
|
16
33
|
text 0
|
|
17
|
-
font
|
|
18
|
-
fore
|
|
19
|
-
back
|
|
34
|
+
font -1 ; -1 here and below: the textStyle's
|
|
35
|
+
fore -1
|
|
36
|
+
back -1
|
|
20
37
|
skip 254
|
|
21
38
|
mode 0 ; 0 left, 1 centred
|
|
22
|
-
borderColor
|
|
39
|
+
borderColor -2 ; -1: none
|
|
40
|
+
frame -2 ; -1: none
|
|
23
41
|
textLeft 0 textTop 0 textRight -1 textBottom -1
|
|
24
42
|
width 200
|
|
25
43
|
height 0)
|
|
26
44
|
|
|
27
|
-
(method (init &tmp r)
|
|
28
|
-
|
|
29
|
-
(
|
|
30
|
-
(
|
|
31
|
-
(
|
|
32
|
-
(
|
|
33
|
-
|
|
34
|
-
(=
|
|
35
|
-
(=
|
|
36
|
-
(
|
|
45
|
+
(method (init &tmp r m padL padT padR padB)
|
|
46
|
+
(if (== font -1) (= font (textStyle font?)))
|
|
47
|
+
(if (== fore -1) (= fore (textStyle fore?)))
|
|
48
|
+
(if (== back -1) (= back (textStyle back?)))
|
|
49
|
+
(if (== frame -2) (= frame (textStyle frame?)))
|
|
50
|
+
(if (== borderColor -2) (= borderColor (if (!= frame -1) -1 else (textStyle border?))))
|
|
51
|
+
;; As tall as the text needs, inside the margin and the frame.
|
|
52
|
+
(= m (textStyle margin?))
|
|
53
|
+
(= padL m) (= padT m) (= padR m) (= padB m)
|
|
54
|
+
(if (!= frame -1)
|
|
55
|
+
(+= padL (CelWide frame 0 6))
|
|
56
|
+
(+= padR (CelWide frame 0 7))
|
|
57
|
+
(+= padT (CelHigh frame 0 4))
|
|
58
|
+
(+= padB (CelHigh frame 0 5)))
|
|
59
|
+
(if (not height)
|
|
60
|
+
(= r (Array ARRAY_NEW 4 0))
|
|
61
|
+
(TextSize r text font (- width (+ padL padR)))
|
|
62
|
+
(= height (+ (Array ARRAY_AT r 3) 1 padT padB))
|
|
63
|
+
(Array ARRAY_FREE r))
|
|
64
|
+
(= textLeft padL)
|
|
65
|
+
(= textTop padT)
|
|
66
|
+
(= textRight (- width (+ padR 1)))
|
|
67
|
+
(= textBottom (- height (+ padB 1)))
|
|
37
68
|
(= plane uiPlane)
|
|
38
69
|
(= bitmap (CreateTextBitmap 0 width height self))
|
|
70
|
+
(if (!= frame -1) (self drawFrame:))
|
|
39
71
|
(AddScreenItem self))
|
|
40
72
|
|
|
73
|
+
;; The edges, tiled, then the corners over their ends.
|
|
74
|
+
(method (drawFrame &tmp i step)
|
|
75
|
+
(if (< (= step (CelWide frame 0 4)) 1) (= step 1))
|
|
76
|
+
(for ((= i (CelWide frame 0 0))) (< i width) ((+= i step))
|
|
77
|
+
(Bitmap BITMAP_DRAW_VIEW bitmap frame 0 4 i 0)
|
|
78
|
+
(Bitmap BITMAP_DRAW_VIEW bitmap frame 0 5 i (- height (CelHigh frame 0 5))))
|
|
79
|
+
(if (< (= step (CelHigh frame 0 6)) 1) (= step 1))
|
|
80
|
+
(for ((= i (CelHigh frame 0 0))) (< i height) ((+= i step))
|
|
81
|
+
(Bitmap BITMAP_DRAW_VIEW bitmap frame 0 6 0 i)
|
|
82
|
+
(Bitmap BITMAP_DRAW_VIEW bitmap frame 0 7 (- width (CelWide frame 0 7)) i))
|
|
83
|
+
(Bitmap BITMAP_DRAW_VIEW bitmap frame 0 0 0 0)
|
|
84
|
+
(Bitmap BITMAP_DRAW_VIEW bitmap frame 0 1 (- width (CelWide frame 0 1)) 0)
|
|
85
|
+
(Bitmap BITMAP_DRAW_VIEW bitmap frame 0 2 0 (- height (CelHigh frame 0 2)))
|
|
86
|
+
(Bitmap BITMAP_DRAW_VIEW bitmap frame 0 3 (- width (CelWide frame 0 3)) (- height (CelHigh frame 0 3))))
|
|
87
|
+
|
|
41
88
|
(method (onMe theX theY)
|
|
42
89
|
(return (and (>= theX x) (< theX (+ x width)) (>= theY y) (< theY (+ y height)))))
|
|
43
90
|
|
|
44
91
|
(method (dispose)
|
|
45
92
|
(DeleteScreenItem self)
|
|
46
|
-
(if bitmap (Bitmap
|
|
93
|
+
(if bitmap (Bitmap BITMAP_DISPOSE bitmap) (= bitmap 0))
|
|
47
94
|
(super dispose:)))
|
|
48
95
|
|
|
49
96
|
;; Shows a line until it's been there long enough to read, or the player clicks; then
|
|
@@ -53,7 +100,7 @@
|
|
|
53
100
|
caller 0
|
|
54
101
|
box 0
|
|
55
102
|
until 0 ; the game time it goes
|
|
56
|
-
font
|
|
103
|
+
font -1 ; -1: the textStyle's
|
|
57
104
|
x -1 ; -1: centred
|
|
58
105
|
y 16
|
|
59
106
|
width 220)
|
|
@@ -106,14 +153,16 @@
|
|
|
106
153
|
(= line (String 11 "%s: %s" name txt))
|
|
107
154
|
(super say: line &rest whoCares)))
|
|
108
155
|
|
|
109
|
-
;; Someone with a portrait: a view whose loop 0 is the bust, loop 1 the mouth
|
|
110
|
-
;;
|
|
111
|
-
;;
|
|
112
|
-
;;
|
|
156
|
+
;; Someone with a portrait: a view whose loop 0 is the bust, loop 1 the mouth and loop 2 the
|
|
157
|
+
;; eyes, every cel the same size and anchored at its top-left corner. The room's talker hands
|
|
158
|
+
;; its parts to init: mouth bust eyes frame, where frame is drawn first (the bust), placed
|
|
159
|
+
;; where the portrait goes (the top left of the screen). The text goes to its right. While
|
|
160
|
+
;; the line is said the mouth moves, then closes; the eyes blink.
|
|
113
161
|
(class PortraitTalker of Talker
|
|
114
162
|
(properties
|
|
115
163
|
mouth 0 bust 0 eyes 0 frame 0
|
|
116
|
-
priority 150
|
|
164
|
+
priority 150
|
|
165
|
+
mouthUntil 0) ; the game time the mouth stops
|
|
117
166
|
|
|
118
167
|
(method (init theMouth theBust theEyes theFrame)
|
|
119
168
|
(= mouth theMouth)
|
|
@@ -125,15 +174,24 @@
|
|
|
125
174
|
(self init:)
|
|
126
175
|
(= portrait (if frame frame else bust))
|
|
127
176
|
(if portrait
|
|
128
|
-
(= x (+ (portrait x?) (
|
|
129
|
-
(= y (
|
|
130
|
-
(= width (- (
|
|
177
|
+
(= x (+ (portrait x?) (CelWide (portrait view?) (portrait loop?) (portrait cel?)) 6))
|
|
178
|
+
(= y (portrait y?))
|
|
179
|
+
(= width (- SCREEN_WIDTH (+ x 6))))
|
|
131
180
|
(super say: txt &rest whoCares)
|
|
132
181
|
(self showPart: frame priority)
|
|
133
182
|
(self showPart: bust priority)
|
|
134
183
|
(self showPart: eyes (+ priority 1))
|
|
135
184
|
(self showPart: mouth (+ priority 1))
|
|
136
|
-
(if
|
|
185
|
+
(if eyes (eyes setCycle: Blink))
|
|
186
|
+
(if mouth
|
|
187
|
+
(mouth setCycle: Forward)
|
|
188
|
+
;; About as long as it takes to say: half a second, and two cycles a letter.
|
|
189
|
+
(= mouthUntil (+ gameTime 30 (* 2 (String STRING_LENGTH txt))))))
|
|
190
|
+
|
|
191
|
+
(method (doit)
|
|
192
|
+
(if (and mouth (mouth cycler?) (>= gameTime mouthUntil))
|
|
193
|
+
(mouth setCycle: 0 setCel: 0))
|
|
194
|
+
(super doit:))
|
|
137
195
|
|
|
138
196
|
(method (showPart part pri)
|
|
139
197
|
(if part
|
package/lib/997.sc
CHANGED
|
@@ -169,6 +169,32 @@
|
|
|
169
169
|
else
|
|
170
170
|
(client cel: c))))
|
|
171
171
|
|
|
172
|
+
;; Eyes: cel 0 (open) most of the time; every two or three seconds, the other cels in turn
|
|
173
|
+
;; (shut) and back to 0.
|
|
174
|
+
(class Blink of Cycle
|
|
175
|
+
(properties
|
|
176
|
+
nextBlink 0)
|
|
177
|
+
|
|
178
|
+
(method (init who)
|
|
179
|
+
(super init: who)
|
|
180
|
+
(self rest:))
|
|
181
|
+
|
|
182
|
+
(method (rest)
|
|
183
|
+
(= nextBlink (+ gameTime 90 (Random 0 90))))
|
|
184
|
+
|
|
185
|
+
(method (doit &tmp c)
|
|
186
|
+
(if (== (client cel?) 0)
|
|
187
|
+
(if (and (>= gameTime nextBlink) (> (self lastCel:) 0))
|
|
188
|
+
(= cycleCnt gameTime)
|
|
189
|
+
(client cel: 1))
|
|
190
|
+
else
|
|
191
|
+
(= c (self nextCel:))
|
|
192
|
+
(if (> c (self lastCel:))
|
|
193
|
+
(client cel: 0)
|
|
194
|
+
(self rest:)
|
|
195
|
+
else
|
|
196
|
+
(client cel: c)))))
|
|
197
|
+
|
|
172
198
|
;; Sizes its client by how far up the screen it stands: frontSize percent at frontY,
|
|
173
199
|
;; backSize at backY, in between in proportion.
|
|
174
200
|
(class Scaler of Obj
|
package/lib/998.sc
CHANGED
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
(= theEvent (Event new:))
|
|
31
31
|
(= messager (Messager new:))
|
|
32
32
|
(= narrator (Narrator new:))
|
|
33
|
+
(= textStyle (TextStyle new:))
|
|
34
|
+
(= inventory (Inventory new:))
|
|
33
35
|
(= music (Sound new:))
|
|
34
36
|
(= sfx (Sound new:))
|
|
35
37
|
(= uiPlane ((Plane new:) priority: 200 picture: -2 yourself:))
|
|
@@ -77,10 +79,10 @@
|
|
|
77
79
|
|
|
78
80
|
;; Clicks do nothing (but dismiss text) until handsOn: for cutscenes.
|
|
79
81
|
(method (handsOff)
|
|
80
|
-
(user canInput: FALSE))
|
|
82
|
+
(user canInput: FALSE showCursor:))
|
|
81
83
|
|
|
82
84
|
(method (handsOn)
|
|
83
|
-
(user canInput: TRUE)))
|
|
85
|
+
(user canInput: TRUE showCursor:)))
|
|
84
86
|
|
|
85
87
|
;; A room: its picture, where you can walk, its exits, and what's said about it.
|
|
86
88
|
(class Room of Obj
|
package/lib/999.sc
CHANGED
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
uiPlane ; the plane over the room, for text
|
|
28
28
|
gameTime ; cycles since the game started
|
|
29
29
|
quit ; set to end the game
|
|
30
|
-
[gameFlags 64]
|
|
30
|
+
[gameFlags 64] ; story flags, a bit each
|
|
31
|
+
textStyle ; how text boxes and menus look (a TextStyle)
|
|
32
|
+
inventory ; what the hero carries (an Inventory)
|
|
33
|
+
theItem) ; the item picked to use, or 0
|
|
31
34
|
|
|
32
35
|
(procedure (SetFlag n)
|
|
33
36
|
(|= [gameFlags (/ n 16)] (<< 1 (mod n 16))))
|
package/lib/resources.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Colour, ResourceType, writeView } from "../tools/game/kit.js";
|
|
2
2
|
/**
|
|
3
|
-
* The class library's own resources: a cursor for each verb, view CURSOR_BASE + verb
|
|
4
|
-
* (system.sh). X is black, W white, . transparent.
|
|
3
|
+
* The class library's own resources: a cursor for each verb, view CURSOR_BASE + verb, and
|
|
4
|
+
* one while the player waits, CURSOR_WAIT (system.sh). X is black, W white, . transparent.
|
|
5
5
|
*/
|
|
6
6
|
const CURSOR_BASE = 990;
|
|
7
|
+
const CURSOR_WAIT = 995;
|
|
7
8
|
const CURSORS = {
|
|
8
9
|
// V_LOOK: an eye.
|
|
9
10
|
1: {
|
|
@@ -70,6 +71,22 @@ const CURSORS = {
|
|
|
70
71
|
"...XXXXXX..",
|
|
71
72
|
],
|
|
72
73
|
},
|
|
74
|
+
// Waiting: an hourglass.
|
|
75
|
+
[CURSOR_WAIT - CURSOR_BASE]: {
|
|
76
|
+
hotspot: "centre",
|
|
77
|
+
art: [
|
|
78
|
+
"XXXXXXXXX",
|
|
79
|
+
"XWWWWWWWX",
|
|
80
|
+
".XWWWWWX.",
|
|
81
|
+
"..XWWWX..",
|
|
82
|
+
"...XWX...",
|
|
83
|
+
"...XWX...",
|
|
84
|
+
"..XWWWX..",
|
|
85
|
+
".XWWWWWX.",
|
|
86
|
+
"XWWWWWWWX",
|
|
87
|
+
"XXXXXXXXX",
|
|
88
|
+
],
|
|
89
|
+
},
|
|
73
90
|
};
|
|
74
91
|
function cursor(art, hotspot) {
|
|
75
92
|
const width = art[0].length, height = art.length;
|
|
@@ -79,6 +96,19 @@ function cursor(art, hotspot) {
|
|
|
79
96
|
const cel = { width, height, displaceX: (width >> 1) - hx, displaceY: height - 1 - hy, skipColor: Colour.Transparent, pixels };
|
|
80
97
|
return writeView({ flags: 1, loops: [{ link: -1, mirror: false, cels: [cel] }], palette: undefined });
|
|
81
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* What a close-up dims the room with (CLOSE_UP_SHADE): the whole screen in colour 253, which
|
|
101
|
+
* CloseUp makes a remap colour that darkens what's under it.
|
|
102
|
+
*/
|
|
103
|
+
const CLOSE_UP_SHADE = 996;
|
|
104
|
+
function shade() {
|
|
105
|
+
const [width, height] = [320, 200];
|
|
106
|
+
const cel = { width, height, displaceX: width >> 1, displaceY: height - 1, skipColor: Colour.Transparent, pixels: new Uint8Array(width * height).fill(253) };
|
|
107
|
+
return writeView({ flags: 1, loops: [{ link: -1, mirror: false, cels: [cel] }], palette: undefined });
|
|
108
|
+
}
|
|
82
109
|
export default function resources() {
|
|
83
|
-
return
|
|
110
|
+
return [
|
|
111
|
+
...Object.entries(CURSORS).map(([verb, c]) => ({ type: ResourceType.View, number: CURSOR_BASE + Number(verb), data: cursor(c.art, c.hotspot) })),
|
|
112
|
+
{ type: ResourceType.View, number: CLOSE_UP_SHADE, data: shade() },
|
|
113
|
+
];
|
|
84
114
|
}
|
package/lib/resources.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Colour, ResourceType, writeView, type ResourceData } from "../tools/game/kit.ts";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The class library's own resources: a cursor for each verb, view CURSOR_BASE + verb
|
|
5
|
-
* (system.sh). X is black, W white, . transparent.
|
|
4
|
+
* The class library's own resources: a cursor for each verb, view CURSOR_BASE + verb, and
|
|
5
|
+
* one while the player waits, CURSOR_WAIT (system.sh). X is black, W white, . transparent.
|
|
6
6
|
*/
|
|
7
7
|
const CURSOR_BASE = 990;
|
|
8
|
+
const CURSOR_WAIT = 995;
|
|
8
9
|
|
|
9
10
|
const CURSORS: Record<number, { art: string[]; hotspot: "tip" | "centre" }> = {
|
|
10
11
|
// V_LOOK: an eye.
|
|
@@ -72,6 +73,22 @@ const CURSORS: Record<number, { art: string[]; hotspot: "tip" | "centre" }> = {
|
|
|
72
73
|
"...XXXXXX..",
|
|
73
74
|
],
|
|
74
75
|
},
|
|
76
|
+
// Waiting: an hourglass.
|
|
77
|
+
[CURSOR_WAIT - CURSOR_BASE]: {
|
|
78
|
+
hotspot: "centre",
|
|
79
|
+
art: [
|
|
80
|
+
"XXXXXXXXX",
|
|
81
|
+
"XWWWWWWWX",
|
|
82
|
+
".XWWWWWX.",
|
|
83
|
+
"..XWWWX..",
|
|
84
|
+
"...XWX...",
|
|
85
|
+
"...XWX...",
|
|
86
|
+
"..XWWWX..",
|
|
87
|
+
".XWWWWWX.",
|
|
88
|
+
"XWWWWWWWX",
|
|
89
|
+
"XXXXXXXXX",
|
|
90
|
+
],
|
|
91
|
+
},
|
|
75
92
|
};
|
|
76
93
|
|
|
77
94
|
function cursor(art: string[], hotspot: "tip" | "centre") {
|
|
@@ -83,6 +100,20 @@ function cursor(art: string[], hotspot: "tip" | "centre") {
|
|
|
83
100
|
return writeView({ flags: 1, loops: [{ link: -1, mirror: false, cels: [cel] }], palette: undefined });
|
|
84
101
|
}
|
|
85
102
|
|
|
103
|
+
/**
|
|
104
|
+
* What a close-up dims the room with (CLOSE_UP_SHADE): the whole screen in colour 253, which
|
|
105
|
+
* CloseUp makes a remap colour that darkens what's under it.
|
|
106
|
+
*/
|
|
107
|
+
const CLOSE_UP_SHADE = 996;
|
|
108
|
+
function shade() {
|
|
109
|
+
const [width, height] = [320, 200];
|
|
110
|
+
const cel = { width, height, displaceX: width >> 1, displaceY: height - 1, skipColor: Colour.Transparent, pixels: new Uint8Array(width * height).fill(253) };
|
|
111
|
+
return writeView({ flags: 1, loops: [{ link: -1, mirror: false, cels: [cel] }], palette: undefined });
|
|
112
|
+
}
|
|
113
|
+
|
|
86
114
|
export default function resources(): ResourceData[] {
|
|
87
|
-
return
|
|
115
|
+
return [
|
|
116
|
+
...Object.entries(CURSORS).map(([verb, c]) => ({ type: ResourceType.View, number: CURSOR_BASE + Number(verb), data: cursor(c.art, c.hotspot) })),
|
|
117
|
+
{ type: ResourceType.View, number: CLOSE_UP_SHADE, data: shade() },
|
|
118
|
+
];
|
|
88
119
|
}
|
package/lib/system.sh
CHANGED
|
@@ -8,15 +8,32 @@
|
|
|
8
8
|
V_WALK
|
|
9
9
|
V_DO)
|
|
10
10
|
(define VERB_COUNT 4)
|
|
11
|
+
(define V_ITEM 5) ; using the item picked in the inventory (theItem)
|
|
11
12
|
|
|
12
|
-
;; Cursors:
|
|
13
|
+
;; Cursors: the library's are views CURSOR_BASE + verb, and CURSOR_WAIT while the player
|
|
14
|
+
;; can't act (a game can bring its own: User).
|
|
13
15
|
(define CURSOR_BASE 990)
|
|
16
|
+
(define CURSOR_WAIT 995)
|
|
17
|
+
|
|
18
|
+
;; The inventory window: icons are INV_ICON pixels square, INV_GAP apart.
|
|
19
|
+
(define INV_ICON 24)
|
|
20
|
+
(define INV_GAP 4)
|
|
21
|
+
|
|
22
|
+
;; Close-ups: the room is dimmed by this view, drawn in a remap colour.
|
|
23
|
+
(define CLOSE_UP_SHADE 996)
|
|
24
|
+
(define REMAP_OFF 0)
|
|
25
|
+
(define REMAP_BY_PERCENT 2)
|
|
14
26
|
|
|
15
27
|
;; Event types, as GetEvent reports them.
|
|
16
28
|
(define EV_NULL 0)
|
|
17
29
|
(define EV_MOUSE_DOWN 1)
|
|
18
30
|
(define EV_MOUSE_UP 2)
|
|
19
31
|
(define EV_KEY_DOWN 4)
|
|
32
|
+
|
|
33
|
+
;; Keys (an event's message).
|
|
34
|
+
(define KEY_TAB 9)
|
|
35
|
+
(define KEY_I 73)
|
|
36
|
+
(define KEY_i 105)
|
|
20
37
|
(define EV_ALL $7fff)
|
|
21
38
|
;; The right button arrives as a click with shift held.
|
|
22
39
|
(define MOD_RIGHT 3)
|
|
@@ -37,6 +54,10 @@
|
|
|
37
54
|
(define ARRAY_FREE 4)
|
|
38
55
|
(define STRING_LENGTH 10)
|
|
39
56
|
|
|
57
|
+
;; Bitmap subfunctions.
|
|
58
|
+
(define BITMAP_DISPOSE 1)
|
|
59
|
+
(define BITMAP_DRAW_VIEW 3)
|
|
60
|
+
|
|
40
61
|
;; DoSound kernel calls.
|
|
41
62
|
(define SND_INIT 6)
|
|
42
63
|
(define SND_DISPOSE 7)
|
package/package.json
CHANGED
|
@@ -537,11 +537,23 @@ class RoomCompiler {
|
|
|
537
537
|
? [{ op: "async", code: send({ global: g.sound }, [["number", [n]], ["loop", [1]], ["play", ["self"]]]) }]
|
|
538
538
|
: [{ op: "code", code: send({ global: g.sound }, [["number", [n]], ["loop", [1]], ["play", []]]) }];
|
|
539
539
|
}
|
|
540
|
+
case "closeup": {
|
|
541
|
+
if (args.length < 1 || args.length > 3)
|
|
542
|
+
fail("expected <<closeup view [loop [cel]]>>");
|
|
543
|
+
if (!this.target.closeUp)
|
|
544
|
+
fail("this game has no close-ups");
|
|
545
|
+
const [view, loop = "0", cel = "0"] = args;
|
|
546
|
+
// A new close-up, shown until the player clicks; then it cues the script.
|
|
547
|
+
return [{ op: "async", code: [
|
|
548
|
+
...send({ cls: this.target.closeUp }, [["new", []]]),
|
|
549
|
+
"pushi #show", "pushi 4", ...pushVal(int(view, "view", 0, 65535)), ...pushVal(int(loop, "loop", 0, 255)), ...pushVal(int(cel, "cel", 0, 255)), "pushSelf", "send 12",
|
|
550
|
+
] }];
|
|
551
|
+
}
|
|
540
552
|
case "room":
|
|
541
553
|
arity(1, "number");
|
|
542
554
|
return [{ op: "code", code: send({ global: g.curRoom }, [["newRoom", [int(args[0], "room", 0, 65535)]]]) }];
|
|
543
555
|
default:
|
|
544
|
-
return fail("unknown command (known: walk, face, wait, hide, show, loop, cel, animate, stop, music, sound, room; and set, if)");
|
|
556
|
+
return fail("unknown command (known: walk, face, wait, hide, show, loop, cel, animate, stop, music, sound, closeup, room; and set, if)");
|
|
545
557
|
}
|
|
546
558
|
}
|
|
547
559
|
/** Loop music n, unless it's what's playing already. */
|
|
@@ -636,11 +636,21 @@ class RoomCompiler {
|
|
|
636
636
|
? [{ op: "async", code: send({ global: g.sound }, [["number", [n]], ["loop", [1]], ["play", ["self"]]]) }]
|
|
637
637
|
: [{ op: "code", code: send({ global: g.sound }, [["number", [n]], ["loop", [1]], ["play", []]]) }];
|
|
638
638
|
}
|
|
639
|
+
case "closeup": {
|
|
640
|
+
if (args.length < 1 || args.length > 3) fail("expected <<closeup view [loop [cel]]>>");
|
|
641
|
+
if (!this.target.closeUp) fail("this game has no close-ups");
|
|
642
|
+
const [view, loop = "0", cel = "0"] = args;
|
|
643
|
+
// A new close-up, shown until the player clicks; then it cues the script.
|
|
644
|
+
return [{ op: "async", code: [
|
|
645
|
+
...send({ cls: this.target.closeUp! }, [["new", []]]),
|
|
646
|
+
"pushi #show", "pushi 4", ...pushVal(int(view, "view", 0, 65535)), ...pushVal(int(loop, "loop", 0, 255)), ...pushVal(int(cel, "cel", 0, 255)), "pushSelf", "send 12",
|
|
647
|
+
] }];
|
|
648
|
+
}
|
|
639
649
|
case "room":
|
|
640
650
|
arity(1, "number");
|
|
641
651
|
return [{ op: "code", code: send({ global: g.curRoom }, [["newRoom", [int(args[0], "room", 0, 65535)]]]) }];
|
|
642
652
|
default:
|
|
643
|
-
return fail("unknown command (known: walk, face, wait, hide, show, loop, cel, animate, stop, music, sound, room; and set, if)");
|
|
653
|
+
return fail("unknown command (known: walk, face, wait, hide, show, loop, cel, animate, stop, music, sound, closeup, room; and set, if)");
|
|
644
654
|
}
|
|
645
655
|
}
|
|
646
656
|
|
|
@@ -59,6 +59,11 @@ export interface Target {
|
|
|
59
59
|
};
|
|
60
60
|
/** The class that cycles a prop's cels forever (<<animate x forever>>, cycle: forward). */
|
|
61
61
|
forwardCycle: string;
|
|
62
|
+
/**
|
|
63
|
+
* Close-ups (<<closeup view [loop [cel]]>>), if the target has them: a class whose new
|
|
64
|
+
* instance takes `show: view loop cel caller` and cues the caller when it's dismissed.
|
|
65
|
+
*/
|
|
66
|
+
closeUp?: string;
|
|
62
67
|
/** Verb number for "said on arrival" messages. */
|
|
63
68
|
narrationVerb: number;
|
|
64
69
|
/** Verb names as used in Yarn node titles (lower case) -> verb numbers. */
|
|
@@ -46,6 +46,11 @@ export interface Target {
|
|
|
46
46
|
teller: { class: string; props: Record<string, number>; verb: number };
|
|
47
47
|
/** The class that cycles a prop's cels forever (<<animate x forever>>, cycle: forward). */
|
|
48
48
|
forwardCycle: string;
|
|
49
|
+
/**
|
|
50
|
+
* Close-ups (<<closeup view [loop [cel]]>>), if the target has them: a class whose new
|
|
51
|
+
* instance takes `show: view loop cel caller` and cues the caller when it's dismissed.
|
|
52
|
+
*/
|
|
53
|
+
closeUp?: string;
|
|
49
54
|
/** Verb number for "said on arrival" messages. */
|
|
50
55
|
narrationVerb: number;
|
|
51
56
|
/** Verb names as used in Yarn node titles (lower case) -> verb numbers. */
|
|
@@ -330,9 +330,34 @@ export const textKernels = {
|
|
|
330
330
|
g.items.delete(obj);
|
|
331
331
|
return text !== original ? 1 : 0;
|
|
332
332
|
},
|
|
333
|
-
Bitmap: (
|
|
334
|
-
|
|
333
|
+
// Bitmap(1, bitmap): dispose. Bitmap(3, bitmap, view, loop, cel, x, y): draw a view's cel
|
|
334
|
+
// into the bitmap with its origin at (x, y), skipping its transparent pixels (SCI2.1's
|
|
335
|
+
// kBitmapDrawView; the class library frames text boxes with it).
|
|
336
|
+
Bitmap: (vm, [subop = 0, ref = 0, ...args]) => {
|
|
337
|
+
const bmp = vm.memory.bitmap(ref);
|
|
338
|
+
if (subop === 1 && bmp)
|
|
335
339
|
vm.memory.free(segmentOf(ref));
|
|
340
|
+
if (subop === 3 && bmp) {
|
|
341
|
+
const [view = 0, loop = 0, celNo = 0, x = 0, y = 0] = args.map(toSigned);
|
|
342
|
+
const found = graphics(vm).cel(view, loop, celNo);
|
|
343
|
+
if (!found)
|
|
344
|
+
return 0;
|
|
345
|
+
const { cel, mirror } = found;
|
|
346
|
+
const left = x - ((cel.width >> 1) - cel.displaceX), top = y - (cel.height - 1 - cel.displaceY);
|
|
347
|
+
for (let cy = 0; cy < cel.height; cy++) {
|
|
348
|
+
const by = top + cy;
|
|
349
|
+
if (by < 0 || by >= bmp.height)
|
|
350
|
+
continue;
|
|
351
|
+
for (let cx = 0; cx < cel.width; cx++) {
|
|
352
|
+
const bx = left + cx;
|
|
353
|
+
if (bx < 0 || bx >= bmp.width)
|
|
354
|
+
continue;
|
|
355
|
+
const c = cel.pixels[cy * cel.width + (mirror ? cel.width - 1 - cx : cx)];
|
|
356
|
+
if (c !== cel.skipColor)
|
|
357
|
+
bmp.pixels[by * bmp.width + bx] = c;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
336
361
|
return 0;
|
|
337
362
|
},
|
|
338
363
|
};
|
|
@@ -321,8 +321,29 @@ export const textKernels: Record<string, KernelFn> = {
|
|
|
321
321
|
return text !== original ? 1 : 0;
|
|
322
322
|
},
|
|
323
323
|
|
|
324
|
-
Bitmap: (
|
|
325
|
-
|
|
324
|
+
// Bitmap(1, bitmap): dispose. Bitmap(3, bitmap, view, loop, cel, x, y): draw a view's cel
|
|
325
|
+
// into the bitmap with its origin at (x, y), skipping its transparent pixels (SCI2.1's
|
|
326
|
+
// kBitmapDrawView; the class library frames text boxes with it).
|
|
327
|
+
Bitmap: (vm, [subop = 0, ref = 0, ...args]) => {
|
|
328
|
+
const bmp = vm.memory.bitmap(ref);
|
|
329
|
+
if (subop === 1 && bmp) vm.memory.free(segmentOf(ref));
|
|
330
|
+
if (subop === 3 && bmp) {
|
|
331
|
+
const [view = 0, loop = 0, celNo = 0, x = 0, y = 0] = args.map(toSigned);
|
|
332
|
+
const found = graphics(vm).cel(view, loop, celNo);
|
|
333
|
+
if (!found) return 0;
|
|
334
|
+
const { cel, mirror } = found;
|
|
335
|
+
const left = x - ((cel.width >> 1) - cel.displaceX), top = y - (cel.height - 1 - cel.displaceY);
|
|
336
|
+
for (let cy = 0; cy < cel.height; cy++) {
|
|
337
|
+
const by = top + cy;
|
|
338
|
+
if (by < 0 || by >= bmp.height) continue;
|
|
339
|
+
for (let cx = 0; cx < cel.width; cx++) {
|
|
340
|
+
const bx = left + cx;
|
|
341
|
+
if (bx < 0 || bx >= bmp.width) continue;
|
|
342
|
+
const c = cel.pixels[cy * cel.width + (mirror ? cel.width - 1 - cx : cx)]!;
|
|
343
|
+
if (c !== cel.skipColor) bmp.pixels[by * bmp.width + bx] = c;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
326
347
|
return 0;
|
|
327
348
|
},
|
|
328
349
|
};
|
package/tools/art/build.d.ts
CHANGED
|
@@ -24,6 +24,16 @@ export interface ArtManifest {
|
|
|
24
24
|
mirror: boolean;
|
|
25
25
|
})[];
|
|
26
26
|
}[];
|
|
27
|
+
/** Fonts drawn as sheets of glyphs (tools/art/font.ts). */
|
|
28
|
+
fonts?: {
|
|
29
|
+
number: number;
|
|
30
|
+
png: string;
|
|
31
|
+
cell: [number, number];
|
|
32
|
+
first?: number;
|
|
33
|
+
spacing?: number;
|
|
34
|
+
space?: number;
|
|
35
|
+
lineHeight?: number;
|
|
36
|
+
}[];
|
|
27
37
|
}
|
|
28
38
|
export declare class ArtError extends Error {
|
|
29
39
|
}
|
package/tools/art/build.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { ResourceType, writeHunkPalette, writePic, writeView } from "../../packages/sci/src/index.js";
|
|
3
|
+
import { ResourceType, writeFont, writeHunkPalette, writePic, writeView } from "../../packages/sci/src/index.js";
|
|
4
4
|
import { basePalette } from "../game/defaults.js";
|
|
5
5
|
import { decodePng } from "../png.js";
|
|
6
|
+
import { FontSheetError, fontFromSheet } from "./font.js";
|
|
6
7
|
export class ArtError extends Error {
|
|
7
8
|
}
|
|
8
9
|
export function buildArt(file) {
|
|
@@ -38,7 +39,7 @@ export function buildArt(file) {
|
|
|
38
39
|
return fail(where, "expected a file path");
|
|
39
40
|
return resolve(dirname(file), value);
|
|
40
41
|
};
|
|
41
|
-
const m = object(json(file), "manifest", ["version", "palette", "maxColours", "pictures", "views"]);
|
|
42
|
+
const m = object(json(file), "manifest", ["version", "palette", "maxColours", "pictures", "views", "fonts"]);
|
|
42
43
|
if (m.version !== 1)
|
|
43
44
|
fail("version", "expected 1");
|
|
44
45
|
const maxColours = m.maxColours === undefined ? 254 : int(m.maxColours, "maxColours", 2, 254);
|
|
@@ -170,6 +171,35 @@ export function buildArt(file) {
|
|
|
170
171
|
fail(where, "a view supports at most 255 stored cels");
|
|
171
172
|
resources.push({ type: ResourceType.View, number: n, data: writeView({ flags: 1, loops, palette }) });
|
|
172
173
|
});
|
|
174
|
+
list(m.fonts ?? [], "fonts", 0, 65536).forEach((value, i) => {
|
|
175
|
+
const where = `fonts[${i}]`;
|
|
176
|
+
const f = object(value, where, ["number", "png", "cell", "first", "spacing", "space", "lineHeight"]);
|
|
177
|
+
const n = number(f.number, "font", `${where}.number`);
|
|
178
|
+
const filename = path(f.png, `${where}.png`);
|
|
179
|
+
let img;
|
|
180
|
+
try {
|
|
181
|
+
img = decodePng(readFileSync(filename));
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
return fail(`${where}.png`, `${filename}: ${e.message}`);
|
|
185
|
+
}
|
|
186
|
+
const cell = list(f.cell, `${where}.cell`, 2, 2);
|
|
187
|
+
const optional = (key, min, max) => (f[key] === undefined ? undefined : int(f[key], `${where}.${key}`, min, max));
|
|
188
|
+
try {
|
|
189
|
+
const font = fontFromSheet(img, {
|
|
190
|
+
cell: [int(cell[0], `${where}.cell[0]`, 1, 128), int(cell[1], `${where}.cell[1]`, 1, 128)],
|
|
191
|
+
first: optional("first", 0, 255), spacing: optional("spacing", 0, 16),
|
|
192
|
+
space: optional("space", 1, 128), lineHeight: optional("lineHeight", 1, 255),
|
|
193
|
+
});
|
|
194
|
+
resources.push({ type: ResourceType.Font, number: n, data: writeFont(font) });
|
|
195
|
+
images++;
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
if (!(e instanceof FontSheetError))
|
|
199
|
+
throw e;
|
|
200
|
+
fail(`${where}.png`, e.message);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
173
203
|
resources.sort((a, b) => a.type - b.type || a.number - b.number);
|
|
174
204
|
return { resources, colours, images };
|
|
175
205
|
}
|
package/tools/art/build.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { ResourceType, writeHunkPalette, writePic, writeView, type ResourceData, type ViewLoop } from "../../packages/sci/src/index.ts";
|
|
3
|
+
import { ResourceType, writeFont, writeHunkPalette, writePic, writeView, type ResourceData, type ViewLoop } from "../../packages/sci/src/index.ts";
|
|
4
4
|
import { basePalette } from "../game/defaults.ts";
|
|
5
5
|
import { decodePng } from "../png.ts";
|
|
6
|
+
import { FontSheetError, fontFromSheet } from "./font.ts";
|
|
6
7
|
|
|
7
8
|
/** Editor-neutral, exact-colour art for standalone games. See docs/art-workflow.md. */
|
|
8
9
|
export interface ArtManifest {
|
|
@@ -12,6 +13,8 @@ export interface ArtManifest {
|
|
|
12
13
|
maxColours?: number;
|
|
13
14
|
pictures: { number: number; layers: { png: string; priority: number }[] }[];
|
|
14
15
|
views: { number: number; loops: ({ cels: { png: string; anchor: [number, number] }[] } | { link: number; mirror: boolean })[] }[];
|
|
16
|
+
/** Fonts drawn as sheets of glyphs (tools/art/font.ts). */
|
|
17
|
+
fonts?: { number: number; png: string; cell: [number, number]; first?: number; spacing?: number; space?: number; lineHeight?: number }[];
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
export class ArtError extends Error {}
|
|
@@ -39,7 +42,7 @@ export function buildArt(file: string): { resources: ResourceData[]; colours: st
|
|
|
39
42
|
if (typeof value !== "string" || !value.length) return fail(where, "expected a file path");
|
|
40
43
|
return resolve(dirname(file), value);
|
|
41
44
|
};
|
|
42
|
-
const m = object(json(file), "manifest", ["version", "palette", "maxColours", "pictures", "views"]);
|
|
45
|
+
const m = object(json(file), "manifest", ["version", "palette", "maxColours", "pictures", "views", "fonts"]);
|
|
43
46
|
if (m.version !== 1) fail("version", "expected 1");
|
|
44
47
|
const maxColours = m.maxColours === undefined ? 254 : int(m.maxColours, "maxColours", 2, 254);
|
|
45
48
|
const paletteInput = json(path(m.palette, "palette"));
|
|
@@ -148,6 +151,29 @@ export function buildArt(file: string): { resources: ResourceData[]; colours: st
|
|
|
148
151
|
if (total > 255) fail(where, "a view supports at most 255 stored cels");
|
|
149
152
|
resources.push({ type: ResourceType.View, number: n, data: writeView({ flags: 1, loops, palette }) });
|
|
150
153
|
});
|
|
154
|
+
list(m.fonts ?? [], "fonts", 0, 65536).forEach((value, i) => {
|
|
155
|
+
const where = `fonts[${i}]`;
|
|
156
|
+
const f = object(value, where, ["number", "png", "cell", "first", "spacing", "space", "lineHeight"]);
|
|
157
|
+
const n = number(f.number, "font", `${where}.number`);
|
|
158
|
+
const filename = path(f.png, `${where}.png`);
|
|
159
|
+
let img: ReturnType<typeof decodePng>;
|
|
160
|
+
try { img = decodePng(readFileSync(filename)); }
|
|
161
|
+
catch (e) { return fail(`${where}.png`, `${filename}: ${(e as Error).message}`); }
|
|
162
|
+
const cell = list(f.cell, `${where}.cell`, 2, 2);
|
|
163
|
+
const optional = (key: string, min: number, max: number) => (f[key] === undefined ? undefined : int(f[key], `${where}.${key}`, min, max));
|
|
164
|
+
try {
|
|
165
|
+
const font = fontFromSheet(img, {
|
|
166
|
+
cell: [int(cell[0], `${where}.cell[0]`, 1, 128), int(cell[1], `${where}.cell[1]`, 1, 128)],
|
|
167
|
+
first: optional("first", 0, 255), spacing: optional("spacing", 0, 16),
|
|
168
|
+
space: optional("space", 1, 128), lineHeight: optional("lineHeight", 1, 255),
|
|
169
|
+
});
|
|
170
|
+
resources.push({ type: ResourceType.Font, number: n, data: writeFont(font) });
|
|
171
|
+
images++;
|
|
172
|
+
} catch (e) {
|
|
173
|
+
if (!(e instanceof FontSheetError)) throw e;
|
|
174
|
+
fail(`${where}.png`, e.message);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
151
177
|
resources.sort((a, b) => a.type - b.type || a.number - b.number);
|
|
152
178
|
return { resources, colours, images };
|
|
153
179
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Font } from "../../packages/sci/src/index.ts";
|
|
2
|
+
import type { Rgba } from "../png.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A font from a sheet of glyphs drawn in a paint program: a grid of same-size cells, one
|
|
5
|
+
* character per cell, left to right and top to bottom from `first`. A glyph is drawn from
|
|
6
|
+
* its cell's left edge; any opaque pixel is ink (its colour doesn't matter: text takes its
|
|
7
|
+
* colour when it's drawn), and its width is up to its rightmost ink. A cell with no ink is
|
|
8
|
+
* a character the font doesn't have, except the space, which is `space` pixels wide.
|
|
9
|
+
*/
|
|
10
|
+
export interface FontSheet {
|
|
11
|
+
/** Cell size in pixels: [width, height]. */
|
|
12
|
+
cell: [number, number];
|
|
13
|
+
/** The character in the first cell (default 32, the space). */
|
|
14
|
+
first?: number;
|
|
15
|
+
/** Pixels between one glyph and the next (default 1). */
|
|
16
|
+
spacing?: number;
|
|
17
|
+
/** Width of the space (default half a cell). */
|
|
18
|
+
space?: number;
|
|
19
|
+
/** Line height (default the cell height). */
|
|
20
|
+
lineHeight?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare class FontSheetError extends Error {
|
|
23
|
+
}
|
|
24
|
+
export declare function fontFromSheet(img: Rgba, sheet: FontSheet): Font;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class FontSheetError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export function fontFromSheet(img, sheet) {
|
|
4
|
+
const [cw, ch] = sheet.cell;
|
|
5
|
+
const first = sheet.first ?? 32, spacing = sheet.spacing ?? 1;
|
|
6
|
+
const space = sheet.space ?? Math.max(1, cw >> 1), lineHeight = sheet.lineHeight ?? ch;
|
|
7
|
+
const fail = (message) => { throw new FontSheetError(message); };
|
|
8
|
+
if (img.width % cw || img.height % ch)
|
|
9
|
+
fail(`${img.width}x${img.height} is not a whole number of ${cw}x${ch} cells`);
|
|
10
|
+
const columns = img.width / cw, cells = columns * (img.height / ch);
|
|
11
|
+
if (first + cells > 256)
|
|
12
|
+
fail(`${cells} cells from character ${first} go past 255`);
|
|
13
|
+
if (lineHeight < 1 || lineHeight > 255)
|
|
14
|
+
fail(`line height ${lineHeight} is outside 1..255`);
|
|
15
|
+
const glyphs = Array.from({ length: Math.max(128, first + cells) }, () => ({ width: 0, height: 0, pixels: new Uint8Array(0) }));
|
|
16
|
+
for (let i = 0; i < cells; i++) {
|
|
17
|
+
const left = (i % columns) * cw, top = Math.floor(i / columns) * ch;
|
|
18
|
+
const ink = new Uint8Array(cw * ch);
|
|
19
|
+
let right = -1;
|
|
20
|
+
for (let y = 0; y < ch; y++) {
|
|
21
|
+
for (let x = 0; x < cw; x++) {
|
|
22
|
+
const a = img.data[((top + y) * img.width + left + x) * 4 + 3];
|
|
23
|
+
if (a !== 0 && a !== 255)
|
|
24
|
+
fail(`character ${first + i} (${left + x},${top + y}): alpha ${a}; export without antialiasing`);
|
|
25
|
+
if (a)
|
|
26
|
+
(ink[y * cw + x] = 1), (right = Math.max(right, x));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const code = first + i;
|
|
30
|
+
if (right < 0 && code !== 32)
|
|
31
|
+
continue;
|
|
32
|
+
const inked = right + 1, width = (code === 32 && right < 0 ? space : inked + spacing);
|
|
33
|
+
const pixels = new Uint8Array(width * ch);
|
|
34
|
+
for (let y = 0; y < ch; y++)
|
|
35
|
+
for (let x = 0; x < inked; x++)
|
|
36
|
+
pixels[y * width + x] = ink[y * cw + x];
|
|
37
|
+
glyphs[code] = { width, height: ch, pixels };
|
|
38
|
+
}
|
|
39
|
+
return { height: lineHeight, glyphs };
|
|
40
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Font, Glyph } from "../../packages/sci/src/index.ts";
|
|
2
|
+
import type { Rgba } from "../png.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A font from a sheet of glyphs drawn in a paint program: a grid of same-size cells, one
|
|
6
|
+
* character per cell, left to right and top to bottom from `first`. A glyph is drawn from
|
|
7
|
+
* its cell's left edge; any opaque pixel is ink (its colour doesn't matter: text takes its
|
|
8
|
+
* colour when it's drawn), and its width is up to its rightmost ink. A cell with no ink is
|
|
9
|
+
* a character the font doesn't have, except the space, which is `space` pixels wide.
|
|
10
|
+
*/
|
|
11
|
+
export interface FontSheet {
|
|
12
|
+
/** Cell size in pixels: [width, height]. */
|
|
13
|
+
cell: [number, number];
|
|
14
|
+
/** The character in the first cell (default 32, the space). */
|
|
15
|
+
first?: number;
|
|
16
|
+
/** Pixels between one glyph and the next (default 1). */
|
|
17
|
+
spacing?: number;
|
|
18
|
+
/** Width of the space (default half a cell). */
|
|
19
|
+
space?: number;
|
|
20
|
+
/** Line height (default the cell height). */
|
|
21
|
+
lineHeight?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class FontSheetError extends Error {}
|
|
25
|
+
|
|
26
|
+
export function fontFromSheet(img: Rgba, sheet: FontSheet): Font {
|
|
27
|
+
const [cw, ch] = sheet.cell;
|
|
28
|
+
const first = sheet.first ?? 32, spacing = sheet.spacing ?? 1;
|
|
29
|
+
const space = sheet.space ?? Math.max(1, cw >> 1), lineHeight = sheet.lineHeight ?? ch;
|
|
30
|
+
const fail = (message: string): never => { throw new FontSheetError(message); };
|
|
31
|
+
if (img.width % cw || img.height % ch) fail(`${img.width}x${img.height} is not a whole number of ${cw}x${ch} cells`);
|
|
32
|
+
const columns = img.width / cw, cells = columns * (img.height / ch);
|
|
33
|
+
if (first + cells > 256) fail(`${cells} cells from character ${first} go past 255`);
|
|
34
|
+
if (lineHeight < 1 || lineHeight > 255) fail(`line height ${lineHeight} is outside 1..255`);
|
|
35
|
+
|
|
36
|
+
const glyphs: Glyph[] = Array.from({ length: Math.max(128, first + cells) }, () => ({ width: 0, height: 0, pixels: new Uint8Array(0) }));
|
|
37
|
+
for (let i = 0; i < cells; i++) {
|
|
38
|
+
const left = (i % columns) * cw, top = Math.floor(i / columns) * ch;
|
|
39
|
+
const ink = new Uint8Array(cw * ch);
|
|
40
|
+
let right = -1;
|
|
41
|
+
for (let y = 0; y < ch; y++) {
|
|
42
|
+
for (let x = 0; x < cw; x++) {
|
|
43
|
+
const a = img.data[((top + y) * img.width + left + x) * 4 + 3]!;
|
|
44
|
+
if (a !== 0 && a !== 255) fail(`character ${first + i} (${left + x},${top + y}): alpha ${a}; export without antialiasing`);
|
|
45
|
+
if (a) (ink[y * cw + x] = 1), (right = Math.max(right, x));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const code = first + i;
|
|
49
|
+
if (right < 0 && code !== 32) continue;
|
|
50
|
+
const inked = right + 1, width = (code === 32 && right < 0 ? space : inked + spacing);
|
|
51
|
+
const pixels = new Uint8Array(width * ch);
|
|
52
|
+
for (let y = 0; y < ch; y++) for (let x = 0; x < inked; x++) pixels[y * width + x] = ink[y * cw + x]!;
|
|
53
|
+
glyphs[code] = { width, height: ch, pixels };
|
|
54
|
+
}
|
|
55
|
+
return { height: lineHeight, glyphs };
|
|
56
|
+
}
|
package/tools/game/build.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { type ResourceData } from "../../packages/sci/src/index.ts";
|
|
|
9
9
|
* games/<name>/rooms/<n>.room.yaml (+ <n>.yarn) rooms as data (packages/content), compiled
|
|
10
10
|
* for the class library; their Yarn variables are flags
|
|
11
11
|
* numbered in flags.yaml
|
|
12
|
+
* games/<name>/items.yaml optional: the verbs of the things the hero can carry
|
|
13
|
+
* (`lens: 10`), so rooms' Yarn can answer them (filings.lens)
|
|
12
14
|
* games/<name>/music/<n>.mid music: sound n, from a Standard MIDI File (General MIDI)
|
|
13
15
|
* games/<name>/sounds/<n>.wav digital effects: sound n plays this instead of music
|
|
14
16
|
* games/<name>/resources.ts optional: `export default () => ResourceData[]` for art,
|
|
@@ -44,3 +46,5 @@ export declare const LIBRARY_DIR: string;
|
|
|
44
46
|
export declare function buildGame(dir: string, options?: {
|
|
45
47
|
library?: boolean;
|
|
46
48
|
}): Promise<BuiltGame>;
|
|
49
|
+
/** items.yaml: `name: verb` lines, each item's verb from 10 to 255 (1 to 4 are the player's). */
|
|
50
|
+
export declare function itemVerbs(dir: string): Record<string, number>;
|
package/tools/game/build.js
CHANGED
|
@@ -108,7 +108,7 @@ export async function buildGame(dir, options = {}) {
|
|
|
108
108
|
const roomsDir = join(dir, "rooms");
|
|
109
109
|
if (!existsSync(roomsDir))
|
|
110
110
|
return [];
|
|
111
|
-
const target = libraryTarget(globals);
|
|
111
|
+
const target = libraryTarget(globals, itemVerbs(dir));
|
|
112
112
|
const flags = modFlags(dir, { content: target });
|
|
113
113
|
const out = [];
|
|
114
114
|
for (const { file, text } of load(roomsDir, /^\d+\.room\.yaml$/)) {
|
|
@@ -257,3 +257,29 @@ export async function buildGame(dir, options = {}) {
|
|
|
257
257
|
warnings: [...new Set(warnings)],
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
|
+
/** items.yaml: `name: verb` lines, each item's verb from 10 to 255 (1 to 4 are the player's). */
|
|
261
|
+
export function itemVerbs(dir) {
|
|
262
|
+
const file = join(dir, "items.yaml");
|
|
263
|
+
if (!existsSync(file))
|
|
264
|
+
return {};
|
|
265
|
+
const where = relative(process.cwd(), file);
|
|
266
|
+
const verbs = {};
|
|
267
|
+
readFileSync(file, "utf8").split("\n").forEach((raw, i) => {
|
|
268
|
+
const line = raw.replace(/#.*/, "").trim();
|
|
269
|
+
if (!line)
|
|
270
|
+
return;
|
|
271
|
+
const at = `${where}:${i + 1}`;
|
|
272
|
+
const m = /^([A-Za-z]\w*)\s*:\s*(\d+)$/.exec(line);
|
|
273
|
+
if (!m)
|
|
274
|
+
throw new GameBuildError(`${at}: expected item: verb`);
|
|
275
|
+
const [, name, n] = m, verb = Number(n);
|
|
276
|
+
if (!/^[a-z][A-Za-z0-9]*$/.test(name) || ["look", "talk", "walk", "do", "enter"].includes(name))
|
|
277
|
+
throw new GameBuildError(`${at}: "${name}" can't name an item (lower camelCase, and not a verb)`);
|
|
278
|
+
if (verb < 10 || verb > 255)
|
|
279
|
+
throw new GameBuildError(`${at}: ${name}: the verb is a number from 10 to 255`);
|
|
280
|
+
if (Object.values(verbs).includes(verb))
|
|
281
|
+
throw new GameBuildError(`${at}: ${name}: verb ${verb} is used twice`);
|
|
282
|
+
verbs[name] = verb;
|
|
283
|
+
});
|
|
284
|
+
return verbs;
|
|
285
|
+
}
|
package/tools/game/build.ts
CHANGED
|
@@ -22,6 +22,8 @@ import { libraryTarget } from "./target.ts";
|
|
|
22
22
|
* games/<name>/rooms/<n>.room.yaml (+ <n>.yarn) rooms as data (packages/content), compiled
|
|
23
23
|
* for the class library; their Yarn variables are flags
|
|
24
24
|
* numbered in flags.yaml
|
|
25
|
+
* games/<name>/items.yaml optional: the verbs of the things the hero can carry
|
|
26
|
+
* (`lens: 10`), so rooms' Yarn can answer them (filings.lens)
|
|
25
27
|
* games/<name>/music/<n>.mid music: sound n, from a Standard MIDI File (General MIDI)
|
|
26
28
|
* games/<name>/sounds/<n>.wav digital effects: sound n plays this instead of music
|
|
27
29
|
* games/<name>/resources.ts optional: `export default () => ResourceData[]` for art,
|
|
@@ -140,7 +142,7 @@ export async function buildGame(dir: string, options: { library?: boolean } = {}
|
|
|
140
142
|
const compileRooms = (): { file: string; text: string }[] => {
|
|
141
143
|
const roomsDir = join(dir, "rooms");
|
|
142
144
|
if (!existsSync(roomsDir)) return [];
|
|
143
|
-
const target = libraryTarget(globals);
|
|
145
|
+
const target = libraryTarget(globals, itemVerbs(dir));
|
|
144
146
|
const flags = modFlags(dir, { content: target });
|
|
145
147
|
const out: { file: string; text: string }[] = [];
|
|
146
148
|
for (const { file, text } of load(roomsDir, /^\d+\.room\.yaml$/)) {
|
|
@@ -281,3 +283,24 @@ export async function buildGame(dir: string, options: { library?: boolean } = {}
|
|
|
281
283
|
warnings: [...new Set(warnings)],
|
|
282
284
|
};
|
|
283
285
|
}
|
|
286
|
+
|
|
287
|
+
/** items.yaml: `name: verb` lines, each item's verb from 10 to 255 (1 to 4 are the player's). */
|
|
288
|
+
export function itemVerbs(dir: string): Record<string, number> {
|
|
289
|
+
const file = join(dir, "items.yaml");
|
|
290
|
+
if (!existsSync(file)) return {};
|
|
291
|
+
const where = relative(process.cwd(), file);
|
|
292
|
+
const verbs: Record<string, number> = {};
|
|
293
|
+
readFileSync(file, "utf8").split("\n").forEach((raw, i) => {
|
|
294
|
+
const line = raw.replace(/#.*/, "").trim();
|
|
295
|
+
if (!line) return;
|
|
296
|
+
const at = `${where}:${i + 1}`;
|
|
297
|
+
const m = /^([A-Za-z]\w*)\s*:\s*(\d+)$/.exec(line);
|
|
298
|
+
if (!m) throw new GameBuildError(`${at}: expected item: verb`);
|
|
299
|
+
const [, name, n] = m, verb = Number(n);
|
|
300
|
+
if (!/^[a-z][A-Za-z0-9]*$/.test(name!) || ["look", "talk", "walk", "do", "enter"].includes(name!)) throw new GameBuildError(`${at}: "${name}" can't name an item (lower camelCase, and not a verb)`);
|
|
301
|
+
if (verb < 10 || verb > 255) throw new GameBuildError(`${at}: ${name}: the verb is a number from 10 to 255`);
|
|
302
|
+
if (Object.values(verbs).includes(verb)) throw new GameBuildError(`${at}: ${name}: verb ${verb} is used twice`);
|
|
303
|
+
verbs[name!] = verb;
|
|
304
|
+
});
|
|
305
|
+
return verbs;
|
|
306
|
+
}
|
package/tools/game/kit.d.ts
CHANGED
|
@@ -5,3 +5,4 @@
|
|
|
5
5
|
export { ResourceType, SoundDevice, writeFont, writeHunkPalette, writePic, writeSound, writeView, type Cel, type Font, type HunkPaletteFile, type MidiEvent, type PicFile, type ResourceData, type SoundSpec, type ViewFile, } from "../../packages/sci/src/index.ts";
|
|
6
6
|
export { BASE_PALETTE, CURSOR_VIEW, Colour, DEFAULT_FONT, basePalette, cube } from "./defaults.ts";
|
|
7
7
|
export { pixelFont } from "./font.ts";
|
|
8
|
+
export { fontFromSheet, type FontSheet } from "../art/font.ts";
|
package/tools/game/kit.js
CHANGED
|
@@ -5,3 +5,4 @@
|
|
|
5
5
|
export { ResourceType, SoundDevice, writeFont, writeHunkPalette, writePic, writeSound, writeView, } from "../../packages/sci/src/index.js";
|
|
6
6
|
export { BASE_PALETTE, CURSOR_VIEW, Colour, DEFAULT_FONT, basePalette, cube } from "./defaults.js";
|
|
7
7
|
export { pixelFont } from "./font.js";
|
|
8
|
+
export { fontFromSheet } from "../art/font.js";
|
package/tools/game/kit.ts
CHANGED
package/tools/game/target.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ import type { Target } from "../../packages/content/src/index.ts";
|
|
|
4
4
|
* (packages/content). Global numbers depend on the game's script 0, so the target is made
|
|
5
5
|
* for each build from the compiled globals' names.
|
|
6
6
|
*/
|
|
7
|
-
export declare function libraryTarget(globals: readonly string[]): Target;
|
|
7
|
+
export declare function libraryTarget(globals: readonly string[], items?: Record<string, number>): Target;
|
package/tools/game/target.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (packages/content). Global numbers depend on the game's script 0, so the target is made
|
|
4
4
|
* for each build from the compiled globals' names.
|
|
5
5
|
*/
|
|
6
|
-
export function libraryTarget(globals) {
|
|
6
|
+
export function libraryTarget(globals, items = {}) {
|
|
7
7
|
const g = (name) => {
|
|
8
8
|
const n = globals.indexOf(name);
|
|
9
9
|
if (n < 0)
|
|
@@ -28,12 +28,14 @@ export function libraryTarget(globals) {
|
|
|
28
28
|
firstRoomTalker: 200,
|
|
29
29
|
voice: { class: "Talker", props: {} },
|
|
30
30
|
// Portraits stand at the top left, the text beside them.
|
|
31
|
-
portrait: { class: "PortraitTalker", props: {}, at: [
|
|
31
|
+
portrait: { class: "PortraitTalker", props: {}, at: [8, 8], frameSignal: 0, partSignal: 0 },
|
|
32
32
|
teller: { class: "Teller", props: {}, verb: 2 },
|
|
33
33
|
forwardCycle: "Forward",
|
|
34
|
+
closeUp: "CloseUp",
|
|
34
35
|
// room.enter lines: a verb the player can't use.
|
|
35
36
|
narrationVerb: 0,
|
|
36
|
-
|
|
37
|
+
// And the game's items (items.yaml): using one is its own verb.
|
|
38
|
+
verbs: { look: 1, talk: 2, walk: 3, do: 4, ...items },
|
|
37
39
|
messageVersion: 4321,
|
|
38
40
|
// SetFlag, ClearFlag and IsFlag: script 999's exports 0, 1 and 2.
|
|
39
41
|
flags: { script: 999, set: 0, clear: 1, test: 2, first: 0, last: 1023 },
|
package/tools/game/target.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Target } from "../../packages/content/src/index.ts";
|
|
|
5
5
|
* (packages/content). Global numbers depend on the game's script 0, so the target is made
|
|
6
6
|
* for each build from the compiled globals' names.
|
|
7
7
|
*/
|
|
8
|
-
export function libraryTarget(globals: readonly string[]): Target {
|
|
8
|
+
export function libraryTarget(globals: readonly string[], items: Record<string, number> = {}): Target {
|
|
9
9
|
const g = (name: string) => {
|
|
10
10
|
const n = globals.indexOf(name);
|
|
11
11
|
if (n < 0) throw new Error(`the library's global ${name} isn't there: is script 0 compiled with the library?`);
|
|
@@ -29,12 +29,14 @@ export function libraryTarget(globals: readonly string[]): Target {
|
|
|
29
29
|
firstRoomTalker: 200,
|
|
30
30
|
voice: { class: "Talker", props: {} },
|
|
31
31
|
// Portraits stand at the top left, the text beside them.
|
|
32
|
-
portrait: { class: "PortraitTalker", props: {}, at: [
|
|
32
|
+
portrait: { class: "PortraitTalker", props: {}, at: [8, 8], frameSignal: 0, partSignal: 0 },
|
|
33
33
|
teller: { class: "Teller", props: {}, verb: 2 },
|
|
34
34
|
forwardCycle: "Forward",
|
|
35
|
+
closeUp: "CloseUp",
|
|
35
36
|
// room.enter lines: a verb the player can't use.
|
|
36
37
|
narrationVerb: 0,
|
|
37
|
-
|
|
38
|
+
// And the game's items (items.yaml): using one is its own verb.
|
|
39
|
+
verbs: { look: 1, talk: 2, walk: 3, do: 4, ...items },
|
|
38
40
|
messageVersion: 4321,
|
|
39
41
|
// SetFlag, ClearFlag and IsFlag: script 999's exports 0, 1 and 2.
|
|
40
42
|
flags: { script: 999, set: 0, clear: 1, test: 2, first: 0, last: 1023 },
|