agentix-cli 0.98.0 → 0.100.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/apps/mac-helper/build.sh +10 -2
- package/apps/mac-voice/Sources/AgentXVoice/Character.swift +41 -63
- package/apps/mac-voice/Sources/AgentXVoice/CharacterMath.swift +9 -0
- package/apps/mac-voice/Sources/AgentXVoice/CharacterSim.swift +46 -44
- package/apps/mac-voice/Sources/AgentXVoice/CharacterSimFrame.swift +59 -0
- package/apps/mac-voice/Sources/AgentXVoice/CharacterView.swift +57 -0
- package/apps/mac-voice/Sources/AgentXVoice/Config.swift +2 -1
- package/apps/mac-voice/Sources/AgentXVoice/Guide.swift +185 -0
- package/apps/mac-voice/Sources/AgentXVoice/GuideMath.swift +49 -0
- package/apps/mac-voice/Sources/AgentXVoice/Meets.swift +39 -0
- package/apps/mac-voice/Sources/AgentXVoice/Panel.swift +9 -0
- package/apps/mac-voice/Sources/AgentXVoice/PointerPlay.swift +137 -0
- package/apps/mac-voice/Sources/AgentXVoice/main.swift +7 -0
- package/apps/mac-voice/build.sh +14 -4
- package/dist/{board-dashboard-AIT26JTK.js → board-dashboard-R6ZTCTSI.js} +467 -321
- package/dist/board-dashboard-R6ZTCTSI.js.map +1 -0
- package/dist/{chunk-WWCVQHV3.js → chunk-6BZMRVKJ.js} +191 -190
- package/dist/chunk-6BZMRVKJ.js.map +1 -0
- package/dist/{chunk-QUU5RJ7D.js → chunk-VBFXUZTY.js} +15 -15
- package/dist/{chunk-QUU5RJ7D.js.map → chunk-VBFXUZTY.js.map} +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-FK7REU2M.js → program-CH5QSA7K.js} +161 -161
- package/dist/program-CH5QSA7K.js.map +1 -0
- package/package.json +1 -1
- package/dist/board-dashboard-AIT26JTK.js.map +0 -1
- package/dist/chunk-WWCVQHV3.js.map +0 -1
- package/dist/program-FK7REU2M.js.map +0 -1
package/apps/mac-helper/build.sh
CHANGED
|
@@ -55,8 +55,16 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
|
|
|
55
55
|
PLIST
|
|
56
56
|
echo "</plist>" >> "$APP/Contents/Info.plist"
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
# Ad hoc by default, which holds a grant for this one build only; see
|
|
59
|
+
# AGENTX_SIGN_IDENTITY in apps/mac-voice/build.sh.
|
|
60
|
+
IDENTITY="${AGENTX_SIGN_IDENTITY:--}"
|
|
61
|
+
if [ "$IDENTITY" = "-" ]; then
|
|
62
|
+
codesign --force --sign - --identifier tn.acme.agentx.helper "$APP" 2>/dev/null \
|
|
63
|
+
|| echo "warning: codesign failed; Accessibility grants will not stick across rebuilds"
|
|
64
|
+
echo "note: ad-hoc signature; macOS asks again for Accessibility and Screen Recording after this install (set AGENTX_SIGN_IDENTITY to keep them)"
|
|
65
|
+
else
|
|
66
|
+
codesign --force --sign "$IDENTITY" --identifier tn.acme.agentx.helper "$APP"
|
|
67
|
+
fi
|
|
60
68
|
|
|
61
69
|
echo "built: $APP"
|
|
62
70
|
echo "binary: $BIN/agentx-mac-helper"
|
|
@@ -4,7 +4,8 @@ import AppKit
|
|
|
4
4
|
/// agent's palette, with two eyes, no mouth and no legs. It hovers above
|
|
5
5
|
/// the bottom edge of the screen, or where it was dragged to (#502),
|
|
6
6
|
/// shows what the assistant is doing, and gets out of the pointer's way.
|
|
7
|
-
///
|
|
7
|
+
/// The answering agent can send it to something on screen, which it
|
|
8
|
+
/// marks (#482). Chosen with `voice.look`; the orb stays the default.
|
|
8
9
|
///
|
|
9
10
|
/// It reacts and never interrupts: its window takes no keys, no clicks
|
|
10
11
|
/// but a drag of its body with ⌘ held, and never comes forward by itself.
|
|
@@ -44,6 +45,12 @@ final class CharacterHost {
|
|
|
44
45
|
/// bubble is, or nil while it is hidden. Set by the app.
|
|
45
46
|
var bubble: ((NSPoint, NSRect) -> NSRect?)?
|
|
46
47
|
private var bubbleFrame: NSRect?
|
|
48
|
+
/// What stands on its line (#539): the sides of the windows there,
|
|
49
|
+
/// read once a second while it may stroll and play mode is ticked.
|
|
50
|
+
private var edges: [Double] = []
|
|
51
|
+
private var edgesRead = 0.0
|
|
52
|
+
/// Its bubble holds something to use: no play with the pointer then.
|
|
53
|
+
var bubbleShows: (() -> Bool)?
|
|
47
54
|
|
|
48
55
|
/// Where it was dragged to: the point under it. Nil: its corner.
|
|
49
56
|
private var place = Config.characterPlace
|
|
@@ -58,10 +65,14 @@ final class CharacterHost {
|
|
|
58
65
|
/// Carried by the pointer: where the pointer took hold, and where it
|
|
59
66
|
/// stood then, which is not where it rests once it has stepped aside.
|
|
60
67
|
private var carried: (from: NSPoint, rest: CGPoint)?
|
|
68
|
+
/// What the answering agent sent it to show (#482), and its mark.
|
|
69
|
+
private let guide = CharacterGuide()
|
|
61
70
|
/// A still picture: Reduce Motion, or "Animated orb" off.
|
|
62
71
|
private var still: Bool { !animated || reduceMotion }
|
|
63
72
|
/// The look is the character: whether it is on screen. Nil with the orb.
|
|
64
73
|
var onScreen: Bool? { shown ? !hidden : nil }
|
|
74
|
+
/// Told when it comes on screen or leaves it: shown or hidden, or the look changed.
|
|
75
|
+
var onScreenChanged: (() -> Void)?
|
|
65
76
|
|
|
66
77
|
init() {
|
|
67
78
|
window = NSPanel(contentRect: NSRect(origin: .zero, size: Self.size),
|
|
@@ -95,6 +106,7 @@ final class CharacterHost {
|
|
|
95
106
|
shown = on
|
|
96
107
|
if on { sim = CharacterSim(unit: Double(Self.diameter) / 100) }
|
|
97
108
|
run()
|
|
109
|
+
onScreenChanged?()
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
/// Hide it, as its bubble is hidden: gone until it is asked back.
|
|
@@ -102,6 +114,7 @@ final class CharacterHost {
|
|
|
102
114
|
guard hidden != on else { return }
|
|
103
115
|
hidden = on
|
|
104
116
|
run()
|
|
117
|
+
onScreenChanged?()
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
/// "Reset position" in the menu: back to its corner, forgetting where
|
|
@@ -130,6 +143,16 @@ final class CharacterHost {
|
|
|
130
143
|
redraw()
|
|
131
144
|
}
|
|
132
145
|
|
|
146
|
+
/// Go beside `rect` and mark it, or with nil go back to where it rests.
|
|
147
|
+
func guide(to rect: NSRect?, mark kind: GuideMath.Mark = .none) {
|
|
148
|
+
if shown, !hidden, carried == nil, let rect {
|
|
149
|
+
guide.show(rect, kind, color: view.stops[2], animated: !still)
|
|
150
|
+
} else {
|
|
151
|
+
guide.end()
|
|
152
|
+
}
|
|
153
|
+
redraw()
|
|
154
|
+
}
|
|
155
|
+
|
|
133
156
|
/// Standing still, it is drawn again when something changes: its
|
|
134
157
|
/// state, or its bubble showing or hiding.
|
|
135
158
|
func redraw() {
|
|
@@ -140,7 +163,7 @@ final class CharacterHost {
|
|
|
140
163
|
private func run() {
|
|
141
164
|
timer?.invalidate()
|
|
142
165
|
timer = nil
|
|
143
|
-
guard shown, !hidden else { carried = nil; window.orderOut(nil); return }
|
|
166
|
+
guard shown, !hidden else { carried = nil; guide(to: nil); window.orderOut(nil); return }
|
|
144
167
|
tick()
|
|
145
168
|
window.orderFrontRegardless()
|
|
146
169
|
// Still, it only watches for the hand that moves it.
|
|
@@ -166,7 +189,7 @@ final class CharacterHost {
|
|
|
166
189
|
switch phase {
|
|
167
190
|
case .began:
|
|
168
191
|
let mouse = pointerSource()
|
|
169
|
-
if body.contains(mouse) { carried = (mouse, CGPoint(x: body.midX, y: rest.y)) }
|
|
192
|
+
if body.contains(mouse) { guide.end(glide: false); carried = (mouse, CGPoint(x: body.midX, y: rest.y)) }
|
|
170
193
|
case .moved:
|
|
171
194
|
if carried != nil { tick() }
|
|
172
195
|
case .ended:
|
|
@@ -194,11 +217,14 @@ final class CharacterHost {
|
|
|
194
217
|
var wanted = place
|
|
195
218
|
if let carried {
|
|
196
219
|
wanted = CGPoint(x: carried.rest.x + mouse.x - carried.from.x, y: carried.rest.y + mouse.y - carried.from.y)
|
|
220
|
+
} else if let beside = guide.stand(screens: screens, body: Self.diameter, tall: Self.head) {
|
|
221
|
+
wanted = beside
|
|
197
222
|
}
|
|
198
223
|
let spot = PillPlacement.character(saved: wanted, room: Self.head + PillPlacement.tail + Panel.size.height,
|
|
199
224
|
screens: screens.map(\.visibleFrame), fallback: first.visibleFrame)
|
|
200
225
|
let visible = spot.visible
|
|
201
|
-
|
|
226
|
+
let now = ProcessInfo.processInfo.systemUptime
|
|
227
|
+
rest = CGPoint(x: spot.place.x, y: guide.height(from: rest.y, toward: spot.place.y, now: now, still: still))
|
|
202
228
|
let home = Double(rest.x)
|
|
203
229
|
if carried != nil { sim.carry(to: home) }
|
|
204
230
|
|
|
@@ -212,15 +238,23 @@ final class CharacterHost {
|
|
|
212
238
|
level = 0
|
|
213
239
|
}
|
|
214
240
|
let near = screens.first { $0.visibleFrame == visible }?.frame.contains(mouse) ?? false
|
|
215
|
-
|
|
241
|
+
// Showing something, it does not step aside for the pointer.
|
|
242
|
+
let pointer = near && guide.showing == nil ? (x: Double(mouse.x), y: Double(mouse.y - rest.y)) : nil
|
|
216
243
|
// The gap its tail fills counts as the bubble, so a pointer a
|
|
217
244
|
// little under a button does not send both away. With ⌘ held
|
|
218
245
|
// it waits too, to be taken hold of.
|
|
219
246
|
let held = carried != nil
|
|
220
247
|
|| (bubbleFrame?.insetBy(dx: 0, dy: -PillPlacement.tail).contains(mouse) ?? false)
|
|
221
248
|
|| (NSEvent.modifierFlags.contains(.command) && window.frame.contains(mouse))
|
|
222
|
-
|
|
223
|
-
|
|
249
|
+
if now - edgesRead >= 1 {
|
|
250
|
+
edgesRead = now
|
|
251
|
+
// The window list counts down from the top of the main screen.
|
|
252
|
+
let top = first.frame.maxY - rest.y
|
|
253
|
+
edges = strolls && Config.playMode ? Meets.edges(of: Meets.windows(), band: (top - Self.head)...top) : []
|
|
254
|
+
}
|
|
255
|
+
frame = sim.step(to: now,
|
|
256
|
+
CharacterSim.Input(activity: activity, level: level, pointer: pointer, held: held, sent: guide.showing != nil, strolls: strolls,
|
|
257
|
+
edges: edges, shows: bubbleShows?() ?? false, plays: Config.playMode, down: NSEvent.pressedMouseButtons & 1 != 0,
|
|
224
258
|
home: home, range: Double(spot.ends.lowerBound)...Double(spot.ends.upperBound)))
|
|
225
259
|
}
|
|
226
260
|
let origin = NSPoint(x: (CGFloat(frame.x) - Self.size.width / 2).rounded(), y: (rest.y - Self.ground).rounded())
|
|
@@ -235,59 +269,3 @@ final class CharacterHost {
|
|
|
235
269
|
aim()
|
|
236
270
|
}
|
|
237
271
|
}
|
|
238
|
-
|
|
239
|
-
/// The character's drawing surface. Flipped, so the drawing's y grows
|
|
240
|
-
/// downwards as in the pose sheet it was ported from.
|
|
241
|
-
final class CharacterView: NSView {
|
|
242
|
-
var shown = CharacterSim.Frame()
|
|
243
|
-
var stops = CharacterDraw.stops(tint: Brand.accent, colors: nil)
|
|
244
|
-
var unit: CGFloat = 0.56
|
|
245
|
-
/// Under the character, in this view; and the view's left edge on screen.
|
|
246
|
-
var edge = CGPoint.zero
|
|
247
|
-
var origin: CGFloat = 0
|
|
248
|
-
/// Its speech bubble in this window, origin bottom-left; nil while hidden.
|
|
249
|
-
var bubble: NSRect?
|
|
250
|
-
|
|
251
|
-
/// A drag of its body. Its window takes the mouse for nothing else.
|
|
252
|
-
enum Drag { case began, moved, ended }
|
|
253
|
-
var onDrag: ((Drag) -> Void)?
|
|
254
|
-
|
|
255
|
-
override var isFlipped: Bool { true }
|
|
256
|
-
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
|
|
257
|
-
override func mouseDown(with event: NSEvent) { onDrag?(.began) }
|
|
258
|
-
override func mouseDragged(with event: NSEvent) { onDrag?(.moved) }
|
|
259
|
-
override func mouseUp(with event: NSEvent) { onDrag?(.ended) }
|
|
260
|
-
|
|
261
|
-
override func draw(_ dirtyRect: NSRect) {
|
|
262
|
-
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
|
|
263
|
-
// The body is drawn where the window is, which follows the frame
|
|
264
|
-
// to the nearest point; the rest of the way is drawn here.
|
|
265
|
-
let at = CGPoint(x: CGFloat(shown.x) - origin, y: edge.y)
|
|
266
|
-
CharacterDraw.draw(shown, in: ctx, edge: at, origin: origin, unit: unit, stops: stops)
|
|
267
|
-
if let bubble { drawTail(in: ctx, from: bubble) }
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/// The bubble's tail: a small point from its bottom edge down to the
|
|
271
|
-
/// character, in the bubble's own colours.
|
|
272
|
-
private func drawTail(in ctx: CGContext, from bubble: NSRect) {
|
|
273
|
-
let half: CGFloat = 7, corner = Brand.Radius.lg + half
|
|
274
|
-
let x = min(max(bounds.midX, bubble.minX + corner), bubble.maxX - corner)
|
|
275
|
-
// One point up into the bubble, so no gap shows between the two.
|
|
276
|
-
let top = bounds.height - bubble.minY - 1
|
|
277
|
-
let tip = CGPoint(x: x, y: top + 1 + PillPlacement.tail)
|
|
278
|
-
effectiveAppearance.performAsCurrentDrawingAppearance {
|
|
279
|
-
ctx.move(to: CGPoint(x: x - half, y: top))
|
|
280
|
-
ctx.addLine(to: tip)
|
|
281
|
-
ctx.addLine(to: CGPoint(x: x + half, y: top))
|
|
282
|
-
ctx.closePath()
|
|
283
|
-
ctx.setFillColor(NSColor.windowBackgroundColor.withAlphaComponent(0.92).cgColor)
|
|
284
|
-
ctx.fillPath()
|
|
285
|
-
ctx.move(to: CGPoint(x: x - half, y: top + 1))
|
|
286
|
-
ctx.addLine(to: tip)
|
|
287
|
-
ctx.addLine(to: CGPoint(x: x + half, y: top + 1))
|
|
288
|
-
ctx.setStrokeColor(NSColor.separatorColor.cgColor)
|
|
289
|
-
ctx.setLineWidth(1)
|
|
290
|
-
ctx.strokePath()
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
@@ -142,4 +142,13 @@ enum CharacterMath {
|
|
|
142
142
|
if range.contains(there) { return there }
|
|
143
143
|
return min(max(pointer - side * clear, range.lowerBound), range.upperBound)
|
|
144
144
|
}
|
|
145
|
+
|
|
146
|
+
/// Walking from `from` to `to`: the nearest of `edges` in its way, and
|
|
147
|
+
/// where it stops, `gap` short of it and never back from where it is.
|
|
148
|
+
static func stop(from: Double, to: Double, edges: [Double], gap: Double) -> (edge: Double, at: Double)? {
|
|
149
|
+
let way: Double = to >= from ? 1 : -1
|
|
150
|
+
let ahead = edges.filter { way * ($0 - from) > 0 && way * ($0 - to) < gap }
|
|
151
|
+
guard let edge = ahead.min(by: { abs($0 - from) < abs($1 - from) }) else { return nil }
|
|
152
|
+
return (edge, from + way * max(way * (edge - from) - gap, 0))
|
|
153
|
+
}
|
|
145
154
|
}
|
|
@@ -10,48 +10,6 @@ import Foundation
|
|
|
10
10
|
struct CharacterSim {
|
|
11
11
|
typealias M = CharacterMath
|
|
12
12
|
|
|
13
|
-
struct Input {
|
|
14
|
-
var activity = M.Activity.idle
|
|
15
|
-
/// 0…1: the microphone while listening.
|
|
16
|
-
var level = 0.0
|
|
17
|
-
/// The pointer: along the edge, and its height above it. Nil when
|
|
18
|
-
/// it is on another screen.
|
|
19
|
-
var pointer: (x: Double, y: Double)?
|
|
20
|
-
/// The pointer is on its speech bubble: it stays where it is, so
|
|
21
|
-
/// the bubble is not pulled from under the pointer.
|
|
22
|
-
var held = false
|
|
23
|
-
/// `voice.stroll`: with nothing to do, it takes a slow stroll
|
|
24
|
-
/// beside where it rests now and then (#482).
|
|
25
|
-
var strolls = false
|
|
26
|
-
/// Where it rests, and how far it may go.
|
|
27
|
-
var home = 0.0
|
|
28
|
-
var range: ClosedRange<Double> = 0...0
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/// A trail dot or a star, where and how it is drawn this frame.
|
|
32
|
-
struct Mark: Equatable {
|
|
33
|
-
/// Along the edge and above it, in points.
|
|
34
|
-
var x, y: Double
|
|
35
|
-
/// Radius in drawing units, opacity, turn in degrees, and which
|
|
36
|
-
/// palette stop colours it.
|
|
37
|
-
var r, alpha, turn: Double
|
|
38
|
-
var shade: Int
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
struct Frame {
|
|
42
|
-
var pose = M.Pose()
|
|
43
|
-
var x = 0.0
|
|
44
|
-
/// -1 turned left, 0 facing you, 1 turned right.
|
|
45
|
-
var face = 0.0
|
|
46
|
-
var level = 0.0
|
|
47
|
-
/// 0…1: the rise and fall of its own voice while speaking.
|
|
48
|
-
var voice = 0.0
|
|
49
|
-
/// Seconds since it appeared, for the marks' own loops.
|
|
50
|
-
var t = 0.0
|
|
51
|
-
var dots: [Mark] = []
|
|
52
|
-
var stars: [Mark] = []
|
|
53
|
-
}
|
|
54
|
-
|
|
55
13
|
/// Points per drawing unit: a body 56 points across.
|
|
56
14
|
let unit: Double
|
|
57
15
|
/// Idle this long, it dozes.
|
|
@@ -82,6 +40,11 @@ struct CharacterSim {
|
|
|
82
40
|
private var strollTo = 0.0, strolled = 0.0
|
|
83
41
|
private var strollNext: Double?
|
|
84
42
|
private var strolls = 0
|
|
43
|
+
/// What stopped the stroll it is on.
|
|
44
|
+
private var met: Double?
|
|
45
|
+
/// Its games with the pointer, and the crouch and hop they give it.
|
|
46
|
+
private(set) var play = PointerPlay()
|
|
47
|
+
private var crouch = 0.0, hop = 0.0
|
|
85
48
|
private var face = 0.0, lean = 0.0
|
|
86
49
|
private var glance = (x: 0.0, y: 0.0)
|
|
87
50
|
|
|
@@ -113,9 +76,28 @@ struct CharacterSim {
|
|
|
113
76
|
// The pointer comes close: out of its way. Left alone: back home,
|
|
114
77
|
// unless the pointer is resting there.
|
|
115
78
|
let near = input.pointer.map { abs($0.x - x) < reach && abs($0.y) < tall } ?? false
|
|
116
|
-
|
|
79
|
+
// Idle with play mode on, a game with the pointer comes first.
|
|
80
|
+
// Sent to show something, it does not play.
|
|
81
|
+
var game = PointerPlay.Out()
|
|
82
|
+
if input.plays && !input.sent && !input.held && !input.shows && mood == .idle && input.activity == .idle {
|
|
83
|
+
game = play.step(now, dt, x: x, pointer: input.pointer, down: input.down, range: input.range)
|
|
84
|
+
} else {
|
|
85
|
+
play.stop(now, down: input.down)
|
|
86
|
+
}
|
|
87
|
+
if input.sent {
|
|
88
|
+
target = min(max(input.home, input.range.lowerBound), input.range.upperBound)
|
|
89
|
+
restSince = now
|
|
90
|
+
// The stroll is over: sent home again, it rests there.
|
|
91
|
+
strolled = 0; strollTo = 0; strollNext = nil
|
|
92
|
+
} else if input.held {
|
|
117
93
|
target = x
|
|
118
94
|
awayUntil = max(awayUntil, now + Self.awayFor)
|
|
95
|
+
} else if let to = game.target {
|
|
96
|
+
target = to
|
|
97
|
+
awayUntil = now + 1
|
|
98
|
+
restSince = now
|
|
99
|
+
// A game ends a stroll, as stepping aside does.
|
|
100
|
+
strolled = 0; strollTo = 0; strollNext = nil
|
|
119
101
|
} else if near {
|
|
120
102
|
target = M.aside(x: x, pointer: input.pointer!.x, clear: Self.clear, range: input.range)
|
|
121
103
|
awayUntil = now + Self.awayFor
|
|
@@ -158,8 +140,19 @@ struct CharacterSim {
|
|
|
158
140
|
let moving = abs(speed) > 40
|
|
159
141
|
let way: Double = speed >= 0 ? 1 : -1
|
|
160
142
|
func toward(_ value: inout Double, _ to: Double, _ rate: Double) { value += (to - value) * (1 - exp(-dt * rate)) }
|
|
161
|
-
|
|
143
|
+
// Stopped at something in its way: it turns to look at it.
|
|
144
|
+
let stopped = strollTo != 0 && abs(strollTo - strolled) < 0.5 ? met : nil
|
|
145
|
+
let turned = stopped.map { $0 > x ? 0.7 : -0.7 } ?? 0
|
|
146
|
+
toward(&face, min(max(speed / 200 + turned, -1), 1), 10)
|
|
162
147
|
toward(&lean, min(max(speed / 480, -1), 1) * 13, 8)
|
|
148
|
+
// A game's crouch and hop are followed, not taken at once, so a
|
|
149
|
+
// game cut short by work does not snap.
|
|
150
|
+
toward(&crouch, game.crouch, 20)
|
|
151
|
+
toward(&hop, game.hop, 20)
|
|
152
|
+
pose.sx *= 1 + 0.14 * crouch
|
|
153
|
+
pose.sy *= 1 - 0.22 * crouch
|
|
154
|
+
pose.lift += hop - 8 * crouch
|
|
155
|
+
if game.caught { burst(now, pose) }
|
|
163
156
|
|
|
164
157
|
// It leaves dots behind while it moves, and stars when it has
|
|
165
158
|
// stepped aside or understood.
|
|
@@ -186,6 +179,7 @@ struct CharacterSim {
|
|
|
186
179
|
let pull = min(far / 60, 1) * 4.5
|
|
187
180
|
look = (dx / far * pull, dy / far * pull)
|
|
188
181
|
}
|
|
182
|
+
if stopped != nil { look = (turned * 5, -2) }
|
|
189
183
|
toward(&glance.x, look.x + face * 2, 9)
|
|
190
184
|
toward(&glance.y, look.y, 9)
|
|
191
185
|
|
|
@@ -229,12 +223,20 @@ struct CharacterSim {
|
|
|
229
223
|
var way: Double = M.rnd(strolls, 2) < 0.5 ? -1 : 1
|
|
230
224
|
if (way > 0 ? room.right : room.left) < far { way = -way }
|
|
231
225
|
strollTo = way * min(far, way > 0 ? room.right : room.left)
|
|
226
|
+
met = nil
|
|
232
227
|
strollNext = now + abs(strollTo) / Self.strollSpeed + 5 + 6 * M.rnd(strolls, 4)
|
|
233
228
|
} else {
|
|
234
229
|
strollTo = 0
|
|
235
230
|
strollNext = nil
|
|
236
231
|
}
|
|
237
232
|
}
|
|
233
|
+
// Something in its way: it goes no further than just short of it.
|
|
234
|
+
// Only on the way out: nothing keeps it from going home.
|
|
235
|
+
if strollTo != 0, strollTo != strolled, let stop = M.stop(from: input.home + strolled, to: input.home + strollTo,
|
|
236
|
+
edges: input.edges, gap: 50 * unit + 10) {
|
|
237
|
+
strollTo = stop.at - input.home
|
|
238
|
+
met = stop.edge
|
|
239
|
+
}
|
|
238
240
|
let pace = Self.strollSpeed * dt
|
|
239
241
|
strolled += min(max(strollTo - strolled, -pace), pace)
|
|
240
242
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// What the character sim is given and returns each frame (CharacterSim.swift).
|
|
4
|
+
extension CharacterSim {
|
|
5
|
+
struct Input {
|
|
6
|
+
var activity = M.Activity.idle
|
|
7
|
+
/// 0…1: the microphone while listening.
|
|
8
|
+
var level = 0.0
|
|
9
|
+
/// The pointer: along the edge, and its height above it. Nil when
|
|
10
|
+
/// it is on another screen.
|
|
11
|
+
var pointer: (x: Double, y: Double)?
|
|
12
|
+
/// The pointer is on its speech bubble: it stays where it is, so
|
|
13
|
+
/// the bubble is not pulled from under the pointer.
|
|
14
|
+
var held = false
|
|
15
|
+
/// The answering agent sent it to show something (#482): it goes
|
|
16
|
+
/// to `home` at once, awake, whatever the pointer did before.
|
|
17
|
+
var sent = false
|
|
18
|
+
/// `voice.stroll`: with nothing to do, it takes a slow stroll
|
|
19
|
+
/// beside where it rests now and then (#482).
|
|
20
|
+
var strolls = false
|
|
21
|
+
/// What stands on its line, along the edge: the sides of the
|
|
22
|
+
/// windows there (#539). On a stroll it stops short of the first.
|
|
23
|
+
var edges: [Double] = []
|
|
24
|
+
/// Its bubble holds something to use (an answer, an error, the
|
|
25
|
+
/// call buttons): no game, so the bubble waits for the hand.
|
|
26
|
+
var shows = false
|
|
27
|
+
/// "Play mode" is ticked: idle, it plays with the pointer (#505).
|
|
28
|
+
var plays = false
|
|
29
|
+
/// The mouse button is down.
|
|
30
|
+
var down = false
|
|
31
|
+
/// Where it rests, and how far it may go.
|
|
32
|
+
var home = 0.0
|
|
33
|
+
var range: ClosedRange<Double> = 0...0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// A trail dot or a star, where and how it is drawn this frame.
|
|
37
|
+
struct Mark: Equatable {
|
|
38
|
+
/// Along the edge and above it, in points.
|
|
39
|
+
var x, y: Double
|
|
40
|
+
/// Radius in drawing units, opacity, turn in degrees, and which
|
|
41
|
+
/// palette stop colours it.
|
|
42
|
+
var r, alpha, turn: Double
|
|
43
|
+
var shade: Int
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
struct Frame {
|
|
47
|
+
var pose = M.Pose()
|
|
48
|
+
var x = 0.0
|
|
49
|
+
/// -1 turned left, 0 facing you, 1 turned right.
|
|
50
|
+
var face = 0.0
|
|
51
|
+
var level = 0.0
|
|
52
|
+
/// 0…1: the rise and fall of its own voice while speaking.
|
|
53
|
+
var voice = 0.0
|
|
54
|
+
/// Seconds since it appeared, for the marks' own loops.
|
|
55
|
+
var t = 0.0
|
|
56
|
+
var dots: [Mark] = []
|
|
57
|
+
var stars: [Mark] = []
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
|
|
3
|
+
/// The character's drawing surface. Flipped, so the drawing's y grows
|
|
4
|
+
/// downwards as in the pose sheet it was ported from.
|
|
5
|
+
final class CharacterView: NSView {
|
|
6
|
+
var shown = CharacterSim.Frame()
|
|
7
|
+
var stops = CharacterDraw.stops(tint: Brand.accent, colors: nil)
|
|
8
|
+
var unit: CGFloat = 0.56
|
|
9
|
+
/// Under the character, in this view; and the view's left edge on screen.
|
|
10
|
+
var edge = CGPoint.zero
|
|
11
|
+
var origin: CGFloat = 0
|
|
12
|
+
/// Its speech bubble in this window, origin bottom-left; nil while hidden.
|
|
13
|
+
var bubble: NSRect?
|
|
14
|
+
|
|
15
|
+
/// A drag of its body. Its window takes the mouse for nothing else.
|
|
16
|
+
enum Drag { case began, moved, ended }
|
|
17
|
+
var onDrag: ((Drag) -> Void)?
|
|
18
|
+
|
|
19
|
+
override var isFlipped: Bool { true }
|
|
20
|
+
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
|
|
21
|
+
override func mouseDown(with event: NSEvent) { onDrag?(.began) }
|
|
22
|
+
override func mouseDragged(with event: NSEvent) { onDrag?(.moved) }
|
|
23
|
+
override func mouseUp(with event: NSEvent) { onDrag?(.ended) }
|
|
24
|
+
|
|
25
|
+
override func draw(_ dirtyRect: NSRect) {
|
|
26
|
+
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
|
|
27
|
+
// The body is drawn where the window is, which follows the frame
|
|
28
|
+
// to the nearest point; the rest of the way is drawn here.
|
|
29
|
+
let at = CGPoint(x: CGFloat(shown.x) - origin, y: edge.y)
|
|
30
|
+
CharacterDraw.draw(shown, in: ctx, edge: at, origin: origin, unit: unit, stops: stops)
|
|
31
|
+
if let bubble { drawTail(in: ctx, from: bubble) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// The bubble's tail: a small point from its bottom edge down to the
|
|
35
|
+
/// character, in the bubble's own colours.
|
|
36
|
+
private func drawTail(in ctx: CGContext, from bubble: NSRect) {
|
|
37
|
+
let half: CGFloat = 7, corner = Brand.Radius.lg + half
|
|
38
|
+
let x = min(max(bounds.midX, bubble.minX + corner), bubble.maxX - corner)
|
|
39
|
+
// One point up into the bubble, so no gap shows between the two.
|
|
40
|
+
let top = bounds.height - bubble.minY - 1
|
|
41
|
+
let tip = CGPoint(x: x, y: top + 1 + PillPlacement.tail)
|
|
42
|
+
effectiveAppearance.performAsCurrentDrawingAppearance {
|
|
43
|
+
ctx.move(to: CGPoint(x: x - half, y: top))
|
|
44
|
+
ctx.addLine(to: tip)
|
|
45
|
+
ctx.addLine(to: CGPoint(x: x + half, y: top))
|
|
46
|
+
ctx.closePath()
|
|
47
|
+
ctx.setFillColor(NSColor.windowBackgroundColor.withAlphaComponent(0.92).cgColor)
|
|
48
|
+
ctx.fillPath()
|
|
49
|
+
ctx.move(to: CGPoint(x: x - half, y: top + 1))
|
|
50
|
+
ctx.addLine(to: tip)
|
|
51
|
+
ctx.addLine(to: CGPoint(x: x + half, y: top + 1))
|
|
52
|
+
ctx.setStrokeColor(NSColor.separatorColor.cgColor)
|
|
53
|
+
ctx.setLineWidth(1)
|
|
54
|
+
ctx.strokePath()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -36,7 +36,8 @@ enum Config {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/// "Play mode" in the menu (#505): the character may play on a frozen
|
|
39
|
-
/// picture of the screen when asked
|
|
39
|
+
/// picture of the screen when asked, and plays with the pointer while
|
|
40
|
+
/// idle. Off by default.
|
|
40
41
|
static var playMode: Bool {
|
|
41
42
|
get { UserDefaults.standard.bool(forKey: "playMode") }
|
|
42
43
|
set { UserDefaults.standard.set(newValue, forKey: "playMode") }
|