agentix-cli 0.96.1 → 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.
Files changed (107) hide show
  1. package/apps/mac-voice/Sources/AgentXVoice/Character.swift +4 -1
  2. package/apps/mac-voice/Sources/AgentXVoice/CharacterSim.swift +50 -4
  3. package/apps/mac-voice/Sources/AgentXVoice/PlayActs.swift +137 -0
  4. package/apps/mac-voice/Sources/AgentXVoice/PlayMath.swift +80 -40
  5. package/apps/mac-voice/Sources/AgentXVoice/PlayStage.swift +18 -3
  6. package/apps/mac-voice/Sources/AgentXVoice/SettingsLook.swift +6 -0
  7. package/apps/mac-voice/Sources/AgentXVoice/SettingsWindow.swift +4 -1
  8. package/apps/mac-voice/Sources/AgentXVoice/VoiceSettings.swift +18 -1
  9. package/apps/mac-voice/Sources/AgentXVoice/main.swift +24 -10
  10. package/dist/{App-UHSIO3UE.js → App-6R4I6QPE.js} +2 -2
  11. package/dist/{ChatApp-LLTFBLJY.js → ChatApp-R3O4BJ24.js} +2 -2
  12. package/dist/{agent-6RJ633MQ.js → agent-P6GPE7HD.js} +2 -2
  13. package/dist/{board-dashboard-A3ZEI4F6.js → board-dashboard-AKQHIY4J.js} +4 -4
  14. package/dist/{chunk-EBVB727L.js → chunk-2GFSHO24.js} +2 -2
  15. package/dist/{chunk-ALWMYA2B.js → chunk-3S4IYAOC.js} +2 -2
  16. package/dist/{chunk-MNS4SBRR.js → chunk-3SF7PLHF.js} +149 -149
  17. package/dist/chunk-3SF7PLHF.js.map +1 -0
  18. package/dist/{chunk-MKF6HSBA.js → chunk-4BG3WJSP.js} +2 -2
  19. package/dist/{chunk-DU6536CC.js → chunk-55HM3SUN.js} +2 -2
  20. package/dist/{chunk-ZVW645NU.js → chunk-6W3SCPDK.js} +2 -2
  21. package/dist/{chunk-J2MHITWR.js → chunk-C2POHIWJ.js} +2 -2
  22. package/dist/{chunk-6NECC6YS.js → chunk-DGVTRG5Q.js} +2 -2
  23. package/dist/{chunk-XEKZKH3W.js → chunk-ERPEU2ZR.js} +2 -2
  24. package/dist/{chunk-XEKZKH3W.js.map → chunk-ERPEU2ZR.js.map} +1 -1
  25. package/dist/{chunk-ZYJDAT3E.js → chunk-F3DW5ZJN.js} +2 -2
  26. package/dist/{chunk-QZIEYDIY.js → chunk-LXHTVVJV.js} +2 -2
  27. package/dist/{chunk-TE44E733.js → chunk-MDFUSQ6S.js} +2 -2
  28. package/dist/{chunk-4XHKJZLJ.js → chunk-Q53XKCRL.js} +2 -2
  29. package/dist/{chunk-VO2GNAKB.js → chunk-QHAO6U7U.js} +2 -2
  30. package/dist/{chunk-ZZSNM2RI.js → chunk-QPIMPKZ2.js} +2 -2
  31. package/dist/{chunk-KIGFWUKP.js → chunk-SZ5TGL25.js} +2 -2
  32. package/dist/{chunk-GHXSBJZS.js → chunk-TOGRRWBX.js} +2 -2
  33. package/dist/{chunk-6DCY2JCB.js → chunk-TWC4CYTU.js} +5 -5
  34. package/dist/{chunk-KEKO4S45.js → chunk-WLKO6FOL.js} +2 -2
  35. package/dist/{chunk-44UGVUN7.js → chunk-X4S2I233.js} +2 -2
  36. package/dist/{chunk-IPDEWVVT.js → chunk-ZCQCUA4J.js} +2 -2
  37. package/dist/cli.js +1 -1
  38. package/dist/{client-ARS6ZFT2.js → client-UQKCIEHA.js} +2 -2
  39. package/dist/{config-XO5Z3BO5.js → config-OE23UC5C.js} +2 -2
  40. package/dist/config-mutator-2UK5GX4B.js +2 -0
  41. package/dist/{doctor-VUK3JE6Z.js → doctor-QQ7YLJK4.js} +2 -2
  42. package/dist/index.d.ts +7 -1
  43. package/dist/index.js +1 -1
  44. package/dist/{program-HUQB4IIO.js → program-O5RQ6NRG.js} +117 -117
  45. package/dist/program-O5RQ6NRG.js.map +1 -0
  46. package/dist/{promote-UGQXA4KV.js → promote-AW4TJXXT.js} +2 -2
  47. package/dist/{query-DL7RHPMA.js → query-24BGIJCZ.js} +2 -2
  48. package/dist/{registry-YIPKBRBH.js → registry-GZT2OSBK.js} +2 -2
  49. package/dist/{request-planner-L2FAVNKP.js → request-planner-U6HHKWLX.js} +2 -2
  50. package/dist/{review-candidates-RPUP4XBU.js → review-candidates-LJUJCVFA.js} +2 -2
  51. package/dist/{routing-G66SVTRO.js → routing-WCYPBKFH.js} +2 -2
  52. package/dist/schedule-tool-E7ER2XOI.js +2 -0
  53. package/dist/scheduler-FK3LOWU2.js +2 -0
  54. package/dist/{seat-ZPH7DA5L.js → seat-5PBTFTGS.js} +2 -2
  55. package/dist/{select-2WTV2BI2.js → select-JD7MSY25.js} +2 -2
  56. package/dist/{set-enabled-KI2FINQD.js → set-enabled-MUSO2KJL.js} +2 -2
  57. package/dist/{tool-2P4LRWN4.js → tool-FMKGK474.js} +2 -2
  58. package/dist/{tool-ZFQ46KU3.js → tool-Y7SOGJ2S.js} +2 -2
  59. package/dist/wiki-SJRTAYMT.js +2 -0
  60. package/package.json +1 -1
  61. package/dist/chunk-MNS4SBRR.js.map +0 -1
  62. package/dist/config-mutator-MJX4EIYV.js +0 -2
  63. package/dist/program-HUQB4IIO.js.map +0 -1
  64. package/dist/schedule-tool-FOWPIWKP.js +0 -2
  65. package/dist/scheduler-GFVUPAD6.js +0 -2
  66. package/dist/wiki-XATGT3DI.js +0 -2
  67. /package/dist/{App-UHSIO3UE.js.map → App-6R4I6QPE.js.map} +0 -0
  68. /package/dist/{ChatApp-LLTFBLJY.js.map → ChatApp-R3O4BJ24.js.map} +0 -0
  69. /package/dist/{agent-6RJ633MQ.js.map → agent-P6GPE7HD.js.map} +0 -0
  70. /package/dist/{board-dashboard-A3ZEI4F6.js.map → board-dashboard-AKQHIY4J.js.map} +0 -0
  71. /package/dist/{chunk-EBVB727L.js.map → chunk-2GFSHO24.js.map} +0 -0
  72. /package/dist/{chunk-ALWMYA2B.js.map → chunk-3S4IYAOC.js.map} +0 -0
  73. /package/dist/{chunk-MKF6HSBA.js.map → chunk-4BG3WJSP.js.map} +0 -0
  74. /package/dist/{chunk-DU6536CC.js.map → chunk-55HM3SUN.js.map} +0 -0
  75. /package/dist/{chunk-ZVW645NU.js.map → chunk-6W3SCPDK.js.map} +0 -0
  76. /package/dist/{chunk-J2MHITWR.js.map → chunk-C2POHIWJ.js.map} +0 -0
  77. /package/dist/{chunk-6NECC6YS.js.map → chunk-DGVTRG5Q.js.map} +0 -0
  78. /package/dist/{chunk-ZYJDAT3E.js.map → chunk-F3DW5ZJN.js.map} +0 -0
  79. /package/dist/{chunk-QZIEYDIY.js.map → chunk-LXHTVVJV.js.map} +0 -0
  80. /package/dist/{chunk-TE44E733.js.map → chunk-MDFUSQ6S.js.map} +0 -0
  81. /package/dist/{chunk-4XHKJZLJ.js.map → chunk-Q53XKCRL.js.map} +0 -0
  82. /package/dist/{chunk-VO2GNAKB.js.map → chunk-QHAO6U7U.js.map} +0 -0
  83. /package/dist/{chunk-ZZSNM2RI.js.map → chunk-QPIMPKZ2.js.map} +0 -0
  84. /package/dist/{chunk-KIGFWUKP.js.map → chunk-SZ5TGL25.js.map} +0 -0
  85. /package/dist/{chunk-GHXSBJZS.js.map → chunk-TOGRRWBX.js.map} +0 -0
  86. /package/dist/{chunk-6DCY2JCB.js.map → chunk-TWC4CYTU.js.map} +0 -0
  87. /package/dist/{chunk-KEKO4S45.js.map → chunk-WLKO6FOL.js.map} +0 -0
  88. /package/dist/{chunk-44UGVUN7.js.map → chunk-X4S2I233.js.map} +0 -0
  89. /package/dist/{chunk-IPDEWVVT.js.map → chunk-ZCQCUA4J.js.map} +0 -0
  90. /package/dist/{client-ARS6ZFT2.js.map → client-UQKCIEHA.js.map} +0 -0
  91. /package/dist/{config-XO5Z3BO5.js.map → config-OE23UC5C.js.map} +0 -0
  92. /package/dist/{config-mutator-MJX4EIYV.js.map → config-mutator-2UK5GX4B.js.map} +0 -0
  93. /package/dist/{doctor-VUK3JE6Z.js.map → doctor-QQ7YLJK4.js.map} +0 -0
  94. /package/dist/{promote-UGQXA4KV.js.map → promote-AW4TJXXT.js.map} +0 -0
  95. /package/dist/{query-DL7RHPMA.js.map → query-24BGIJCZ.js.map} +0 -0
  96. /package/dist/{registry-YIPKBRBH.js.map → registry-GZT2OSBK.js.map} +0 -0
  97. /package/dist/{request-planner-L2FAVNKP.js.map → request-planner-U6HHKWLX.js.map} +0 -0
  98. /package/dist/{review-candidates-RPUP4XBU.js.map → review-candidates-LJUJCVFA.js.map} +0 -0
  99. /package/dist/{routing-G66SVTRO.js.map → routing-WCYPBKFH.js.map} +0 -0
  100. /package/dist/{schedule-tool-FOWPIWKP.js.map → schedule-tool-E7ER2XOI.js.map} +0 -0
  101. /package/dist/{scheduler-GFVUPAD6.js.map → scheduler-FK3LOWU2.js.map} +0 -0
  102. /package/dist/{seat-ZPH7DA5L.js.map → seat-5PBTFTGS.js.map} +0 -0
  103. /package/dist/{select-2WTV2BI2.js.map → select-JD7MSY25.js.map} +0 -0
  104. /package/dist/{set-enabled-KI2FINQD.js.map → set-enabled-MUSO2KJL.js.map} +0 -0
  105. /package/dist/{tool-2P4LRWN4.js.map → tool-FMKGK474.js.map} +0 -0
  106. /package/dist/{tool-ZFQ46KU3.js.map → tool-Y7SOGJ2S.js.map} +0 -0
  107. /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
- target = min(max(input.home, input.range.lowerBound), input.range.upperBound)
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
- /// Carried by the pointer (#502): it is where it is put, at once.
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
- Frame(pose: M.pose(M.mood(for: activity)), x: home, level: 1, voice: 1)
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 and wipes a line. These are
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
- if m.kind == .jump {
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
- case .rest: break
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
- private 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) }
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 [:] }