agentix-cli 0.102.2 → 0.103.1

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 (114) hide show
  1. package/apps/mac-voice/Sources/AgentXVoice/Character.swift +33 -8
  2. package/apps/mac-voice/Sources/AgentXVoice/CharacterMath.swift +13 -0
  3. package/apps/mac-voice/Sources/AgentXVoice/CharacterSim.swift +14 -16
  4. package/apps/mac-voice/Sources/AgentXVoice/CharacterSimFrame.swift +15 -0
  5. package/apps/mac-voice/Sources/AgentXVoice/Guide.swift +14 -6
  6. package/apps/mac-voice/Sources/AgentXVoice/GuideMath.swift +14 -2
  7. package/apps/mac-voice/Sources/AgentXVoice/IdlePlay.swift +113 -0
  8. package/apps/mac-voice/Sources/AgentXVoice/PlayActs.swift +67 -14
  9. package/apps/mac-voice/Sources/AgentXVoice/PlayMath.swift +13 -17
  10. package/apps/mac-voice/Sources/AgentXVoice/PlayRead.swift +10 -3
  11. package/apps/mac-voice/Sources/AgentXVoice/PlayStage.swift +6 -3
  12. package/apps/mac-voice/Sources/AgentXVoice/SettingsLook.swift +11 -0
  13. package/apps/mac-voice/Sources/AgentXVoice/SettingsWindow.swift +4 -1
  14. package/apps/mac-voice/Sources/AgentXVoice/VoiceSettings.swift +8 -2
  15. package/apps/mac-voice/Sources/AgentXVoice/main.swift +23 -11
  16. package/dist/{App-EYUDDF6K.js → App-A7WZMOGA.js} +2 -2
  17. package/dist/{ChatApp-F5LZLBI3.js → ChatApp-EWNT36ZE.js} +2 -2
  18. package/dist/{agent-XF3BWMOB.js → agent-MUGC4RZP.js} +2 -2
  19. package/dist/{board-dashboard-TVSOWW32.js → board-dashboard-IRMCGCU5.js} +4 -4
  20. package/dist/{chunk-ZJXDBVBT.js → chunk-25RQQ55U.js} +2 -2
  21. package/dist/{chunk-YOCQ2MWL.js → chunk-32BPHJ4I.js} +2 -2
  22. package/dist/{chunk-AYNZAOJN.js → chunk-76KJXOEV.js} +2 -2
  23. package/dist/{chunk-V6W3FFW7.js → chunk-BEFKOHRC.js} +2 -2
  24. package/dist/{chunk-QRD3ZG27.js → chunk-BUAVQUHR.js} +2 -2
  25. package/dist/{chunk-PYCTWI2I.js → chunk-CUS7WGZT.js} +2 -2
  26. package/dist/{chunk-CDM4G3CW.js → chunk-CW667M47.js} +185 -185
  27. package/dist/{chunk-CDM4G3CW.js.map → chunk-CW667M47.js.map} +1 -1
  28. package/dist/{chunk-EXPIKLUN.js → chunk-FZ7VHFL5.js} +2 -2
  29. package/dist/{chunk-RWSCQMLD.js → chunk-HE4PDESB.js} +2 -2
  30. package/dist/{chunk-HMDM5MCR.js → chunk-HVPRCHRG.js} +2 -2
  31. package/dist/{chunk-ECM2QEXF.js → chunk-IA3BC7M3.js} +10 -10
  32. package/dist/chunk-IA3BC7M3.js.map +1 -0
  33. package/dist/{chunk-S337S2M7.js → chunk-IOPAWHKW.js} +2 -2
  34. package/dist/{chunk-Y6POV2X5.js → chunk-MVLNVMWI.js} +2 -2
  35. package/dist/{chunk-HQJW3BC5.js → chunk-N4IWNCH3.js} +2 -2
  36. package/dist/{chunk-MWLX3QCD.js → chunk-NWNVHL57.js} +2 -2
  37. package/dist/{chunk-5UO6BBDG.js → chunk-OLUAEBHJ.js} +2 -2
  38. package/dist/{chunk-XO55WKCX.js → chunk-Q57GZPDA.js} +2 -2
  39. package/dist/{chunk-TXFIMVVF.js → chunk-UXTAK5WD.js} +5 -5
  40. package/dist/{chunk-GSFB46YC.js → chunk-WTFNEPJ7.js} +2 -2
  41. package/dist/{chunk-EEZI2K7S.js → chunk-XHUOWYNY.js} +2 -2
  42. package/dist/{chunk-N57ES35J.js → chunk-ZBKKOUNJ.js} +2 -2
  43. package/dist/cli.js +1 -1
  44. package/dist/{client-526OCGWD.js → client-ZX7AY7H3.js} +2 -2
  45. package/dist/config-BXOS2PLC.js +2 -0
  46. package/dist/config-mutator-KRAUD3CA.js +2 -0
  47. package/dist/{doctor-AC5ONEO2.js → doctor-P3QQP33C.js} +2 -2
  48. package/dist/index.d.ts +11 -0
  49. package/dist/index.js +1 -1
  50. package/dist/{program-M5LDV6UQ.js → program-DELG6KQQ.js} +169 -169
  51. package/dist/program-DELG6KQQ.js.map +1 -0
  52. package/dist/{promote-CNKIINYK.js → promote-FHX4Q62Z.js} +2 -2
  53. package/dist/{query-A2N2LIR7.js → query-WBK3YEPV.js} +2 -2
  54. package/dist/{registry-REKWHOHS.js → registry-NQHC7ZAV.js} +2 -2
  55. package/dist/{request-planner-MS5BS6HY.js → request-planner-3LA5RAKG.js} +2 -2
  56. package/dist/{review-candidates-2F4QK6QQ.js → review-candidates-QV66TOCL.js} +2 -2
  57. package/dist/{routing-SU5Q4IPM.js → routing-HF4XVD4Y.js} +2 -2
  58. package/dist/schedule-tool-V66FX5XL.js +2 -0
  59. package/dist/scheduler-XVMWASQR.js +2 -0
  60. package/dist/{seat-PZBFMYQL.js → seat-NQTZUIKH.js} +2 -2
  61. package/dist/{select-D3XOFXA5.js → select-3GVC5RDA.js} +2 -2
  62. package/dist/{set-enabled-DNDRQ3CZ.js → set-enabled-SXCE2MZX.js} +2 -2
  63. package/dist/{tool-FB6SPVIS.js → tool-23GOQNUW.js} +2 -2
  64. package/dist/{tool-RRGOHJW6.js → tool-E6JBENAN.js} +2 -2
  65. package/dist/wiki-VM2RTHVF.js +2 -0
  66. package/package.json +1 -1
  67. package/dist/chunk-ECM2QEXF.js.map +0 -1
  68. package/dist/config-HCJ5NZW5.js +0 -2
  69. package/dist/config-mutator-PGR2OGWC.js +0 -2
  70. package/dist/program-M5LDV6UQ.js.map +0 -1
  71. package/dist/schedule-tool-24KHZY2Z.js +0 -2
  72. package/dist/scheduler-ALDZOUKJ.js +0 -2
  73. package/dist/wiki-3DH2QYTX.js +0 -2
  74. /package/dist/{App-EYUDDF6K.js.map → App-A7WZMOGA.js.map} +0 -0
  75. /package/dist/{ChatApp-F5LZLBI3.js.map → ChatApp-EWNT36ZE.js.map} +0 -0
  76. /package/dist/{agent-XF3BWMOB.js.map → agent-MUGC4RZP.js.map} +0 -0
  77. /package/dist/{board-dashboard-TVSOWW32.js.map → board-dashboard-IRMCGCU5.js.map} +0 -0
  78. /package/dist/{chunk-ZJXDBVBT.js.map → chunk-25RQQ55U.js.map} +0 -0
  79. /package/dist/{chunk-YOCQ2MWL.js.map → chunk-32BPHJ4I.js.map} +0 -0
  80. /package/dist/{chunk-AYNZAOJN.js.map → chunk-76KJXOEV.js.map} +0 -0
  81. /package/dist/{chunk-V6W3FFW7.js.map → chunk-BEFKOHRC.js.map} +0 -0
  82. /package/dist/{chunk-QRD3ZG27.js.map → chunk-BUAVQUHR.js.map} +0 -0
  83. /package/dist/{chunk-PYCTWI2I.js.map → chunk-CUS7WGZT.js.map} +0 -0
  84. /package/dist/{chunk-EXPIKLUN.js.map → chunk-FZ7VHFL5.js.map} +0 -0
  85. /package/dist/{chunk-RWSCQMLD.js.map → chunk-HE4PDESB.js.map} +0 -0
  86. /package/dist/{chunk-HMDM5MCR.js.map → chunk-HVPRCHRG.js.map} +0 -0
  87. /package/dist/{chunk-S337S2M7.js.map → chunk-IOPAWHKW.js.map} +0 -0
  88. /package/dist/{chunk-Y6POV2X5.js.map → chunk-MVLNVMWI.js.map} +0 -0
  89. /package/dist/{chunk-HQJW3BC5.js.map → chunk-N4IWNCH3.js.map} +0 -0
  90. /package/dist/{chunk-MWLX3QCD.js.map → chunk-NWNVHL57.js.map} +0 -0
  91. /package/dist/{chunk-5UO6BBDG.js.map → chunk-OLUAEBHJ.js.map} +0 -0
  92. /package/dist/{chunk-XO55WKCX.js.map → chunk-Q57GZPDA.js.map} +0 -0
  93. /package/dist/{chunk-TXFIMVVF.js.map → chunk-UXTAK5WD.js.map} +0 -0
  94. /package/dist/{chunk-GSFB46YC.js.map → chunk-WTFNEPJ7.js.map} +0 -0
  95. /package/dist/{chunk-EEZI2K7S.js.map → chunk-XHUOWYNY.js.map} +0 -0
  96. /package/dist/{chunk-N57ES35J.js.map → chunk-ZBKKOUNJ.js.map} +0 -0
  97. /package/dist/{client-526OCGWD.js.map → client-ZX7AY7H3.js.map} +0 -0
  98. /package/dist/{config-HCJ5NZW5.js.map → config-BXOS2PLC.js.map} +0 -0
  99. /package/dist/{config-mutator-PGR2OGWC.js.map → config-mutator-KRAUD3CA.js.map} +0 -0
  100. /package/dist/{doctor-AC5ONEO2.js.map → doctor-P3QQP33C.js.map} +0 -0
  101. /package/dist/{promote-CNKIINYK.js.map → promote-FHX4Q62Z.js.map} +0 -0
  102. /package/dist/{query-A2N2LIR7.js.map → query-WBK3YEPV.js.map} +0 -0
  103. /package/dist/{registry-REKWHOHS.js.map → registry-NQHC7ZAV.js.map} +0 -0
  104. /package/dist/{request-planner-MS5BS6HY.js.map → request-planner-3LA5RAKG.js.map} +0 -0
  105. /package/dist/{review-candidates-2F4QK6QQ.js.map → review-candidates-QV66TOCL.js.map} +0 -0
  106. /package/dist/{routing-SU5Q4IPM.js.map → routing-HF4XVD4Y.js.map} +0 -0
  107. /package/dist/{schedule-tool-24KHZY2Z.js.map → schedule-tool-V66FX5XL.js.map} +0 -0
  108. /package/dist/{scheduler-ALDZOUKJ.js.map → scheduler-XVMWASQR.js.map} +0 -0
  109. /package/dist/{seat-PZBFMYQL.js.map → seat-NQTZUIKH.js.map} +0 -0
  110. /package/dist/{select-D3XOFXA5.js.map → select-3GVC5RDA.js.map} +0 -0
  111. /package/dist/{set-enabled-DNDRQ3CZ.js.map → set-enabled-SXCE2MZX.js.map} +0 -0
  112. /package/dist/{tool-FB6SPVIS.js.map → tool-23GOQNUW.js.map} +0 -0
  113. /package/dist/{tool-RRGOHJW6.js.map → tool-E6JBENAN.js.map} +0 -0
  114. /package/dist/{wiki-3DH2QYTX.js.map → wiki-VM2RTHVF.js.map} +0 -0
@@ -32,6 +32,8 @@ final class CharacterHost {
32
32
  private var shown = false
33
33
  /// Hidden with its bubble, until the talk key or the menu (#502).
34
34
  private var hidden = false
35
+ /// On the page, playing (#580): its window is out until it is back.
36
+ private var away = false
35
37
  private var animated = true
36
38
  private var reduceMotion = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
37
39
  private var activity = CharacterMath.Activity.idle
@@ -64,6 +66,9 @@ final class CharacterHost {
64
66
  /// (#505) takes it from there.
65
67
  var stops: [NSColor] { view.stops }
66
68
  var foot: NSPoint { NSPoint(x: CGFloat(sim.x), y: rest.y) }
69
+ /// Its windows, by number: its own and its mark's. The picture of a
70
+ /// play leaves them out (#583).
71
+ var windows: [Int] { [window.windowNumber, guide.window] }
67
72
  /// Carried by the pointer: where the pointer took hold, and where it
68
73
  /// stood then, which is not where it rests once it has stepped aside.
69
74
  private var carried: (from: NSPoint, rest: CGPoint)?
@@ -72,12 +77,14 @@ final class CharacterHost {
72
77
  /// Sent somewhere, and what its bubble says there (#562).
73
78
  var sent: Bool { guide.showing != nil }
74
79
  var caption: String? { guide.caption }
80
+ /// The state it was asked to show by name (#570), there or where it rests.
81
+ private var asked: CharacterMath.Mood?
75
82
  /// Told when it is sent somewhere, to its next stop or back.
76
83
  var onGuide: (() -> Void)?
77
84
  /// A still picture: Reduce Motion, or "Animated orb" off.
78
85
  private var still: Bool { !animated || reduceMotion }
79
86
  /// The look is the character: whether it is on screen. Nil with the orb.
80
- var onScreen: Bool? { shown ? !hidden : nil }
87
+ var onScreen: Bool? { shown ? !hidden && !away : nil }
81
88
  /// Told when it comes on screen or leaves it: shown or hidden, or the look changed.
82
89
  var onScreenChanged: (() -> Void)?
83
90
 
@@ -117,6 +124,14 @@ final class CharacterHost {
117
124
  onScreenChanged?()
118
125
  }
119
126
 
127
+ /// Out for a play on the page, and back as it left, where it left.
128
+ func setAway(_ on: Bool) {
129
+ guard away != on else { return }
130
+ away = on
131
+ run()
132
+ onScreenChanged?()
133
+ }
134
+
120
135
  /// Hide it, as its bubble is hidden: gone until it is asked back.
121
136
  func setHidden(_ on: Bool) {
122
137
  guard hidden != on else { return }
@@ -136,6 +151,10 @@ final class CharacterHost {
136
151
 
137
152
  /// `voice.stroll`: a slow walk now and then while it has nothing to do.
138
153
  var strolls = false
154
+ /// `voice.animations` as the wait between two small animations by
155
+ /// itself (`IdlePlay.gap`), and AgentX's own hold, which stops them.
156
+ var animates = 0.0
157
+ private var quiet = false
139
158
 
140
159
  /// "Animated orb" in the menu.
141
160
  func setAnimated(_ on: Bool) {
@@ -151,9 +170,11 @@ final class CharacterHost {
151
170
  redraw()
152
171
  }
153
172
 
154
- /// Go beside `rect` and mark it, or with nil go back to where it rests.
155
- func guide(to rect: NSRect?, mark kind: GuideMath.Mark = .none, caption: String? = nil) {
156
- if shown, !hidden, carried == nil, let rect {
173
+ /// Go beside `rect` and mark it, or with nil go back to where it
174
+ /// rests; in the state `asked`, or with nil its real one.
175
+ func guide(to rect: NSRect?, mark kind: GuideMath.Mark = .none, caption: String? = nil, asked mood: CharacterMath.Mood? = nil) {
176
+ asked = shown && !hidden && !away ? mood : nil
177
+ if shown, !hidden, !away, carried == nil, let rect {
157
178
  guide.show(rect, kind, caption: caption, color: view.stops[2], animated: !still)
158
179
  } else {
159
180
  guide.end()
@@ -172,7 +193,7 @@ final class CharacterHost {
172
193
  private func run() {
173
194
  timer?.invalidate()
174
195
  timer = nil
175
- guard shown, !hidden else { carried = nil; guide(to: nil); window.orderOut(nil); return }
196
+ guard shown, !hidden, !away else { carried = nil; guide(to: nil); window.orderOut(nil); return }
176
197
  tick()
177
198
  window.orderFrontRegardless()
178
199
  // Still, it only watches for the hand that moves it.
@@ -239,10 +260,13 @@ final class CharacterHost {
239
260
 
240
261
  let frame: CharacterSim.Frame
241
262
  if still {
242
- frame = CharacterSim.still(activity, home: home)
263
+ frame = CharacterSim.still(activity, asked: asked, sent: guide.showing != nil, home: home)
243
264
  } else {
244
265
  if activity == .listening, let read = levelSource {
245
266
  level = OrbMath.smooth(level, toward: OrbMath.level(fromRMS: read()))
267
+ } else if asked == .listening {
268
+ // Asked to listen, there is no microphone to follow: a slow swell.
269
+ level = 0.45 + 0.3 * sin(now * 5)
246
270
  } else {
247
271
  level = 0
248
272
  }
@@ -259,11 +283,12 @@ final class CharacterHost {
259
283
  edgesRead = now
260
284
  // The window list counts down from the top of the main screen.
261
285
  let top = first.frame.maxY - rest.y
286
+ quiet = animates > 0 && Hold.isOn
262
287
  edges = strolls && Config.playMode ? Meets.edges(of: Meets.windows(), band: (top - Self.head)...top) : []
263
288
  }
264
289
  frame = sim.step(to: now,
265
- CharacterSim.Input(activity: activity, level: level, pointer: pointer, held: held, sent: guide.showing != nil, strolls: strolls,
266
- edges: edges, shows: bubbleShows?() ?? false, plays: Config.playMode, down: NSEvent.pressedMouseButtons & 1 != 0,
290
+ CharacterSim.Input(activity: activity, level: level, pointer: pointer, held: held, sent: guide.showing != nil, asked: asked, strolls: strolls,
291
+ edges: edges, shows: bubbleShows?() ?? false, plays: Config.playMode, animates: quiet ? 0 : animates, down: NSEvent.pressedMouseButtons & 1 != 0,
267
292
  home: home, range: Double(spot.ends.lowerBound)...Double(spot.ends.upperBound)))
268
293
  }
269
294
  let origin = NSPoint(x: (CGFloat(frame.x) - Self.size.width / 2).rounded(), y: (rest.y - Self.ground).rounded())
@@ -31,6 +31,19 @@ enum CharacterMath {
31
31
  }
32
32
  }
33
33
 
34
+ /// The state shown when one was asked for by name, or it was sent to
35
+ /// show something (#570). Someone talking, a ringing call and a call
36
+ /// between turns always show: the real turn comes first. Otherwise
37
+ /// the state asked for; and sent somewhere with none asked for, it
38
+ /// does not take the busy look, which would hide the move.
39
+ static func mood(for activity: Activity, asked: Mood?, sent: Bool) -> Mood {
40
+ switch activity {
41
+ case .listening, .ringing, .waiting: return mood(for: activity)
42
+ case .thinking: return asked ?? (sent ? .idle : .working)
43
+ case .idle, .speaking: return asked ?? mood(for: activity)
44
+ }
45
+ }
46
+
34
47
  /// The state it goes through on the way from `from` to `to`, if any:
35
48
  /// it notices you before it listens and when it wakes, and nods once
36
49
  /// it has heard you.
@@ -44,6 +44,8 @@ struct CharacterSim {
44
44
  private var met: Double?
45
45
  /// Its games with the pointer, and the crouch and hop they give it.
46
46
  private(set) var play = PointerPlay()
47
+ /// Its small animations by itself (#571).
48
+ private(set) var idle = IdlePlay()
47
49
  private var crouch = 0.0, hop = 0.0
48
50
  private var face = 0.0, lean = 0.0
49
51
  private var glance = (x: 0.0, y: 0.0)
@@ -117,8 +119,8 @@ struct CharacterSim {
117
119
 
118
120
  // Which state. A change starts from the pose it has now, so a
119
121
  // change in the middle of another is still one smooth move.
120
- if input.activity != .idle { restSince = now }
121
- var want = M.mood(for: input.activity)
122
+ if input.activity != .idle || input.asked != nil { restSince = now }
123
+ var want = M.mood(for: input.activity, asked: input.asked, sent: input.sent)
122
124
  if want == .idle && now - restSince >= Self.dozeAfter { want = .dozing }
123
125
  if want != goal {
124
126
  goal = want
@@ -130,6 +132,10 @@ struct CharacterSim {
130
132
  }
131
133
  if let at = pending, now >= at { pending = nil; go(goal, now) }
132
134
  var pose = change.pose(at: now)
135
+ // Idle, left alone and standing: a small animation now and then.
136
+ let alone = mood == .idle && input.activity == .idle && input.asked == nil && !input.sent && !input.held && !input.shows
137
+ && !near && game == PointerPlay.Out() && play.game == nil && abs(speed) < 40 && strolled == 0 && strollTo == 0
138
+ let own = idle.step(now, dt, free: alone, gap: input.animates, dozeIn: restSince + Self.dozeAfter - now)
133
139
 
134
140
  // Along the edge: a spring a little short of critical, so it
135
141
  // arrives with a small overshoot.
@@ -144,11 +150,11 @@ struct CharacterSim {
144
150
  let stopped = strollTo != 0 && abs(strollTo - strolled) < 0.5 ? met : nil
145
151
  let turned = stopped.map { $0 > x ? 0.7 : -0.7 } ?? 0
146
152
  toward(&face, min(max(speed / 200 + turned, -1), 1), 10)
147
- toward(&lean, min(max(speed / 480, -1), 1) * 13, 8)
153
+ toward(&lean, min(max(speed / 480, -1), 1) * 13 + own.tilt, 8)
148
154
  // A game's crouch and hop are followed, not taken at once, so a
149
155
  // game cut short by work does not snap.
150
- toward(&crouch, game.crouch, 20)
151
- toward(&hop, game.hop, 20)
156
+ toward(&crouch, game.crouch + own.crouch, 20)
157
+ toward(&hop, game.hop + own.hop, 20)
152
158
  pose.sx *= 1 + 0.14 * crouch
153
159
  pose.sy *= 1 - 0.22 * crouch
154
160
  pose.lift += hop - 8 * crouch
@@ -180,8 +186,8 @@ struct CharacterSim {
180
186
  look = (dx / far * pull, dy / far * pull)
181
187
  }
182
188
  if stopped != nil { look = (turned * 5, -2) }
183
- toward(&glance.x, look.x + face * 2, 9)
184
- toward(&glance.y, look.y, 9)
189
+ toward(&glance.x, look.x + face * 2 + own.gx, 9)
190
+ toward(&glance.y, look.y + own.gy, 9)
185
191
 
186
192
  // What keeps it alive: breath, bob, blinks, and the voice.
187
193
  let t = now - (began ?? now)
@@ -201,6 +207,7 @@ struct CharacterSim {
201
207
  nextBlink = now + 2.2 + 4.5 * M.rnd(blinks) // uneven, never the same loop twice
202
208
  }
203
209
  if now >= nextBlink { pose.open *= 1 - sin(.pi * (now - nextBlink) / Self.blink) }
210
+ pose.open *= 1 - own.shut
204
211
 
205
212
  return Frame(pose: pose, x: x, face: face, level: input.level, voice: voice, t: t,
206
213
  dots: dots.compactMap { dot(at: now, $0) }, stars: bursts.flatMap { stars(at: now, $0) })
@@ -248,15 +255,6 @@ struct CharacterSim {
248
255
  strolled = 0; strollTo = 0; strollNext = nil
249
256
  }
250
257
 
251
- /// The state alone, where it rests: for Reduce Motion, which shows a
252
- /// still picture that changes between states and never a loop.
253
- static func still(_ activity: M.Activity, home: Double) -> Frame {
254
- // Its marks at full strength, so each state reads without motion:
255
- // the arcs of its voice only while it speaks.
256
- let pose = M.pose(M.mood(for: activity))
257
- return Frame(pose: pose, x: home, level: 1, voice: pose.speak)
258
- }
259
-
260
258
  private mutating func go(_ to: M.Mood, _ now: Double) {
261
259
  change = M.Transition(from: change.pose(at: now), to: M.pose(to), start: now, duration: M.duration(into: to))
262
260
  mood = to
@@ -15,6 +15,9 @@ extension CharacterSim {
15
15
  /// The answering agent sent it to show something (#482): it goes
16
16
  /// to `home` at once, awake, whatever the pointer did before.
17
17
  var sent = false
18
+ /// The state it was asked to show by name (#570), in place of
19
+ /// what the assistant is doing.
20
+ var asked: M.Mood?
18
21
  /// `voice.stroll`: with nothing to do, it takes a slow stroll
19
22
  /// beside where it rests now and then (#482).
20
23
  var strolls = false
@@ -26,6 +29,9 @@ extension CharacterSim {
26
29
  var shows = false
27
30
  /// "Play mode" is ticked: idle, it plays with the pointer (#505).
28
31
  var plays = false
32
+ /// `voice.animations`: the shortest wait between two small
33
+ /// animations by itself, in seconds (#571). 0: none.
34
+ var animates = 0.0
29
35
  /// The mouse button is down.
30
36
  var down = false
31
37
  /// Where it rests, and how far it may go.
@@ -56,4 +62,13 @@ extension CharacterSim {
56
62
  var dots: [Mark] = []
57
63
  var stars: [Mark] = []
58
64
  }
65
+
66
+ /// The state alone, where it rests: for Reduce Motion, which shows a
67
+ /// still picture that changes between states and never a loop.
68
+ static func still(_ activity: M.Activity, asked: M.Mood? = nil, sent: Bool = false, home: Double) -> Frame {
69
+ // Its marks at full strength, so each state reads without motion:
70
+ // the arcs of its voice only while it speaks.
71
+ let pose = M.pose(M.mood(for: activity, asked: asked, sent: sent))
72
+ return Frame(pose: pose, x: home, level: 1, voice: pose.speak)
73
+ }
59
74
  }
@@ -13,6 +13,9 @@ enum GuideClient {
13
13
  /// What its bubble says at the stop (#562). Nil: no bubble there,
14
14
  /// and a daemon too old to send one.
15
15
  let text: String?
16
+ /// The state it shows meanwhile (#570). Nil: its real state, and
17
+ /// a daemon too old to send one.
18
+ let expression: String?
16
19
  }
17
20
 
18
21
  /// GET /voice/guide: the command after `seq`, as soon as there is
@@ -37,8 +40,9 @@ final class GuideWatcher {
37
40
  private var task: Task<Void, Never>?
38
41
  /// The character is on screen.
39
42
  var wanted: (() -> Bool)?
40
- /// Show this, in AppKit coordinates, with this caption, or go home.
41
- var onCommand: ((NSRect?, GuideMath.Mark, String?) -> Void)?
43
+ /// Show this, in AppKit coordinates, with this caption and in this
44
+ /// state, or go home.
45
+ var onCommand: ((NSRect?, GuideMath.Mark, String?, CharacterMath.Mood?) -> Void)?
42
46
 
43
47
  /// (Re)start the wait. Called again when the character comes on screen
44
48
  /// or leaves it, so a wait already open is ended: cancelling it closes
@@ -56,7 +60,7 @@ final class GuideWatcher {
56
60
  private func turn() async {
57
61
  guard wanted?() == true, let command = await GuideClient.next(after: seq) else {
58
62
  // Hidden, or the daemon is gone: nothing is left on screen.
59
- if away { away = false; onCommand?(nil, .none, nil) }
63
+ if away { away = false; onCommand?(nil, .none, nil, nil) }
60
64
  try? await Task.sleep(nanoseconds: 2_000_000_000)
61
65
  return
62
66
  }
@@ -66,9 +70,10 @@ final class GuideWatcher {
66
70
  GuideMath.toAppKit(CGRect(x: $0.x, y: $0.y, width: $0.width, height: $0.height),
67
71
  primaryHeight: NSScreen.screens.first?.frame.maxY ?? 0)
68
72
  }
69
- away = rect != nil
70
- Log.info("guide: \(rect.map { "to \(Int($0.minX)),\(Int($0.minY)) \(Int($0.width))x\(Int($0.height)) \(command.mark)" } ?? "home")")
71
- onCommand?(rect, GuideMath.Mark(rawValue: command.mark) ?? .box, rect == nil ? nil : command.text)
73
+ let asked = command.expression.flatMap { CharacterMath.Mood(rawValue: $0) }
74
+ away = rect != nil || asked != nil
75
+ Log.info("guide: \(rect.map { "to \(Int($0.minX)),\(Int($0.minY)) \(Int($0.width))x\(Int($0.height)) \(command.mark)" } ?? "home")\(asked.map { ", \($0.rawValue)" } ?? "")")
76
+ onCommand?(rect, GuideMath.Mark(rawValue: command.mark) ?? .box, rect == nil ? nil : command.text, asked)
72
77
  }
73
78
  }
74
79
 
@@ -80,6 +85,8 @@ final class CharacterGuide {
80
85
  /// What its bubble says there (#562).
81
86
  private(set) var caption: String?
82
87
  private let mark = GuideMark()
88
+ /// The number of its mark's window.
89
+ var window: Int { mark.number }
83
90
  /// On its way up or down, to what it shows or back; and the last frame.
84
91
  private var gliding = false
85
92
  private var ticked = 0.0
@@ -125,6 +132,7 @@ final class GuideMark {
125
132
  private static let margin: CGFloat = 6
126
133
  private let window: NSPanel
127
134
  private let shape = CAShapeLayer()
135
+ var number: Int { window.windowNumber }
128
136
 
129
137
  init() {
130
138
  window = NSPanel(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false)
@@ -46,9 +46,21 @@ enum GuideMath {
46
46
  /// At a stop the bubble says the command's caption, and with none
47
47
  /// there is no bubble; the idle hint is for where it rests, not for
48
48
  /// a stop, the way there or a play. `away`: sent somewhere, or playing.
49
- static func idle(caption: String?, away: Bool) -> Idle {
49
+ /// `hint`: the hint may show (hintShows).
50
+ static func idle(caption: String?, away: Bool, hint: Bool = true) -> Idle {
50
51
  if let caption, !caption.isEmpty { return .caption(caption) }
51
- return away ? .nothing : .hint
52
+ return away || !hint ? .nothing : .hint
53
+ }
54
+
55
+ /// Seconds after the app starts that the character's bubble says the hint.
56
+ static let hintSeconds = 6.0
57
+
58
+ /// Whether the idle hint may show (#576). A pill of its own always
59
+ /// says it. The character's bubble says it only for the first seconds
60
+ /// after the app starts, then there is no bubble at idle; "notifications
61
+ /// held" stays, since nothing else on screen says it.
62
+ static func hintShows(character: Bool, sinceStart: Double, held: Bool) -> Bool {
63
+ !character || held || sinceStart < hintSeconds
52
64
  }
53
65
 
54
66
  /// What a label needs beyond the width of its words (#569): a text
@@ -0,0 +1,113 @@
1
+ import Foundation
2
+
3
+ /// Small animations by itself (#571): idle and left alone, the character
4
+ /// now and then looks around, hops or sways, just for the fun of it, and
5
+ /// stretches with a yawn before it dozes. `voice.animations` says how
6
+ /// often, or not at all.
7
+ ///
8
+ /// It only decides what an animation adds to the pose; `CharacterSim`
9
+ /// follows it, so one cut short by work does not snap. Foundation only,
10
+ /// and the same animations in the same order on every run, so the tests
11
+ /// can count on them.
12
+ struct IdlePlay {
13
+ typealias M = CharacterMath
14
+
15
+ /// The animations, by name. The yawn comes before it dozes; the
16
+ /// others are picked now and then.
17
+ enum Act: String, CaseIterable { case look, hop, sway, yawn }
18
+
19
+ /// `voice.animations` as the shortest wait between two, in seconds.
20
+ /// The wait is up to twice that. 0: none.
21
+ static func gap(_ often: String?) -> Double {
22
+ switch often {
23
+ case "rarely": return 60
24
+ case "sometimes": return 25
25
+ case "often": return 10
26
+ default: return 0
27
+ }
28
+ }
29
+
30
+ /// Seconds an animation takes.
31
+ static func length(_ act: Act) -> Double {
32
+ switch act {
33
+ case .look, .yawn: return 2.6
34
+ case .hop: return 0.9
35
+ case .sway: return 1.8
36
+ }
37
+ }
38
+
39
+ /// This close to dozing, in seconds, no animation starts but the yawn.
40
+ static let calm = 6.0
41
+
42
+ /// What an animation adds to the pose: gaze and height in drawing
43
+ /// units, lean in degrees, 0…1 crouched (below 0: stretched tall) and
44
+ /// 0…1 how far the eyes are shut.
45
+ struct Out: Equatable {
46
+ var gx = 0.0, gy = 0.0, tilt = 0.0, hop = 0.0, crouch = 0.0, shut = 0.0
47
+ }
48
+
49
+ private(set) var act: Act?
50
+ private(set) var played = 0
51
+ private var began = 0.0
52
+ private var next: Double?
53
+ private var yawned = false
54
+ private var turn = 0
55
+ private var shut = 0.0
56
+
57
+ /// One frame. `free`: idle, left alone and standing where it is.
58
+ /// `dozeIn`: seconds until it dozes.
59
+ mutating func step(_ now: Double, _ dt: Double, free: Bool, gap: Double, dozeIn: Double) -> Out {
60
+ var out = Out()
61
+ if !free || gap <= 0 {
62
+ act = nil; next = nil
63
+ } else {
64
+ if dozeIn > Self.calm { yawned = false }
65
+ if let on = act, now - began >= Self.length(on) { act = nil; next = nil }
66
+ if act == nil {
67
+ if !yawned && dozeIn <= Self.length(.yawn) {
68
+ yawned = true
69
+ start(.yawn, now)
70
+ } else if next == nil {
71
+ next = now + gap * (1 + M.rnd(played, 5))
72
+ } else if now >= next!, dozeIn > Self.calm {
73
+ // One of the other two, so the same never comes twice.
74
+ turn = (turn + 1 + min(Int(M.rnd(played, 6) * 2), 1)) % 3
75
+ start([Act.look, .hop, .sway][turn], now)
76
+ }
77
+ }
78
+ if let on = act { out = Self.shape(on, (now - began) / Self.length(on)) }
79
+ }
80
+ // The eyes are followed here; the rest by the sim.
81
+ shut += (out.shut - shut) * (1 - exp(-dt * 20))
82
+ out.shut = shut
83
+ return out
84
+ }
85
+
86
+ /// Play `act` now, whatever the wait.
87
+ mutating func start(_ act: Act, _ now: Double) {
88
+ self.act = act
89
+ began = now
90
+ played += 1
91
+ }
92
+
93
+ /// An animation `p` of the way through, 0…1. Each starts and ends at
94
+ /// nothing, so it joins the resting pose at both ends.
95
+ static func shape(_ act: Act, _ p: Double) -> Out {
96
+ switch act {
97
+ case .look:
98
+ // To one side, to the other, and back.
99
+ return Out(gx: 8 * sin(2 * .pi * p), gy: -1.5 * sin(.pi * p))
100
+ case .hop:
101
+ // A short crouch, then up and down again.
102
+ return Out(hop: 26 * sin(.pi * M.span(p, 0.3, 1)),
103
+ crouch: 0.7 * M.ease(p / 0.3) * (1 - M.ease(M.span(p, 0.3, 0.5))))
104
+ case .sway:
105
+ // Two swings from side to side, the widest in the middle.
106
+ return Out(tilt: 10 * sin(4 * .pi * p) * sin(.pi * p))
107
+ case .yawn:
108
+ // It stretches tall, looks up, and shuts its eyes a moment.
109
+ return Out(gy: -2 * sin(.pi * p), crouch: -0.8 * sin(.pi * M.span(p, 0, 0.8)),
110
+ shut: M.ease(M.span(p, 0.15, 0.4)) * (1 - M.ease(M.span(p, 0.75, 1))))
111
+ }
112
+ }
113
+ }
@@ -65,6 +65,22 @@ extension PlayMath {
65
65
  return Piece(from: source, at: Point(x: x, y: source.y + v.y * s + gravity * s * s / 2 - PlayMotion.bounce(more, speed: hit, gravity: gravity)), turn: landing > 0 ? flat * s / landing : 0)
66
66
  }
67
67
 
68
+ /// A word's letters: the word's rectangle cut into equal parts, one a
69
+ /// character. An estimate, close enough to take them one at a time.
70
+ static func letters(of word: Word) -> [Rect] {
71
+ let n = max(word.text.count, 1), w = word.rect.w / Double(n)
72
+ return (0..<n).map { Rect(x: word.rect.x + Double($0) * w, y: word.rect.y, w: w, h: word.rect.h) }
73
+ }
74
+
75
+ /// The colour most of `samples` share: the page around a line. Close
76
+ /// shades count as one, so the soft edge of a letter does not win.
77
+ static func common(_ samples: [UInt32]) -> UInt32? {
78
+ var count: [UInt32: Int] = [:]
79
+ for s in samples { count[s & 0xF8F8F8, default: 0] += 1 }
80
+ guard let best = count.max(by: { ($0.value, $0.key) < ($1.value, $1.key) })?.key else { return nil }
81
+ return samples.first { $0 & 0xF8F8F8 == best }
82
+ }
83
+
68
84
  /// What it can do to a line.
69
85
  enum Act: CaseIterable { case eat, wipe, kick, stomp, carry }
70
86
 
@@ -82,24 +98,31 @@ extension PlayMath {
82
98
  }
83
99
  }
84
100
 
85
- /// A play, different for each seed: it jumps onto one of the lines
86
- /// near the middle of the page and walks it, then does one thing to
87
- /// each of up to four others, never the same thing twice. Lines too
88
- /// close to the top to stand on, too short or of one word are left
89
- /// alone.
101
+ /// A play, different for each page and each seed. It jumps onto the
102
+ /// page's heading, the line clearly taller than the others, or with
103
+ /// no heading onto one of the lines near the middle, and walks it.
104
+ /// Then it does one thing to each of up to four others, never the same
105
+ /// thing twice, and each thing to the line that suits it (`suits`).
106
+ /// Then it goes home. Lines too close to the top to stand on, too
107
+ /// short or of one word are left alone.
90
108
  static func script(_ lines: [Line], width: Double, height: Double, seed: UInt64) -> [Step] {
91
109
  var rng = Seeded(seed)
92
110
  let fit = lines.indices.filter { lines[$0].words.count >= 2 && lines[$0].rect.w >= 120 && lines[$0].rect.y >= top + 40 }
93
111
  func far(_ i: Int) -> Double { hypot(lines[i].rect.midX - width / 2, lines[i].rect.midY - height / 2) }
94
- let near = fit.sorted { far($0) < far($1) }.prefix(8).shuffled(using: &rng)
95
- guard let first = near.first else { return [] }
96
- let acts = Act.allCases.shuffled(using: &rng)
112
+ var near = Array(fit.sorted { far($0) < far($1) }.prefix(8))
113
+ let usual = fit.map { lines[$0].rect.h }.sorted()[safe: fit.count / 2] ?? 0
114
+ let heading = fit.filter { lines[$0].rect.h >= usual * 1.4 }.max { lines[$0].rect.h < lines[$1].rect.h }
115
+ guard let first = heading ?? near.randomElement(using: &rng) else { return [] }
116
+ near.removeAll { $0 == first }
97
117
  var steps: [Step] = [.jump(line: first), .walk(line: first)]
98
118
  // With one line only, it plays on the line it walked.
99
- let stage = near.count > 1 ? Array(near.dropFirst().prefix(4)) : [first]
100
- for (k, i) in stage.enumerated() {
101
- let n = lines[i].words.count, word = Int.random(in: 0..<n, using: &rng)
102
- switch acts[k] {
119
+ if near.isEmpty { near = [first] }
120
+ for act in Act.allCases.shuffled(using: &rng).prefix(4) {
121
+ let luck = near.map { _ in Double.random(in: 0..<1, using: &rng) }
122
+ guard let k = near.indices.max(by: { suits(act, lines[near[$0]], luck[$0]) < suits(act, lines[near[$1]], luck[$1]) }) else { break }
123
+ let i = near.remove(at: k), words = lines[i].words
124
+ let n = words.count, word = Int.random(in: 0..<n, using: &rng)
125
+ switch act {
103
126
  case .eat:
104
127
  let from = Int.random(in: 0..<n - 1, using: &rng)
105
128
  steps.append(.eat(line: i, words: from..<min(from + 3, n)))
@@ -107,14 +130,44 @@ extension PlayMath {
107
130
  case .stomp: steps.append(.stomp(line: i))
108
131
  // The walked line is whole: the word goes to its end.
109
132
  case .carry where i != first: steps.append(.carry(line: i, word: word, to: first))
110
- case .kick, .carry: steps.append(.kick(line: i, word: word))
133
+ // Its longest word: the most letters to fly.
134
+ case .kick, .carry: steps.append(.kick(line: i, word: words.indices.max { words[$0].text.count < words[$1].text.count } ?? word))
111
135
  }
112
136
  }
113
- return steps + [.rest(1.2)]
137
+ return steps + [.rest(0.6), .home, .rest(0.3)]
114
138
  }
139
+
140
+ /// How well a line suits an act, the page deciding what happens
141
+ /// where (#580): the widest line is wiped, the one with the most words
142
+ /// stomped, the one with the longest word kicked, the shortest gives
143
+ /// the word that is carried. Any line can be eaten: `luck` decides.
144
+ static func suits(_ act: Act, _ line: Line, _ luck: Double) -> Double {
145
+ switch act {
146
+ case .wipe: return line.rect.w
147
+ case .stomp: return Double(line.words.count)
148
+ case .kick: return Double(line.words.map(\.text.count).max() ?? 0)
149
+ case .carry: return -line.rect.w
150
+ case .eat: return luck
151
+ }
152
+ }
153
+ }
154
+
155
+ private extension Array {
156
+ subscript(safe i: Int) -> Element? { indices.contains(i) ? self[i] : nil }
115
157
  }
116
158
 
117
159
  extension Play {
160
+ /// Its size at `t`, in play sizes (#580): as big as on its edge when
161
+ /// it leaves it and when it is back, shrinking in the air of its
162
+ /// first jump and growing in the air of its last. It never changes
163
+ /// size at once.
164
+ func scale(at t: Double) -> Double {
165
+ func air(_ m: Move) -> Double { M.ease(M.span(t, m.start + Mo.crouch, m.end - Mo.landing)) }
166
+ if let back, t >= moves[back].start { return 1 + (P.edge - 1) * air(moves[back]) }
167
+ guard let first = moves.first, first.kind == .jump, back != 0 else { return 1 }
168
+ return P.edge - (P.edge - 1) * air(first)
169
+ }
170
+
118
171
  /// The pieces that have left their place by this frame: each place is
119
172
  /// covered, and the piece is drawn where it is now.
120
173
  func move(_ f: inout Frame) {
@@ -52,11 +52,15 @@ enum PlayMath {
52
52
  case carry(line: Int, word: Int, to: Int)
53
53
  /// Stand still, facing you, for so many seconds.
54
54
  case rest(Double)
55
+ /// Jump back to where it left its edge, growing to its size there.
56
+ case home
55
57
  }
56
58
 
57
59
  /// Points per drawing unit on the page: a body 40 points across,
58
60
  /// smaller than on the bottom edge so it fits between lines.
59
61
  static let unit = 0.4
62
+ /// How many times bigger it is on its edge: 56 points across there.
63
+ static let edge = 1.4
60
64
  /// The middle of its body above its feet, and its top, in points.
61
65
  static let middle = (M.Pose().lift + 50) * unit
62
66
  static let top = (M.Pose().lift + 100) * unit + 8
@@ -66,22 +70,6 @@ enum PlayMath {
66
70
  static let perLetter = 0.09
67
71
  /// Its mouth, ahead of its middle.
68
72
  static let mouth = 10.0
69
-
70
- /// A word's letters: the word's rectangle cut into equal parts, one a
71
- /// character. An estimate, close enough to take them one at a time.
72
- static func letters(of word: Word) -> [Rect] {
73
- let n = max(word.text.count, 1), w = word.rect.w / Double(n)
74
- return (0..<n).map { Rect(x: word.rect.x + Double($0) * w, y: word.rect.y, w: w, h: word.rect.h) }
75
- }
76
-
77
- /// The colour most of `samples` share: the page around a line. Close
78
- /// shades count as one, so the soft edge of a letter does not win.
79
- static func common(_ samples: [UInt32]) -> UInt32? {
80
- var count: [UInt32: Int] = [:]
81
- for s in samples { count[s & 0xF8F8F8, default: 0] += 1 }
82
- guard let best = count.max(by: { ($0.value, $0.key) < ($1.value, $1.key) })?.key else { return nil }
83
- return samples.first { $0 & 0xF8F8F8 == best }
84
- }
85
73
  }
86
74
 
87
75
  /// A script laid out in time: every move has its start and its end, so
@@ -110,6 +98,8 @@ struct Play {
110
98
  var face = 0.0
111
99
  var pose = M.Pose()
112
100
  var t = 0.0
101
+ /// Its size, in play sizes: bigger as it leaves its edge and comes back.
102
+ var scale = 1.0
113
103
  /// What is covered with the page's colour, and which colour.
114
104
  var gone: [(rect: P.Rect, paper: UInt32)] = []
115
105
  /// The cloth, while it wipes.
@@ -124,6 +114,8 @@ struct Play {
124
114
  let size: P.Point
125
115
  private(set) var moves: [Move] = []
126
116
  private(set) var cuts: [P.Cut] = []
117
+ /// The jump that takes it back to its place, if the script has one.
118
+ private(set) var back: Int?
127
119
  var duration: Double { moves.last?.end ?? 0 }
128
120
 
129
121
  /// `start` is where its feet are when play begins. Steps that name a
@@ -223,6 +215,10 @@ struct Play {
223
215
  case .rest(let seconds):
224
216
  let m = add(.rest, to: at, max(seconds, 0))
225
217
  list.append(m); now = m.end
218
+ case .home:
219
+ guard at != start else { continue }
220
+ back = list.count
221
+ reach(start, into: &list)
226
222
  }
227
223
  }
228
224
  moves = list
@@ -232,7 +228,7 @@ struct Play {
232
228
  func frame(at t: Double) -> Frame {
233
229
  guard let i = moves.lastIndex(where: { $0.start <= t }) else { return Frame(done: true) }
234
230
  let m = moves[i], p = M.span(t, m.start, m.end)
235
- var f = Frame(t: t, done: t >= duration)
231
+ var f = Frame(t: t, scale: scale(at: t), done: t >= duration)
236
232
 
237
233
  let body = body(m, at: t)
238
234
  f.at = P.Point(x: m.from.x + (m.to.x - m.from.x) * body.travel, y: m.from.y + (m.to.y - m.from.y) * body.travel)