agentix-cli 0.96.0 → 0.97.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-voice/Sources/AgentXVoice/Character.swift +4 -1
- package/apps/mac-voice/Sources/AgentXVoice/CharacterSim.swift +50 -4
- package/apps/mac-voice/Sources/AgentXVoice/PlayActs.swift +137 -0
- package/apps/mac-voice/Sources/AgentXVoice/PlayMath.swift +80 -40
- package/apps/mac-voice/Sources/AgentXVoice/PlayStage.swift +18 -3
- package/apps/mac-voice/Sources/AgentXVoice/SettingsLook.swift +6 -0
- package/apps/mac-voice/Sources/AgentXVoice/SettingsWindow.swift +4 -1
- package/apps/mac-voice/Sources/AgentXVoice/VoiceSettings.swift +18 -1
- package/apps/mac-voice/Sources/AgentXVoice/main.swift +24 -10
- package/dist/{App-UHSIO3UE.js → App-6R4I6QPE.js} +2 -2
- package/dist/{ChatApp-LLTFBLJY.js → ChatApp-R3O4BJ24.js} +2 -2
- package/dist/{agent-6RJ633MQ.js → agent-P6GPE7HD.js} +2 -2
- package/dist/{board-dashboard-A3ZEI4F6.js → board-dashboard-AKQHIY4J.js} +4 -4
- package/dist/{chunk-EBVB727L.js → chunk-2GFSHO24.js} +2 -2
- package/dist/{chunk-ALWMYA2B.js → chunk-3S4IYAOC.js} +2 -2
- package/dist/{chunk-76IOF3DK.js → chunk-3SF7PLHF.js} +149 -149
- package/dist/chunk-3SF7PLHF.js.map +1 -0
- package/dist/{chunk-MKF6HSBA.js → chunk-4BG3WJSP.js} +2 -2
- package/dist/{chunk-DU6536CC.js → chunk-55HM3SUN.js} +2 -2
- package/dist/{chunk-ZVW645NU.js → chunk-6W3SCPDK.js} +2 -2
- package/dist/{chunk-J2MHITWR.js → chunk-C2POHIWJ.js} +2 -2
- package/dist/{chunk-6NECC6YS.js → chunk-DGVTRG5Q.js} +2 -2
- package/dist/{chunk-XEKZKH3W.js → chunk-ERPEU2ZR.js} +2 -2
- package/dist/{chunk-XEKZKH3W.js.map → chunk-ERPEU2ZR.js.map} +1 -1
- package/dist/{chunk-ZYJDAT3E.js → chunk-F3DW5ZJN.js} +2 -2
- package/dist/{chunk-QZIEYDIY.js → chunk-LXHTVVJV.js} +2 -2
- package/dist/{chunk-TE44E733.js → chunk-MDFUSQ6S.js} +2 -2
- package/dist/{chunk-4XHKJZLJ.js → chunk-Q53XKCRL.js} +2 -2
- package/dist/{chunk-VO2GNAKB.js → chunk-QHAO6U7U.js} +2 -2
- package/dist/{chunk-ZZSNM2RI.js → chunk-QPIMPKZ2.js} +2 -2
- package/dist/{chunk-KIGFWUKP.js → chunk-SZ5TGL25.js} +2 -2
- package/dist/{chunk-GHXSBJZS.js → chunk-TOGRRWBX.js} +2 -2
- package/dist/{chunk-6DCY2JCB.js → chunk-TWC4CYTU.js} +5 -5
- package/dist/{chunk-KEKO4S45.js → chunk-WLKO6FOL.js} +2 -2
- package/dist/{chunk-44UGVUN7.js → chunk-X4S2I233.js} +2 -2
- package/dist/{chunk-IPDEWVVT.js → chunk-ZCQCUA4J.js} +2 -2
- package/dist/cli.js +1 -1
- package/dist/{client-ARS6ZFT2.js → client-UQKCIEHA.js} +2 -2
- package/dist/{config-XO5Z3BO5.js → config-OE23UC5C.js} +2 -2
- package/dist/config-mutator-2UK5GX4B.js +2 -0
- package/dist/{doctor-VUK3JE6Z.js → doctor-QQ7YLJK4.js} +2 -2
- package/dist/index.d.ts +10 -1
- package/dist/index.js +1 -1
- package/dist/{program-QO72D2EL.js → program-O5RQ6NRG.js} +117 -117
- package/dist/program-O5RQ6NRG.js.map +1 -0
- package/dist/{promote-UGQXA4KV.js → promote-AW4TJXXT.js} +2 -2
- package/dist/{query-DL7RHPMA.js → query-24BGIJCZ.js} +2 -2
- package/dist/{registry-YIPKBRBH.js → registry-GZT2OSBK.js} +2 -2
- package/dist/{request-planner-L2FAVNKP.js → request-planner-U6HHKWLX.js} +2 -2
- package/dist/{review-candidates-RPUP4XBU.js → review-candidates-LJUJCVFA.js} +2 -2
- package/dist/{routing-G66SVTRO.js → routing-WCYPBKFH.js} +2 -2
- package/dist/schedule-tool-E7ER2XOI.js +2 -0
- package/dist/scheduler-FK3LOWU2.js +2 -0
- package/dist/{seat-ZPH7DA5L.js → seat-5PBTFTGS.js} +2 -2
- package/dist/{select-2WTV2BI2.js → select-JD7MSY25.js} +2 -2
- package/dist/{set-enabled-KI2FINQD.js → set-enabled-MUSO2KJL.js} +2 -2
- package/dist/{tool-2P4LRWN4.js → tool-FMKGK474.js} +2 -2
- package/dist/{tool-ZFQ46KU3.js → tool-Y7SOGJ2S.js} +2 -2
- package/dist/wiki-SJRTAYMT.js +2 -0
- package/package.json +1 -1
- package/dist/chunk-76IOF3DK.js.map +0 -1
- package/dist/config-mutator-MJX4EIYV.js +0 -2
- package/dist/program-QO72D2EL.js.map +0 -1
- package/dist/schedule-tool-FOWPIWKP.js +0 -2
- package/dist/scheduler-GFVUPAD6.js +0 -2
- package/dist/wiki-XATGT3DI.js +0 -2
- /package/dist/{App-UHSIO3UE.js.map → App-6R4I6QPE.js.map} +0 -0
- /package/dist/{ChatApp-LLTFBLJY.js.map → ChatApp-R3O4BJ24.js.map} +0 -0
- /package/dist/{agent-6RJ633MQ.js.map → agent-P6GPE7HD.js.map} +0 -0
- /package/dist/{board-dashboard-A3ZEI4F6.js.map → board-dashboard-AKQHIY4J.js.map} +0 -0
- /package/dist/{chunk-EBVB727L.js.map → chunk-2GFSHO24.js.map} +0 -0
- /package/dist/{chunk-ALWMYA2B.js.map → chunk-3S4IYAOC.js.map} +0 -0
- /package/dist/{chunk-MKF6HSBA.js.map → chunk-4BG3WJSP.js.map} +0 -0
- /package/dist/{chunk-DU6536CC.js.map → chunk-55HM3SUN.js.map} +0 -0
- /package/dist/{chunk-ZVW645NU.js.map → chunk-6W3SCPDK.js.map} +0 -0
- /package/dist/{chunk-J2MHITWR.js.map → chunk-C2POHIWJ.js.map} +0 -0
- /package/dist/{chunk-6NECC6YS.js.map → chunk-DGVTRG5Q.js.map} +0 -0
- /package/dist/{chunk-ZYJDAT3E.js.map → chunk-F3DW5ZJN.js.map} +0 -0
- /package/dist/{chunk-QZIEYDIY.js.map → chunk-LXHTVVJV.js.map} +0 -0
- /package/dist/{chunk-TE44E733.js.map → chunk-MDFUSQ6S.js.map} +0 -0
- /package/dist/{chunk-4XHKJZLJ.js.map → chunk-Q53XKCRL.js.map} +0 -0
- /package/dist/{chunk-VO2GNAKB.js.map → chunk-QHAO6U7U.js.map} +0 -0
- /package/dist/{chunk-ZZSNM2RI.js.map → chunk-QPIMPKZ2.js.map} +0 -0
- /package/dist/{chunk-KIGFWUKP.js.map → chunk-SZ5TGL25.js.map} +0 -0
- /package/dist/{chunk-GHXSBJZS.js.map → chunk-TOGRRWBX.js.map} +0 -0
- /package/dist/{chunk-6DCY2JCB.js.map → chunk-TWC4CYTU.js.map} +0 -0
- /package/dist/{chunk-KEKO4S45.js.map → chunk-WLKO6FOL.js.map} +0 -0
- /package/dist/{chunk-44UGVUN7.js.map → chunk-X4S2I233.js.map} +0 -0
- /package/dist/{chunk-IPDEWVVT.js.map → chunk-ZCQCUA4J.js.map} +0 -0
- /package/dist/{client-ARS6ZFT2.js.map → client-UQKCIEHA.js.map} +0 -0
- /package/dist/{config-XO5Z3BO5.js.map → config-OE23UC5C.js.map} +0 -0
- /package/dist/{config-mutator-MJX4EIYV.js.map → config-mutator-2UK5GX4B.js.map} +0 -0
- /package/dist/{doctor-VUK3JE6Z.js.map → doctor-QQ7YLJK4.js.map} +0 -0
- /package/dist/{promote-UGQXA4KV.js.map → promote-AW4TJXXT.js.map} +0 -0
- /package/dist/{query-DL7RHPMA.js.map → query-24BGIJCZ.js.map} +0 -0
- /package/dist/{registry-YIPKBRBH.js.map → registry-GZT2OSBK.js.map} +0 -0
- /package/dist/{request-planner-L2FAVNKP.js.map → request-planner-U6HHKWLX.js.map} +0 -0
- /package/dist/{review-candidates-RPUP4XBU.js.map → review-candidates-LJUJCVFA.js.map} +0 -0
- /package/dist/{routing-G66SVTRO.js.map → routing-WCYPBKFH.js.map} +0 -0
- /package/dist/{schedule-tool-FOWPIWKP.js.map → schedule-tool-E7ER2XOI.js.map} +0 -0
- /package/dist/{scheduler-GFVUPAD6.js.map → scheduler-FK3LOWU2.js.map} +0 -0
- /package/dist/{seat-ZPH7DA5L.js.map → seat-5PBTFTGS.js.map} +0 -0
- /package/dist/{select-2WTV2BI2.js.map → select-JD7MSY25.js.map} +0 -0
- /package/dist/{set-enabled-KI2FINQD.js.map → set-enabled-MUSO2KJL.js.map} +0 -0
- /package/dist/{tool-2P4LRWN4.js.map → tool-FMKGK474.js.map} +0 -0
- /package/dist/{tool-ZFQ46KU3.js.map → tool-Y7SOGJ2S.js.map} +0 -0
- /package/dist/{wiki-XATGT3DI.js.map → wiki-SJRTAYMT.js.map} +0 -0
|
@@ -113,6 +113,9 @@ final class CharacterHost {
|
|
|
113
113
|
redraw()
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/// `voice.stroll`: a slow walk now and then while it has nothing to do.
|
|
117
|
+
var strolls = false
|
|
118
|
+
|
|
116
119
|
/// "Animated orb" in the menu.
|
|
117
120
|
func setAnimated(_ on: Bool) {
|
|
118
121
|
guard animated != on else { return }
|
|
@@ -217,7 +220,7 @@ final class CharacterHost {
|
|
|
217
220
|
|| (bubbleFrame?.insetBy(dx: 0, dy: -PillPlacement.tail).contains(mouse) ?? false)
|
|
218
221
|
|| (NSEvent.modifierFlags.contains(.command) && window.frame.contains(mouse))
|
|
219
222
|
frame = sim.step(to: ProcessInfo.processInfo.systemUptime,
|
|
220
|
-
CharacterSim.Input(activity: activity, level: level, pointer: pointer, held: held,
|
|
223
|
+
CharacterSim.Input(activity: activity, level: level, pointer: pointer, held: held, strolls: strolls,
|
|
221
224
|
home: home, range: Double(spot.ends.lowerBound)...Double(spot.ends.upperBound)))
|
|
222
225
|
}
|
|
223
226
|
let origin = NSPoint(x: (CGFloat(frame.x) - Self.size.width / 2).rounded(), y: (rest.y - Self.ground).rounded())
|
|
@@ -20,6 +20,9 @@ struct CharacterSim {
|
|
|
20
20
|
/// The pointer is on its speech bubble: it stays where it is, so
|
|
21
21
|
/// the bubble is not pulled from under the pointer.
|
|
22
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
|
|
23
26
|
/// Where it rests, and how far it may go.
|
|
24
27
|
var home = 0.0
|
|
25
28
|
var range: ClosedRange<Double> = 0...0
|
|
@@ -56,6 +59,9 @@ struct CharacterSim {
|
|
|
56
59
|
/// How far it keeps from the pointer, and how long before it comes back.
|
|
57
60
|
static let clear = 150.0
|
|
58
61
|
static let awayFor = 4.0
|
|
62
|
+
/// A stroll: how far from where it rests, at most, and how fast.
|
|
63
|
+
static let strollReach = 110.0
|
|
64
|
+
static let strollSpeed = 28.0
|
|
59
65
|
|
|
60
66
|
private(set) var mood = M.Mood.idle
|
|
61
67
|
private var goal = M.Mood.idle
|
|
@@ -71,6 +77,11 @@ struct CharacterSim {
|
|
|
71
77
|
private var target = 0.0
|
|
72
78
|
private var awayUntil = 0.0
|
|
73
79
|
private var steppedAside = false
|
|
80
|
+
/// A stroll: how far from home it is going and has got, when it turns
|
|
81
|
+
/// round or sets off again, and how many it has taken.
|
|
82
|
+
private var strollTo = 0.0, strolled = 0.0
|
|
83
|
+
private var strollNext: Double?
|
|
84
|
+
private var strolls = 0
|
|
74
85
|
private var face = 0.0, lean = 0.0
|
|
75
86
|
private var glance = (x: 0.0, y: 0.0)
|
|
76
87
|
|
|
@@ -110,11 +121,15 @@ struct CharacterSim {
|
|
|
110
121
|
awayUntil = now + Self.awayFor
|
|
111
122
|
restSince = now
|
|
112
123
|
steppedAside = true
|
|
124
|
+
// The stroll is over: left alone it goes home, not back to
|
|
125
|
+
// where the pointer met it.
|
|
126
|
+
strolled = 0; strollTo = 0; strollNext = nil
|
|
113
127
|
} else if now >= awayUntil {
|
|
114
128
|
let taken = input.pointer.map { abs($0.x - input.home) < Self.clear && abs($0.y) < tall } ?? false
|
|
115
129
|
if !taken {
|
|
116
130
|
if target != input.home { steppedAside = false }
|
|
117
|
-
|
|
131
|
+
stroll(now, dt, input)
|
|
132
|
+
target = min(max(input.home + strolled, input.range.lowerBound), input.range.upperBound)
|
|
118
133
|
}
|
|
119
134
|
}
|
|
120
135
|
|
|
@@ -197,16 +212,47 @@ struct CharacterSim {
|
|
|
197
212
|
dots: dots.compactMap { dot(at: now, $0) }, stars: bursts.flatMap { stars(at: now, $0) })
|
|
198
213
|
}
|
|
199
214
|
|
|
200
|
-
///
|
|
215
|
+
/// With nothing to do, a slow walk a little way from home, a wait
|
|
216
|
+
/// there, and a slow walk back: slow enough to leave no trail. Any
|
|
217
|
+
/// work, or dozing off, takes it home the same way.
|
|
218
|
+
private mutating func stroll(_ now: Double, _ dt: Double, _ input: Input) {
|
|
219
|
+
if !input.strolls || mood != .idle {
|
|
220
|
+
strollTo = 0; strollNext = nil
|
|
221
|
+
} else if strollNext == nil {
|
|
222
|
+
strollNext = now + 25 + 30 * M.rnd(strolls, 3)
|
|
223
|
+
} else if now >= strollNext! {
|
|
224
|
+
if strollTo == 0 {
|
|
225
|
+
strolls += 1
|
|
226
|
+
let far = 40 + (Self.strollReach - 40) * M.rnd(strolls, 1)
|
|
227
|
+
let room = (left: input.home - input.range.lowerBound, right: input.range.upperBound - input.home)
|
|
228
|
+
// To the side picked, unless only the other has the room.
|
|
229
|
+
var way: Double = M.rnd(strolls, 2) < 0.5 ? -1 : 1
|
|
230
|
+
if (way > 0 ? room.right : room.left) < far { way = -way }
|
|
231
|
+
strollTo = way * min(far, way > 0 ? room.right : room.left)
|
|
232
|
+
strollNext = now + abs(strollTo) / Self.strollSpeed + 5 + 6 * M.rnd(strolls, 4)
|
|
233
|
+
} else {
|
|
234
|
+
strollTo = 0
|
|
235
|
+
strollNext = nil
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
let pace = Self.strollSpeed * dt
|
|
239
|
+
strolled += min(max(strollTo - strolled, -pace), pace)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Carried by the pointer (#502): it is where it is put, at once, and
|
|
243
|
+
/// a stroll it was on is over.
|
|
201
244
|
mutating func carry(to place: Double) {
|
|
202
245
|
x = place; target = place; speed = 0
|
|
246
|
+
strolled = 0; strollTo = 0; strollNext = nil
|
|
203
247
|
}
|
|
204
248
|
|
|
205
249
|
/// The state alone, where it rests: for Reduce Motion, which shows a
|
|
206
250
|
/// still picture that changes between states and never a loop.
|
|
207
251
|
static func still(_ activity: M.Activity, home: Double) -> Frame {
|
|
208
|
-
// Its marks at full strength, so each state reads without motion
|
|
209
|
-
|
|
252
|
+
// Its marks at full strength, so each state reads without motion:
|
|
253
|
+
// the arcs of its voice only while it speaks.
|
|
254
|
+
let pose = M.pose(M.mood(for: activity))
|
|
255
|
+
return Frame(pose: pose, x: home, level: 1, voice: pose.speak)
|
|
210
256
|
}
|
|
211
257
|
|
|
212
258
|
private mutating func go(_ to: M.Mood, _ now: Double) {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Play mode past the first script (#505): pieces of the picture that
|
|
4
|
+
/// move (a kicked letter, a word that falls, a word carried away), and a
|
|
5
|
+
/// different play each time. Numbers only, like `PlayMath`.
|
|
6
|
+
extension PlayMath {
|
|
7
|
+
/// A piece cut out of the picture and drawn somewhere else: which
|
|
8
|
+
/// part of the picture, where its top-left corner is now, and how far
|
|
9
|
+
/// it has turned (radians).
|
|
10
|
+
struct Piece: Equatable {
|
|
11
|
+
var from: Rect
|
|
12
|
+
var at: Point
|
|
13
|
+
var turn = 0.0
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// A piece from the moment it leaves its place. Its place is covered
|
|
17
|
+
/// with the page's colour from then on.
|
|
18
|
+
struct Cut {
|
|
19
|
+
enum Path {
|
|
20
|
+
/// Thrown at this speed (points a second) and turning (about
|
|
21
|
+
/// so many radians a second); it falls and stays on the bottom
|
|
22
|
+
/// of the picture, `rise` points up so a pile is not one row.
|
|
23
|
+
case thrown(v: Point, spin: Double, rise: Double)
|
|
24
|
+
/// Lifted above the character's head, kept there until
|
|
25
|
+
/// `until`, then put down with its corner at `to`.
|
|
26
|
+
case held(until: Double, to: Point)
|
|
27
|
+
}
|
|
28
|
+
var source: Rect
|
|
29
|
+
var paper: UInt32
|
|
30
|
+
var start: Double
|
|
31
|
+
var path: Path
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Points a second each second, and the seconds to lift a word or put
|
|
35
|
+
/// it down.
|
|
36
|
+
static let gravity = 1400.0
|
|
37
|
+
static let lift = 0.35
|
|
38
|
+
|
|
39
|
+
/// Where a thrown piece is `seconds` after it left. It stops on the
|
|
40
|
+
/// bottom of the picture and at its sides.
|
|
41
|
+
static func thrown(_ source: Rect, v: Point, spin: Double, rise: Double, after seconds: Double,
|
|
42
|
+
width: Double, height: Double) -> Piece {
|
|
43
|
+
let floor = max(height - source.h - rise, source.y)
|
|
44
|
+
let landing = (-v.y + (v.y * v.y + 2 * gravity * (floor - source.y)).squareRoot()) / gravity
|
|
45
|
+
let s = min(max(seconds, 0), landing)
|
|
46
|
+
let x = min(max(source.x + v.x * s, 0), max(width - source.w, 0))
|
|
47
|
+
// It turns about as fast as `spin`, and lands flat: the right way
|
|
48
|
+
// up or upside down.
|
|
49
|
+
let flat = (spin * landing / .pi).rounded() * .pi
|
|
50
|
+
return Piece(from: source, at: Point(x: x, y: source.y + v.y * s + gravity * s * s / 2), turn: landing > 0 ? flat * s / landing : 0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// What it can do to a line.
|
|
54
|
+
enum Act: CaseIterable { case eat, wipe, kick, stomp, carry }
|
|
55
|
+
|
|
56
|
+
/// The same numbers for the same seed, so a play can be told again
|
|
57
|
+
/// and tested (SplitMix64).
|
|
58
|
+
struct Seeded: RandomNumberGenerator {
|
|
59
|
+
var state: UInt64
|
|
60
|
+
init(_ seed: UInt64) { state = seed }
|
|
61
|
+
mutating func next() -> UInt64 {
|
|
62
|
+
state &+= 0x9E37_79B9_7F4A_7C15
|
|
63
|
+
var z = state
|
|
64
|
+
z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9
|
|
65
|
+
z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB
|
|
66
|
+
return z ^ (z >> 31)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// A play, different for each seed: it jumps onto one of the lines
|
|
71
|
+
/// near the middle of the page and walks it, then does one thing to
|
|
72
|
+
/// each of up to four others, never the same thing twice. Lines too
|
|
73
|
+
/// close to the top to stand on, too short or of one word are left
|
|
74
|
+
/// alone.
|
|
75
|
+
static func script(_ lines: [Line], width: Double, height: Double, seed: UInt64) -> [Step] {
|
|
76
|
+
var rng = Seeded(seed)
|
|
77
|
+
let fit = lines.indices.filter { lines[$0].words.count >= 2 && lines[$0].rect.w >= 120 && lines[$0].rect.y >= top + 40 }
|
|
78
|
+
func far(_ i: Int) -> Double { hypot(lines[i].rect.midX - width / 2, lines[i].rect.midY - height / 2) }
|
|
79
|
+
let near = fit.sorted { far($0) < far($1) }.prefix(8).shuffled(using: &rng)
|
|
80
|
+
guard let first = near.first else { return [] }
|
|
81
|
+
let acts = Act.allCases.shuffled(using: &rng)
|
|
82
|
+
var steps: [Step] = [.jump(line: first), .walk(line: first)]
|
|
83
|
+
// With one line only, it plays on the line it walked.
|
|
84
|
+
let stage = near.count > 1 ? Array(near.dropFirst().prefix(4)) : [first]
|
|
85
|
+
for (k, i) in stage.enumerated() {
|
|
86
|
+
let n = lines[i].words.count, word = Int.random(in: 0..<n, using: &rng)
|
|
87
|
+
switch acts[k] {
|
|
88
|
+
case .eat:
|
|
89
|
+
let from = Int.random(in: 0..<n - 1, using: &rng)
|
|
90
|
+
steps.append(.eat(line: i, words: from..<min(from + 3, n)))
|
|
91
|
+
case .wipe: steps.append(.wipe(line: i))
|
|
92
|
+
case .stomp: steps.append(.stomp(line: i))
|
|
93
|
+
// The walked line is whole: the word goes to its end.
|
|
94
|
+
case .carry where i != first: steps.append(.carry(line: i, word: word, to: first))
|
|
95
|
+
case .kick, .carry: steps.append(.kick(line: i, word: word))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return steps + [.rest(1.2)]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
extension Play {
|
|
103
|
+
/// The pieces that have left their place by this frame: each place is
|
|
104
|
+
/// covered, and the piece is drawn where it is now.
|
|
105
|
+
func move(_ f: inout Frame) {
|
|
106
|
+
for cut in cuts where cut.start <= f.t {
|
|
107
|
+
f.gone.append((cover(cut.source), cut.paper))
|
|
108
|
+
switch cut.path {
|
|
109
|
+
case .thrown(let v, let spin, let rise):
|
|
110
|
+
f.pieces.append(P.thrown(cut.source, v: v, spin: spin, rise: rise, after: f.t - cut.start, width: size.x, height: size.y))
|
|
111
|
+
case .held(let until, let to):
|
|
112
|
+
// From its place to above the head, and from there down.
|
|
113
|
+
let head = P.Point(x: f.at.x - cut.source.w / 2, y: f.at.y - P.top - cut.source.h)
|
|
114
|
+
let a = f.t < until ? P.Point(x: cut.source.x, y: cut.source.y) : head, b = f.t < until ? head : to
|
|
115
|
+
let q = M.ease(f.t < until ? M.span(f.t, cut.start, cut.start + P.lift) : M.span(f.t, until, until + P.lift))
|
|
116
|
+
f.pieces.append(P.Piece(from: cut.source, at: P.Point(x: a.x + (b.x - a.x) * q, y: a.y + (b.y - a.y) * q)))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Which way it faces during a move, and its pose without the motion.
|
|
122
|
+
func look(_ m: Move) -> (face: Double, pose: M.Pose) {
|
|
123
|
+
let way: Double = m.to.x >= m.from.x ? 1 : -1
|
|
124
|
+
switch m.kind {
|
|
125
|
+
case .jump: return (way, M.Pose(tilt: 6 * way, gy: -2))
|
|
126
|
+
case .walk: return (way, M.Pose(tilt: 7 * way))
|
|
127
|
+
case .eat: return (way, M.Pose(size: 1.12))
|
|
128
|
+
case .wipe: return (way, M.Pose(tilt: 12 * way, gy: 4, lid: 0.3))
|
|
129
|
+
case .kick: return (1, M.Pose(gy: 3, size: 1.08))
|
|
130
|
+
case .stomp: return (0, M.Pose(gy: 5))
|
|
131
|
+
case .fall: return (0, M.Pose(gy: -4, size: 1.22))
|
|
132
|
+
case .lift, .drop: return (1, M.Pose(gy: -5))
|
|
133
|
+
case .carry: return (way, M.Pose(tilt: 6 * way, gy: -5))
|
|
134
|
+
case .rest: return (0, M.Pose())
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -2,8 +2,8 @@ import Foundation
|
|
|
2
2
|
|
|
3
3
|
/// Play mode (#505): the character leaves the bottom edge and plays on a
|
|
4
4
|
/// frozen picture of the screen. It walks along a line of text as on a
|
|
5
|
-
/// floor, jumps to another line, eats words
|
|
6
|
-
/// the numbers only: where the words are, what it does and when, where it
|
|
5
|
+
/// floor, jumps to another line, eats words, wipes a line, kicks a word,
|
|
6
|
+
/// stomps a line down and carries a word away. These are the numbers only: where the words are, what it does and when, where it
|
|
7
7
|
/// is at a given moment and which letters are gone. Foundation only, so
|
|
8
8
|
/// the tests can run it without a window.
|
|
9
9
|
///
|
|
@@ -44,6 +44,12 @@ enum PlayMath {
|
|
|
44
44
|
case eat(line: Int, words: Range<Int>)
|
|
45
45
|
/// Wipe a whole line with a cloth.
|
|
46
46
|
case wipe(line: Int)
|
|
47
|
+
/// Kick a word: its letters fly and pile up at the bottom.
|
|
48
|
+
case kick(line: Int, word: Int)
|
|
49
|
+
/// Hop on a line until its words drop, and fall after them.
|
|
50
|
+
case stomp(line: Int)
|
|
51
|
+
/// Lift a word, carry it to the end of another line, put it down.
|
|
52
|
+
case carry(line: Int, word: Int, to: Int)
|
|
47
53
|
/// Stand still, facing you, for so many seconds.
|
|
48
54
|
case rest(Double)
|
|
49
55
|
}
|
|
@@ -76,25 +82,6 @@ enum PlayMath {
|
|
|
76
82
|
guard let best = count.max(by: { ($0.value, $0.key) < ($1.value, $1.key) })?.key else { return nil }
|
|
77
83
|
return samples.first { $0 & 0xF8F8F8 == best }
|
|
78
84
|
}
|
|
79
|
-
|
|
80
|
-
/// The built-in script: the line nearest the middle of the page and
|
|
81
|
-
/// the two under it in the same column. It walks the first, eats the
|
|
82
|
-
/// start of the second and wipes the third. Lines too close to the top
|
|
83
|
-
/// to stand on, too short or of one word are left alone.
|
|
84
|
-
static func demo(_ lines: [Line], width: Double, height: Double) -> [Step] {
|
|
85
|
-
let fit = lines.indices.filter { lines[$0].words.count >= 2 && lines[$0].rect.w >= 120 && lines[$0].rect.y >= top + 40 }
|
|
86
|
-
func far(_ i: Int) -> Double { hypot(lines[i].rect.midX - width / 2, lines[i].rect.midY - height / 2) }
|
|
87
|
-
guard let first = fit.min(by: { far($0) < far($1) }) else { return [] }
|
|
88
|
-
let a = lines[first].rect
|
|
89
|
-
let under = fit.filter { lines[$0].rect.y > a.y + a.h / 2 && lines[$0].rect.x < a.maxX && lines[$0].rect.maxX > a.x }
|
|
90
|
-
.sorted { lines[$0].rect.y < lines[$1].rect.y }
|
|
91
|
-
let picked = [first] + under.prefix(2)
|
|
92
|
-
var steps: [Step] = [.jump(line: first), .walk(line: first)]
|
|
93
|
-
if picked.count > 1 { steps += [.eat(line: picked[1], words: 0..<min(3, lines[picked[1]].words.count))] }
|
|
94
|
-
else { steps += [.eat(line: first, words: 0..<min(2, lines[first].words.count))] }
|
|
95
|
-
if picked.count > 2 { steps += [.wipe(line: picked[2])] }
|
|
96
|
-
return steps + [.rest(1.2)]
|
|
97
|
-
}
|
|
98
85
|
}
|
|
99
86
|
|
|
100
87
|
/// A script laid out in time: every move has its start and its end, so
|
|
@@ -104,7 +91,7 @@ struct Play {
|
|
|
104
91
|
typealias P = PlayMath
|
|
105
92
|
typealias M = CharacterMath
|
|
106
93
|
|
|
107
|
-
enum Kind { case jump, walk, eat, wipe, rest }
|
|
94
|
+
enum Kind { case jump, walk, eat, wipe, kick, stomp, fall, lift, carry, drop, rest }
|
|
108
95
|
|
|
109
96
|
struct Move {
|
|
110
97
|
var kind: Kind
|
|
@@ -126,17 +113,23 @@ struct Play {
|
|
|
126
113
|
var gone: [(rect: P.Rect, paper: UInt32)] = []
|
|
127
114
|
/// The cloth, while it wipes.
|
|
128
115
|
var cloth: P.Rect?
|
|
116
|
+
/// What was kicked, dropped or carried, where it is now.
|
|
117
|
+
var pieces: [P.Piece] = []
|
|
129
118
|
var done = false
|
|
130
119
|
}
|
|
131
120
|
|
|
132
121
|
let lines: [P.Line]
|
|
122
|
+
/// The picture's width and height: where a thrown piece stops.
|
|
123
|
+
let size: P.Point
|
|
133
124
|
private(set) var moves: [Move] = []
|
|
125
|
+
private(set) var cuts: [P.Cut] = []
|
|
134
126
|
var duration: Double { moves.last?.end ?? 0 }
|
|
135
127
|
|
|
136
128
|
/// `start` is where its feet are when play begins. Steps that name a
|
|
137
129
|
/// line or a word that is not there are skipped.
|
|
138
|
-
init(lines: [P.Line], steps: [P.Step], start: P.Point) {
|
|
130
|
+
init(lines: [P.Line], steps: [P.Step], start: P.Point, size: P.Point) {
|
|
139
131
|
self.lines = lines
|
|
132
|
+
self.size = size
|
|
140
133
|
var at = start, now = 0.0
|
|
141
134
|
func add(_ kind: Kind, to: P.Point, _ seconds: Double, line: Int = 0, words: Range<Int> = 0..<0) -> Move {
|
|
142
135
|
Move(kind: kind, from: at, to: to, start: now, duration: seconds, line: line, words: words)
|
|
@@ -147,7 +140,18 @@ struct Play {
|
|
|
147
140
|
let m = add(.jump, to: to, 0.5 + hypot(to.x - at.x, to.y - at.y) / 900)
|
|
148
141
|
list.append(m); at = to; now = m.end
|
|
149
142
|
}
|
|
150
|
-
var list: [Move] = []
|
|
143
|
+
var list: [Move] = [], cuts: [P.Cut] = []
|
|
144
|
+
/// A move on the spot.
|
|
145
|
+
func stay(_ kind: Kind, _ seconds: Double, line: Int) -> Move {
|
|
146
|
+
let m = add(kind, to: at, seconds, line: line)
|
|
147
|
+
list.append(m); now = m.end
|
|
148
|
+
return m
|
|
149
|
+
}
|
|
150
|
+
/// A piece falls: each one a little higher on the pile.
|
|
151
|
+
func throwing(_ source: P.Rect, line: Int, at start: Double, v: P.Point, spin: Double) {
|
|
152
|
+
cuts.append(P.Cut(source: source, paper: lines[line].paper, start: start,
|
|
153
|
+
path: .thrown(v: v, spin: spin, rise: Double(cuts.count % 4) * 4)))
|
|
154
|
+
}
|
|
151
155
|
for step in steps {
|
|
152
156
|
switch step {
|
|
153
157
|
case .jump(let i):
|
|
@@ -174,12 +178,49 @@ struct Play {
|
|
|
174
178
|
reach(P.Point(x: r.x, y: r.y), into: &list)
|
|
175
179
|
let m = add(.wipe, to: P.Point(x: r.maxX, y: r.y), r.w / P.wipeSpeed, line: i)
|
|
176
180
|
list.append(m); at = m.to; now = m.end
|
|
181
|
+
case .kick(let i, let w):
|
|
182
|
+
guard lines.indices.contains(i), lines[i].words.indices.contains(w) else { continue }
|
|
183
|
+
let word = lines[i].words[w]
|
|
184
|
+
reach(P.Point(x: word.rect.x - P.mouth - 4, y: lines[i].rect.midY + P.middle), into: &list)
|
|
185
|
+
let m = stay(.kick, 0.6, line: i)
|
|
186
|
+
for (k, letter) in P.letters(of: word).enumerated() {
|
|
187
|
+
throwing(letter, line: i, at: m.start + 0.25, v: P.Point(x: 150 + 40 * Double(k), y: -320 - 30 * Double(k % 3)),
|
|
188
|
+
spin: 4 + Double(k % 3))
|
|
189
|
+
}
|
|
190
|
+
case .stomp(let i):
|
|
191
|
+
guard lines.indices.contains(i) else { continue }
|
|
192
|
+
let r = lines[i].rect
|
|
193
|
+
reach(P.Point(x: r.midX, y: r.y), into: &list)
|
|
194
|
+
let m = stay(.stomp, 1.2, line: i)
|
|
195
|
+
for (k, word) in lines[i].words.enumerated() {
|
|
196
|
+
let side: Double = k % 2 == 0 ? -1 : 1
|
|
197
|
+
throwing(word.rect, line: i, at: m.start + 0.4 + 0.8 * Double(k) / Double(lines[i].words.count),
|
|
198
|
+
v: P.Point(x: 50 * side, y: -90), spin: 5 * side)
|
|
199
|
+
}
|
|
200
|
+
// Its floor is gone: it falls after the words, slowly
|
|
201
|
+
// enough to follow with the eye.
|
|
202
|
+
let floor = P.Point(x: at.x, y: max(size.y - 6, at.y)), drop = floor.y - at.y
|
|
203
|
+
let fall = add(.fall, to: floor, max((2 * drop / P.gravity).squareRoot(), drop / 600), line: i)
|
|
204
|
+
list.append(fall); at = floor; now = fall.end
|
|
205
|
+
case .carry(let i, let w, let j):
|
|
206
|
+
guard lines.indices.contains(i), lines[i].words.indices.contains(w), lines.indices.contains(j), j != i else { continue }
|
|
207
|
+
let word = lines[i].words[w].rect, home = lines[j].rect
|
|
208
|
+
reach(P.Point(x: word.x - P.mouth - 4, y: lines[i].rect.midY + P.middle), into: &list)
|
|
209
|
+
let up = stay(.lift, P.lift, line: i)
|
|
210
|
+
let m = add(.carry, to: P.Point(x: home.maxX - 4, y: home.y), 0.5 + hypot(home.maxX - 4 - at.x, home.y - at.y) / 900, line: j)
|
|
211
|
+
list.append(m); at = m.to; now = m.end
|
|
212
|
+
let down = stay(.drop, P.lift, line: j)
|
|
213
|
+
// After the line, or on top of its end when the page stops there.
|
|
214
|
+
let after = home.maxX + 8 + word.w <= size.x
|
|
215
|
+
let to = after ? P.Point(x: home.maxX + 8, y: home.midY - word.h / 2) : P.Point(x: home.maxX - word.w, y: home.y - word.h - 2)
|
|
216
|
+
cuts.append(P.Cut(source: word, paper: lines[i].paper, start: up.start, path: .held(until: down.start, to: to)))
|
|
177
217
|
case .rest(let seconds):
|
|
178
218
|
let m = add(.rest, to: at, max(seconds, 0))
|
|
179
219
|
list.append(m); now = m.end
|
|
180
220
|
}
|
|
181
221
|
}
|
|
182
222
|
moves = list
|
|
223
|
+
self.cuts = cuts
|
|
183
224
|
}
|
|
184
225
|
|
|
185
226
|
func frame(at t: Double) -> Frame {
|
|
@@ -188,10 +229,16 @@ struct Play {
|
|
|
188
229
|
var f = Frame(t: t, done: t >= duration)
|
|
189
230
|
|
|
190
231
|
f.at = P.Point(x: m.from.x + (m.to.x - m.from.x) * p, y: m.from.y + (m.to.y - m.from.y) * p)
|
|
191
|
-
|
|
232
|
+
switch m.kind {
|
|
233
|
+
case .jump, .carry:
|
|
192
234
|
// An arc over both ends, kept inside the picture.
|
|
193
235
|
let rise = 30 + hypot(m.to.x - m.from.x, m.to.y - m.from.y) * 0.12
|
|
194
236
|
f.at.y -= min(rise, max(min(m.from.y, m.to.y) - P.top, 0)) * 4 * p * (1 - p)
|
|
237
|
+
// Three hops on the spot.
|
|
238
|
+
case .stomp: f.at.y -= min(22, max(m.from.y - P.top, 0)) * abs(sin(3 * .pi * p))
|
|
239
|
+
// Faster and faster.
|
|
240
|
+
case .fall: f.at.y = m.from.y + (m.to.y - m.from.y) * p * p
|
|
241
|
+
default: break
|
|
195
242
|
}
|
|
196
243
|
|
|
197
244
|
// The look of this move, reached from the look of the one before
|
|
@@ -203,14 +250,18 @@ struct Play {
|
|
|
203
250
|
f.pose = M.blend(before.pose, now.pose, eyes: into, body: into, marks: into)
|
|
204
251
|
switch m.kind {
|
|
205
252
|
case .walk: f.pose.lift += 3 * into * abs(sin(2 * .pi * t * 2.2))
|
|
206
|
-
case .jump:
|
|
253
|
+
case .jump, .carry:
|
|
207
254
|
let air = sin(.pi * p)
|
|
208
255
|
f.pose.sy *= 1 + 0.12 * air; f.pose.sx *= 1 - 0.08 * air
|
|
209
256
|
case .eat:
|
|
210
257
|
let bite = into * (0.5 + 0.5 * sin(2 * .pi * t / P.perLetter))
|
|
211
258
|
f.pose.sy *= 1 - 0.08 * bite; f.pose.sx *= 1 + 0.05 * bite
|
|
212
259
|
case .wipe: f.pose.tilt += 5 * into * sin(2 * .pi * t * 5)
|
|
213
|
-
|
|
260
|
+
// Leans back, then into the kick.
|
|
261
|
+
case .kick: f.pose.tilt -= 14 * sin(2 * .pi * p)
|
|
262
|
+
case .stomp: f.pose.sy *= 1 + into * (0.1 * abs(sin(3 * .pi * p)) - 0.05)
|
|
263
|
+
case .fall: f.pose.sy *= 1 + 0.12 * p; f.pose.sx *= 1 - 0.08 * p
|
|
264
|
+
case .lift, .drop, .rest: break
|
|
214
265
|
}
|
|
215
266
|
|
|
216
267
|
for done in moves[...i] {
|
|
@@ -230,21 +281,10 @@ struct Play {
|
|
|
230
281
|
default: break
|
|
231
282
|
}
|
|
232
283
|
}
|
|
284
|
+
move(&f)
|
|
233
285
|
return f
|
|
234
286
|
}
|
|
235
287
|
|
|
236
288
|
/// A little more than the letters, so nothing of them shows at the edges.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
/// Which way it faces during a move, and its pose without the motion.
|
|
240
|
-
private func look(_ m: Move) -> (face: Double, pose: M.Pose) {
|
|
241
|
-
let way: Double = m.to.x >= m.from.x ? 1 : -1
|
|
242
|
-
switch m.kind {
|
|
243
|
-
case .jump: return (way, M.Pose(tilt: 6 * way, gy: -2))
|
|
244
|
-
case .walk: return (way, M.Pose(tilt: 7 * way))
|
|
245
|
-
case .eat: return (way, M.Pose(size: 1.12))
|
|
246
|
-
case .wipe: return (way, M.Pose(tilt: 12 * way, gy: 4, lid: 0.3))
|
|
247
|
-
case .rest: return (0, M.Pose())
|
|
248
|
-
}
|
|
249
|
-
}
|
|
289
|
+
func cover(_ r: P.Rect) -> P.Rect { P.Rect(x: r.x - 1, y: r.y - 2, w: r.w + 2, h: r.h + 4) }
|
|
250
290
|
}
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import AppKit
|
|
2
2
|
|
|
3
3
|
/// What play mode (#505) draws over the picture: the page's colour where
|
|
4
|
-
/// words are gone, the cloth, and the character. The context is flipped,
|
|
4
|
+
/// words are gone, the pieces that moved, the cloth, and the character. The context is flipped,
|
|
5
5
|
/// in the picture's points.
|
|
6
6
|
enum PlayDraw {
|
|
7
|
-
static func draw(_ frame: Play.Frame, in ctx: CGContext, stops: [NSColor]) {
|
|
7
|
+
static func draw(_ frame: Play.Frame, in ctx: CGContext, stops: [NSColor], picture: CGImage?, scale: CGFloat) {
|
|
8
8
|
for gone in frame.gone {
|
|
9
9
|
let c = gone.paper
|
|
10
10
|
ctx.setFillColor(CGColor(srgbRed: CGFloat(c >> 16 & 0xFF) / 255, green: CGFloat(c >> 8 & 0xFF) / 255,
|
|
11
11
|
blue: CGFloat(c & 0xFF) / 255, alpha: 1))
|
|
12
12
|
ctx.fill(cg(gone.rect))
|
|
13
13
|
}
|
|
14
|
+
for piece in frame.pieces {
|
|
15
|
+
let from = cg(piece.from)
|
|
16
|
+
guard let part = picture?.cropping(to: from.applying(CGAffineTransform(scaleX: scale, y: scale))) else { continue }
|
|
17
|
+
ctx.saveGState()
|
|
18
|
+
ctx.translateBy(x: piece.at.x + from.width / 2, y: piece.at.y + from.height / 2)
|
|
19
|
+
ctx.rotate(by: piece.turn)
|
|
20
|
+
// The context is flipped; a picture is not.
|
|
21
|
+
ctx.scaleBy(x: 1, y: -1)
|
|
22
|
+
ctx.draw(part, in: CGRect(x: -from.width / 2, y: -from.height / 2, width: from.width, height: from.height))
|
|
23
|
+
ctx.restoreGState()
|
|
24
|
+
}
|
|
14
25
|
if let cloth = frame.cloth {
|
|
15
26
|
let path = CGPath(roundedRect: cg(cloth), cornerWidth: 4, cornerHeight: 4, transform: nil)
|
|
16
27
|
ctx.addPath(path)
|
|
@@ -73,6 +84,7 @@ final class PlayHost {
|
|
|
73
84
|
backdrop.layer?.contents = picture
|
|
74
85
|
let view = PlayView(frame: bounds)
|
|
75
86
|
view.stops = stops
|
|
87
|
+
view.picture = picture
|
|
76
88
|
view.shown = Play.Frame(at: foot, pose: CharacterMath.pose(.working))
|
|
77
89
|
view.onInput = { [weak self] in self?.end("a key or a click") }
|
|
78
90
|
backdrop.addSubview(view)
|
|
@@ -144,6 +156,8 @@ private final class PlayWindow: NSPanel {
|
|
|
144
156
|
private final class PlayView: NSView {
|
|
145
157
|
var shown = Play.Frame()
|
|
146
158
|
var stops = CharacterDraw.stops(tint: Brand.accent, colors: nil)
|
|
159
|
+
/// The picture under it: moved pieces are cut from it.
|
|
160
|
+
var picture: CGImage?
|
|
147
161
|
var onInput: (() -> Void)?
|
|
148
162
|
|
|
149
163
|
override var isFlipped: Bool { true }
|
|
@@ -152,7 +166,8 @@ private final class PlayView: NSView {
|
|
|
152
166
|
|
|
153
167
|
override func draw(_ dirtyRect: NSRect) {
|
|
154
168
|
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
|
|
155
|
-
PlayDraw.draw(shown, in: ctx, stops: stops
|
|
169
|
+
PlayDraw.draw(shown, in: ctx, stops: stops, picture: picture,
|
|
170
|
+
scale: CGFloat(picture?.width ?? 0) / max(bounds.width, 1))
|
|
156
171
|
}
|
|
157
172
|
|
|
158
173
|
override func keyDown(with event: NSEvent) { onInput?() }
|
|
@@ -57,6 +57,8 @@ struct LookSection: View {
|
|
|
57
57
|
@Binding var look: String
|
|
58
58
|
/// Nil hides the control: the daemon is older than the reduced pill.
|
|
59
59
|
var startReduced: Binding<Bool>?
|
|
60
|
+
/// Nil hides the control: the daemon is older than the stroll.
|
|
61
|
+
var stroll: Binding<Bool>?
|
|
60
62
|
|
|
61
63
|
var body: some View {
|
|
62
64
|
Section {
|
|
@@ -68,6 +70,10 @@ struct LookSection: View {
|
|
|
68
70
|
Toggle("Start reduced to the orb", isOn: startReduced)
|
|
69
71
|
.disabled(look == "character")
|
|
70
72
|
}
|
|
73
|
+
if let stroll {
|
|
74
|
+
Toggle("Character strolls when idle", isOn: stroll)
|
|
75
|
+
.disabled(look != "character")
|
|
76
|
+
}
|
|
71
77
|
} header: {
|
|
72
78
|
Text("Assistant")
|
|
73
79
|
} footer: {
|
|
@@ -378,7 +378,10 @@ private struct GeneralTab: View {
|
|
|
378
378
|
set: { model.draft?.general.look = $0 }),
|
|
379
379
|
startReduced: draft.general.startReduced == nil ? nil
|
|
380
380
|
: Binding(get: { model.draft?.general.startReduced ?? false },
|
|
381
|
-
set: { model.draft?.general.startReduced = $0 })
|
|
381
|
+
set: { model.draft?.general.startReduced = $0 }),
|
|
382
|
+
stroll: draft.general.stroll == nil ? nil
|
|
383
|
+
: Binding(get: { model.draft?.general.stroll ?? false },
|
|
384
|
+
set: { model.draft?.general.stroll = $0 }))
|
|
382
385
|
}
|
|
383
386
|
if draft.general.card != nil {
|
|
384
387
|
AnswerCardSection(card: Binding(get: { model.draft?.general.card ?? .standard },
|
|
@@ -32,13 +32,17 @@ struct VoiceSettings: Codable, Equatable {
|
|
|
32
32
|
/// The pill starts reduced to its orb. Nil from a daemon older
|
|
33
33
|
/// than the reduced pill.
|
|
34
34
|
var startReduced: Bool?
|
|
35
|
+
/// The character takes a stroll when it has nothing to do. Nil
|
|
36
|
+
/// from a daemon older than the stroll.
|
|
37
|
+
var stroll: Bool?
|
|
35
38
|
|
|
36
39
|
init(provider: String, fallback: String, stt: String, localStt: String = "mlx-whisper",
|
|
37
40
|
endOfTurn: String = "vad", hotkeys: Hotkeys, card: Card? = nil, look: String? = nil,
|
|
38
|
-
startReduced: Bool? = nil) {
|
|
41
|
+
startReduced: Bool? = nil, stroll: Bool? = nil) {
|
|
39
42
|
self.provider = provider; self.fallback = fallback; self.stt = stt
|
|
40
43
|
self.localStt = localStt; self.endOfTurn = endOfTurn; self.hotkeys = hotkeys
|
|
41
44
|
self.card = card; self.look = look; self.startReduced = startReduced
|
|
45
|
+
self.stroll = stroll
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
/// A daemon from before these fields leaves them out: the defaults.
|
|
@@ -53,6 +57,7 @@ struct VoiceSettings: Codable, Equatable {
|
|
|
53
57
|
card = try c.decodeIfPresent(Card.self, forKey: .card)
|
|
54
58
|
look = try c.decodeIfPresent(String.self, forKey: .look)
|
|
55
59
|
startReduced = try c.decodeIfPresent(Bool.self, forKey: .startReduced)
|
|
60
|
+
stroll = try c.decodeIfPresent(Bool.self, forKey: .stroll)
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
struct Voice: Codable, Equatable {
|
|
@@ -113,6 +118,7 @@ struct VoiceSettings: Codable, Equatable {
|
|
|
113
118
|
if self.general.endOfTurn != old.general.endOfTurn { general["endOfTurn"] = self.general.endOfTurn }
|
|
114
119
|
if let look = self.general.look, look != old.general.look { general["look"] = look }
|
|
115
120
|
if let start = self.general.startReduced, start != old.general.startReduced { general["startReduced"] = start }
|
|
121
|
+
if let stroll = self.general.stroll, stroll != old.general.stroll { general["stroll"] = stroll }
|
|
116
122
|
var keys: [String: Any] = [:]
|
|
117
123
|
if self.general.hotkeys.talk != old.general.hotkeys.talk { keys["talk"] = self.general.hotkeys.talk }
|
|
118
124
|
if self.general.hotkeys.stop != old.general.hotkeys.stop { keys["stop"] = self.general.hotkeys.stop }
|
|
@@ -157,6 +163,17 @@ struct VoiceSettings: Codable, Equatable {
|
|
|
157
163
|
min(30, pow(2, Double(max(1, failures))))
|
|
158
164
|
}
|
|
159
165
|
|
|
166
|
+
/// Seconds between reads once the daemon has answered, so a change
|
|
167
|
+
/// made outside the app shows without a restart.
|
|
168
|
+
static let rereadDelay: Double = 5
|
|
169
|
+
|
|
170
|
+
/// Whether a read from the daemon replaces what the app holds: it
|
|
171
|
+
/// differs, and nothing was saved here while it was on its way. A read
|
|
172
|
+
/// that started before a save in the window carries the old settings.
|
|
173
|
+
static func replaces(_ read: VoiceSettings, held: VoiceSettings?, heldWhenAsked: VoiceSettings?) -> Bool {
|
|
174
|
+
held == heldWhenAsked && read != held
|
|
175
|
+
}
|
|
176
|
+
|
|
160
177
|
/// The fields a preview speaks with: the draft's voice for one agent.
|
|
161
178
|
func previewVoice(for id: String) -> [String: Any] {
|
|
162
179
|
guard let v = agents.first(where: { $0.id == id })?.voice else { return [:] }
|