agentix-cli 0.98.0 → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/mac-helper/build.sh +10 -2
- package/apps/mac-voice/Sources/AgentXVoice/Character.swift +41 -63
- package/apps/mac-voice/Sources/AgentXVoice/CharacterMath.swift +9 -0
- package/apps/mac-voice/Sources/AgentXVoice/CharacterSim.swift +46 -44
- package/apps/mac-voice/Sources/AgentXVoice/CharacterSimFrame.swift +59 -0
- package/apps/mac-voice/Sources/AgentXVoice/CharacterView.swift +57 -0
- package/apps/mac-voice/Sources/AgentXVoice/Config.swift +2 -1
- package/apps/mac-voice/Sources/AgentXVoice/Guide.swift +185 -0
- package/apps/mac-voice/Sources/AgentXVoice/GuideMath.swift +49 -0
- package/apps/mac-voice/Sources/AgentXVoice/Meets.swift +39 -0
- package/apps/mac-voice/Sources/AgentXVoice/Panel.swift +9 -0
- package/apps/mac-voice/Sources/AgentXVoice/PointerPlay.swift +137 -0
- package/apps/mac-voice/Sources/AgentXVoice/main.swift +7 -0
- package/apps/mac-voice/build.sh +14 -4
- package/dist/{board-dashboard-AIT26JTK.js → board-dashboard-R6ZTCTSI.js} +467 -321
- package/dist/board-dashboard-R6ZTCTSI.js.map +1 -0
- package/dist/{chunk-WWCVQHV3.js → chunk-6BZMRVKJ.js} +191 -190
- package/dist/chunk-6BZMRVKJ.js.map +1 -0
- package/dist/{chunk-QUU5RJ7D.js → chunk-VBFXUZTY.js} +15 -15
- package/dist/{chunk-QUU5RJ7D.js.map → chunk-VBFXUZTY.js.map} +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-FK7REU2M.js → program-CH5QSA7K.js} +161 -161
- package/dist/program-CH5QSA7K.js.map +1 -0
- package/package.json +1 -1
- package/dist/board-dashboard-AIT26JTK.js.map +0 -1
- package/dist/chunk-WWCVQHV3.js.map +0 -1
- package/dist/program-FK7REU2M.js.map +0 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
|
|
3
|
+
/// Guiding (#482): the daemon's /voice/guide route, the wait on it, and
|
|
4
|
+
/// the mark the character leaves on what it shows. Where it stands is in
|
|
5
|
+
/// GuideMath.swift; Character.swift moves it there.
|
|
6
|
+
enum GuideClient {
|
|
7
|
+
struct Command: Decodable {
|
|
8
|
+
struct Rect: Decodable { let x, y, width, height: Double }
|
|
9
|
+
let seq: Int
|
|
10
|
+
/// Nil: back to where it rests.
|
|
11
|
+
let rect: Rect?
|
|
12
|
+
let mark: String
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// GET /voice/guide: the command after `seq`, as soon as there is
|
|
16
|
+
/// one; the current one after 25 s; nil when the daemon cannot be
|
|
17
|
+
/// reached or is too old to know the route.
|
|
18
|
+
static func next(after seq: Int) async -> Command? {
|
|
19
|
+
guard let url = URL(string: "\(Config.daemonURL)/voice/guide?after=\(seq)") else { return nil }
|
|
20
|
+
var req = URLRequest(url: url)
|
|
21
|
+
req.timeoutInterval = 35
|
|
22
|
+
guard let (data, response) = try? await URLSession.shared.data(for: req),
|
|
23
|
+
(response as? HTTPURLResponse)?.statusCode == 200 else { return nil }
|
|
24
|
+
return try? JSONDecoder().decode(Command.self, from: data)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Waits on the daemon while the character is on screen. The wait is
|
|
29
|
+
/// also how the daemon knows there is a character to send.
|
|
30
|
+
@MainActor
|
|
31
|
+
final class GuideWatcher {
|
|
32
|
+
private var seq = 0
|
|
33
|
+
private var away = false
|
|
34
|
+
private var task: Task<Void, Never>?
|
|
35
|
+
/// The character is on screen.
|
|
36
|
+
var wanted: (() -> Bool)?
|
|
37
|
+
/// Show this, in AppKit coordinates, or go home.
|
|
38
|
+
var onCommand: ((NSRect?, GuideMath.Mark) -> Void)?
|
|
39
|
+
|
|
40
|
+
/// (Re)start the wait. Called again when the character comes on screen
|
|
41
|
+
/// or leaves it, so a wait already open is ended: cancelling it closes
|
|
42
|
+
/// its socket, and the daemon stops counting a character as there.
|
|
43
|
+
func start() {
|
|
44
|
+
task?.cancel()
|
|
45
|
+
task = Task { [weak self] in
|
|
46
|
+
while !Task.isCancelled {
|
|
47
|
+
guard let self else { return }
|
|
48
|
+
await self.turn()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private func turn() async {
|
|
54
|
+
guard wanted?() == true, let command = await GuideClient.next(after: seq) else {
|
|
55
|
+
// Hidden, or the daemon is gone: nothing is left on screen.
|
|
56
|
+
if away { away = false; onCommand?(nil, .none) }
|
|
57
|
+
try? await Task.sleep(nanoseconds: 2_000_000_000)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
guard command.seq != seq else { return }
|
|
61
|
+
seq = command.seq
|
|
62
|
+
let rect = command.rect.map {
|
|
63
|
+
GuideMath.toAppKit(CGRect(x: $0.x, y: $0.y, width: $0.width, height: $0.height),
|
|
64
|
+
primaryHeight: NSScreen.screens.first?.frame.maxY ?? 0)
|
|
65
|
+
}
|
|
66
|
+
away = rect != nil
|
|
67
|
+
Log.info("guide: \(rect.map { "to \(Int($0.minX)),\(Int($0.minY)) \(Int($0.width))x\(Int($0.height)) \(command.mark)" } ?? "home")")
|
|
68
|
+
onCommand?(rect, GuideMath.Mark(rawValue: command.mark) ?? .box)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// What the character was sent to show (#482): it stands beside it until
|
|
73
|
+
/// it is sent home, taken hold of or hidden; its mark; its glide there and back.
|
|
74
|
+
@MainActor
|
|
75
|
+
final class CharacterGuide {
|
|
76
|
+
private(set) var showing: NSRect?
|
|
77
|
+
private let mark = GuideMark()
|
|
78
|
+
/// On its way up or down, to what it shows or back; and the last frame.
|
|
79
|
+
private var gliding = false
|
|
80
|
+
private var ticked = 0.0
|
|
81
|
+
|
|
82
|
+
func show(_ rect: NSRect, _ kind: GuideMath.Mark, color: NSColor, animated: Bool) {
|
|
83
|
+
showing = rect
|
|
84
|
+
gliding = true
|
|
85
|
+
mark.show(kind, around: rect, color: color, animated: animated)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Back to where it rests, gliding there unless it was taken hold of.
|
|
89
|
+
func end(glide: Bool = true) {
|
|
90
|
+
gliding = glide && showing != nil
|
|
91
|
+
showing = nil
|
|
92
|
+
mark.hide()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Where it stands beside what it shows, on that thing's screen.
|
|
96
|
+
func stand(screens: [NSScreen], body: CGFloat, tall: CGFloat) -> CGPoint? {
|
|
97
|
+
guard let showing, let first = screens.first else { return nil }
|
|
98
|
+
let middle = CGPoint(x: showing.midX, y: showing.midY)
|
|
99
|
+
return GuideMath.stand(beside: showing, in: (screens.first { $0.frame.contains(middle) } ?? first).visibleFrame,
|
|
100
|
+
body: body, tall: tall, inset: PillPlacement.characterInset)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Its height this frame: sent somewhere or back, it glides up or down
|
|
104
|
+
/// to `y`; otherwise it is there at once.
|
|
105
|
+
func height(from: CGFloat, toward y: CGFloat, now: Double, still: Bool) -> CGFloat {
|
|
106
|
+
let h = gliding && !still ? GuideMath.glide(from, toward: y, dt: now - ticked) : y
|
|
107
|
+
gliding = h != y
|
|
108
|
+
ticked = now
|
|
109
|
+
return h
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// The mark on what the character shows: drawn on its own click-through
|
|
114
|
+
/// window over the page, which is never touched.
|
|
115
|
+
@MainActor
|
|
116
|
+
final class GuideMark {
|
|
117
|
+
/// Room around the outline for the stroke.
|
|
118
|
+
private static let margin: CGFloat = 6
|
|
119
|
+
private let window: NSPanel
|
|
120
|
+
private let shape = CAShapeLayer()
|
|
121
|
+
|
|
122
|
+
init() {
|
|
123
|
+
window = NSPanel(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false)
|
|
124
|
+
window.level = .floating
|
|
125
|
+
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
|
|
126
|
+
window.isOpaque = false
|
|
127
|
+
window.backgroundColor = .clear
|
|
128
|
+
window.hasShadow = false
|
|
129
|
+
window.hidesOnDeactivate = false
|
|
130
|
+
window.ignoresMouseEvents = true
|
|
131
|
+
let view = NSView()
|
|
132
|
+
view.wantsLayer = true
|
|
133
|
+
view.layer?.addSublayer(shape)
|
|
134
|
+
window.contentView = view
|
|
135
|
+
shape.lineWidth = 3
|
|
136
|
+
shape.lineCap = .round
|
|
137
|
+
shape.lineJoin = .round
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Draw `mark` around `rect`. Animated, it is drawn on once the
|
|
141
|
+
/// character has had time to get there.
|
|
142
|
+
func show(_ mark: GuideMath.Mark, around rect: NSRect, color: NSColor, animated: Bool) {
|
|
143
|
+
// Kept to the screen it is on, whatever size it was sent.
|
|
144
|
+
let screen = NSScreen.screens.first { $0.frame.intersects(rect) }?.frame ?? .null
|
|
145
|
+
let outline = GuideMath.outline(mark, around: rect).intersection(screen)
|
|
146
|
+
guard !outline.isNull, !outline.isEmpty else { return hide() }
|
|
147
|
+
let frame = outline.insetBy(dx: -Self.margin, dy: -Self.margin)
|
|
148
|
+
window.setFrame(frame, display: false)
|
|
149
|
+
let inner = CGRect(x: Self.margin, y: Self.margin, width: outline.width, height: outline.height)
|
|
150
|
+
let path = CGMutablePath()
|
|
151
|
+
switch mark {
|
|
152
|
+
case .circle: path.addEllipse(in: inner)
|
|
153
|
+
case .underline: path.move(to: inner.origin); path.addLine(to: CGPoint(x: inner.maxX, y: inner.minY))
|
|
154
|
+
default: path.addRoundedRect(in: inner, cornerWidth: min(6, inner.width / 2), cornerHeight: min(6, inner.height / 2))
|
|
155
|
+
}
|
|
156
|
+
shape.frame = CGRect(origin: .zero, size: frame.size)
|
|
157
|
+
shape.path = path
|
|
158
|
+
shape.strokeColor = color.cgColor
|
|
159
|
+
shape.fillColor = mark == .box ? color.withAlphaComponent(0.12).cgColor : nil
|
|
160
|
+
shape.removeAllAnimations()
|
|
161
|
+
if animated {
|
|
162
|
+
let draw = CABasicAnimation(keyPath: "strokeEnd")
|
|
163
|
+
draw.fromValue = 0
|
|
164
|
+
draw.toValue = 1
|
|
165
|
+
draw.beginTime = CACurrentMediaTime() + 0.35
|
|
166
|
+
draw.duration = 0.45
|
|
167
|
+
draw.fillMode = .backwards
|
|
168
|
+
draw.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
169
|
+
shape.add(draw, forKey: "draw")
|
|
170
|
+
let appear = CABasicAnimation(keyPath: "opacity")
|
|
171
|
+
appear.fromValue = 0
|
|
172
|
+
appear.toValue = 1
|
|
173
|
+
appear.beginTime = draw.beginTime
|
|
174
|
+
appear.duration = 0.15
|
|
175
|
+
appear.fillMode = .backwards
|
|
176
|
+
shape.add(appear, forKey: "appear")
|
|
177
|
+
}
|
|
178
|
+
window.orderFrontRegardless()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func hide() {
|
|
182
|
+
shape.removeAllAnimations()
|
|
183
|
+
window.orderOut(nil)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import CoreGraphics
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Guiding (#482): the answering agent sends the character to something
|
|
5
|
+
/// on screen. Where it stands to show it, and the mark it leaves there.
|
|
6
|
+
/// Kept free of AppKit so the tests can check it without a window.
|
|
7
|
+
/// Rectangles are in screen coordinates, origin bottom-left, as AppKit
|
|
8
|
+
/// uses them, once toAppKit has turned them.
|
|
9
|
+
enum GuideMath {
|
|
10
|
+
enum Mark: String {
|
|
11
|
+
case box, circle, underline, none
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Accessibility → AppKit: the daemon's rectangles are measured from
|
|
15
|
+
/// the top of the primary display, whichever screen they are on.
|
|
16
|
+
static func toAppKit(_ r: CGRect, primaryHeight: CGFloat) -> CGRect {
|
|
17
|
+
CGRect(x: r.minX, y: primaryHeight - r.maxY, width: r.width, height: r.height)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// The point under the character while it shows `rect`: beside it,
|
|
21
|
+
/// its body level with the middle. On the left, away from which its
|
|
22
|
+
/// bubble reaches, so the bubble does not cover what it shows; on the
|
|
23
|
+
/// right when the left has no room in `visible`.
|
|
24
|
+
static func stand(beside rect: CGRect, in visible: CGRect, body: CGFloat, tall: CGFloat,
|
|
25
|
+
inset: CGFloat, gap: CGFloat = 14) -> CGPoint {
|
|
26
|
+
let left = rect.minX - gap - body / 2
|
|
27
|
+
let x = left >= visible.minX + inset ? left : rect.maxX + gap + body / 2
|
|
28
|
+
return CGPoint(x: x, y: rect.midY - tall / 2)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// What the mark is drawn along, around `rect`: a box a little wider
|
|
32
|
+
/// than it, an oval that clears its corners, or a line under it.
|
|
33
|
+
static func outline(_ mark: Mark, around rect: CGRect) -> CGRect {
|
|
34
|
+
switch mark {
|
|
35
|
+
case .box: return rect.insetBy(dx: -5, dy: -5)
|
|
36
|
+
// An oval through a rectangle's corners is √2 times its size.
|
|
37
|
+
case .circle: return rect.insetBy(dx: -(rect.width * 0.21 + 6), dy: -(rect.height * 0.21 + 6))
|
|
38
|
+
case .underline: return CGRect(x: rect.minX - 2, y: rect.minY - 5, width: rect.width + 4, height: 0)
|
|
39
|
+
case .none: return .null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// One frame of its way up or down to where it is sent: most of the
|
|
44
|
+
/// way in a third of a second, and there once it is within a point.
|
|
45
|
+
static func glide(_ y: CGFloat, toward goal: CGFloat, dt: Double) -> CGFloat {
|
|
46
|
+
let next = y + (goal - y) * CGFloat(1 - exp(-7 * min(max(dt, 0), 0.1)))
|
|
47
|
+
return abs(goal - next) < 1 ? goal : next
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import CoreGraphics
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// What the character meets on the live screen (#539): the sides of the
|
|
5
|
+
/// windows that stand on its line. Read from the window list, which gives
|
|
6
|
+
/// each window's rectangle and no picture, title or text, so it needs no
|
|
7
|
+
/// permission.
|
|
8
|
+
enum Meets {
|
|
9
|
+
/// A window smaller than this is not something to stop at.
|
|
10
|
+
static let least = CGSize(width: 80, height: 40)
|
|
11
|
+
|
|
12
|
+
/// Where the sides of `windows`, front to back, cross `band`: each
|
|
13
|
+
/// side only where no window in front covers it. One set of
|
|
14
|
+
/// coordinates for both.
|
|
15
|
+
static func edges(of windows: [CGRect], band: ClosedRange<CGFloat>) -> [Double] {
|
|
16
|
+
var found: [Double] = []
|
|
17
|
+
for (i, window) in windows.enumerated() {
|
|
18
|
+
let low = max(window.minY, band.lowerBound), high = min(window.maxY, band.upperBound)
|
|
19
|
+
guard window.width >= least.width, window.height >= least.height, low < high else { continue }
|
|
20
|
+
let y = (low + high) / 2
|
|
21
|
+
for x in [window.minX, window.maxX] {
|
|
22
|
+
let covered = windows[..<i].contains { $0.minX < x && x < $0.maxX && $0.minY <= y && y < $0.maxY }
|
|
23
|
+
if !covered { found.append(Double(x)) }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return found
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// The ordinary windows on screen, front to back, measured from the
|
|
30
|
+
/// top left of the main screen.
|
|
31
|
+
static func windows() -> [CGRect] {
|
|
32
|
+
let list = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
|
|
33
|
+
return list.compactMap { window in
|
|
34
|
+
guard window[kCGWindowLayer as String] as? Int == 0, window[kCGWindowAlpha as String] as? Double != 0,
|
|
35
|
+
let bounds = window[kCGWindowBounds as String] else { return nil }
|
|
36
|
+
return CGRect(dictionaryRepresentation: bounds as! CFDictionary)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -570,6 +570,15 @@ final class Panel: NSPanel {
|
|
|
570
570
|
onRender?(state)
|
|
571
571
|
}
|
|
572
572
|
|
|
573
|
+
/// On screen with something to use: an answer, an error or the call
|
|
574
|
+
/// buttons. The character does not play with the pointer then (#537).
|
|
575
|
+
@MainActor
|
|
576
|
+
var holdsSomething: Bool {
|
|
577
|
+
guard isVisible else { return false }
|
|
578
|
+
if case .error = current { return true }
|
|
579
|
+
return expanded || callBar.mode != .hidden
|
|
580
|
+
}
|
|
581
|
+
|
|
573
582
|
/// The call buttons for this moment of a call; `.hidden` outside one.
|
|
574
583
|
@MainActor
|
|
575
584
|
func showCall(_ mode: CallBar.Mode) {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Play with the pointer (#505): with "Play mode" ticked, an idle
|
|
4
|
+
/// character does more than step aside. When the pointer moves within
|
|
5
|
+
/// its sight it starts a game: it follows it along the edge, crouches
|
|
6
|
+
/// and jumps at it like a cat, or runs away from it. Between games it
|
|
7
|
+
/// rests, and the usual rules hold. A click in its sight makes it jump.
|
|
8
|
+
///
|
|
9
|
+
/// It only decides where the character wants to be and how it crouches
|
|
10
|
+
/// and hops; `CharacterSim` moves it. Foundation only, and the same
|
|
11
|
+
/// games in the same order on every run, so the tests can count on them.
|
|
12
|
+
struct PointerPlay {
|
|
13
|
+
typealias M = CharacterMath
|
|
14
|
+
|
|
15
|
+
enum Game: Equatable { case follow, pounce, flee }
|
|
16
|
+
|
|
17
|
+
struct Out: Equatable {
|
|
18
|
+
/// Where it wants to be along the edge. Nil: no game, the usual rules.
|
|
19
|
+
var target: Double?
|
|
20
|
+
/// 0…1 crouched, and the height of a hop in drawing units.
|
|
21
|
+
var crouch = 0.0
|
|
22
|
+
var hop = 0.0
|
|
23
|
+
/// It came down on the pointer just now.
|
|
24
|
+
var caught = false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// How far along the edge and above it the pointer is seen, in points.
|
|
28
|
+
static let sight = (x: 520.0, y: 360.0)
|
|
29
|
+
/// A pointer slower than this, in points a second, starts no game.
|
|
30
|
+
static let stir = 80.0
|
|
31
|
+
/// Following, it stops this short of the pointer.
|
|
32
|
+
static let gap = 70.0
|
|
33
|
+
/// It jumps at a pointer no further than this, and no higher.
|
|
34
|
+
static let leap = (x: 220.0, y: 150.0)
|
|
35
|
+
/// It has caught a pointer this close to where it comes down.
|
|
36
|
+
static let grip = 45.0
|
|
37
|
+
/// Running away, it puts this much between the pointer and itself.
|
|
38
|
+
static let far = 320.0
|
|
39
|
+
/// Seconds: the crouch before a jump, the jump, running away, and
|
|
40
|
+
/// the jump a click gives it.
|
|
41
|
+
static let wind = 0.5, air = 0.6, flight = 2.5, start = 0.35
|
|
42
|
+
/// Drawing units: how high it jumps at the pointer, and at a click.
|
|
43
|
+
static let high = 70.0, jolt = 34.0
|
|
44
|
+
|
|
45
|
+
private(set) var game: Game?
|
|
46
|
+
private(set) var games = 0
|
|
47
|
+
private var began = 0.0, until = 0.0, next = 0.0
|
|
48
|
+
private var jumpTo: Double?
|
|
49
|
+
private var seen: (x: Double, y: Double)?
|
|
50
|
+
private var down = false
|
|
51
|
+
private var jolted: Double?
|
|
52
|
+
|
|
53
|
+
/// One frame. `x` is where the character is; `pointer` along the edge
|
|
54
|
+
/// and above it, nil on another screen; `down` the mouse button.
|
|
55
|
+
mutating func step(_ now: Double, _ dt: Double, x: Double, pointer: (x: Double, y: Double)?,
|
|
56
|
+
down: Bool, range: ClosedRange<Double>) -> Out {
|
|
57
|
+
let clicked = down && !self.down
|
|
58
|
+
self.down = down
|
|
59
|
+
let before = seen
|
|
60
|
+
seen = pointer
|
|
61
|
+
guard let p = pointer, abs(p.x - x) < Self.sight.x, abs(p.y) < Self.sight.y else {
|
|
62
|
+
if game != nil { end(now) }
|
|
63
|
+
return Out(hop: hop(now))
|
|
64
|
+
}
|
|
65
|
+
func inside(_ v: Double) -> Double { min(max(v, range.lowerBound), range.upperBound) }
|
|
66
|
+
|
|
67
|
+
// A click startles it: a small jump, and the game is over.
|
|
68
|
+
if clicked && jumpTo == nil {
|
|
69
|
+
jolted = now
|
|
70
|
+
if game != nil { end(now) }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if game == nil, now >= next, jolted == nil, let was = before, dt > 0,
|
|
74
|
+
hypot(p.x - was.x, p.y - was.y) / dt > Self.stir {
|
|
75
|
+
games += 1
|
|
76
|
+
let pick = M.rnd(games, 7)
|
|
77
|
+
let reach = abs(p.x - x) < Self.leap.x && abs(p.y) < Self.leap.y
|
|
78
|
+
game = pick < 0.5 ? .follow : pick < 0.8 ? (reach ? .pounce : .follow) : .flee
|
|
79
|
+
began = now
|
|
80
|
+
jumpTo = nil
|
|
81
|
+
until = now + (game == .follow ? 4 + 3 * M.rnd(games, 2) : game == .flee ? Self.flight : Self.wind + Self.air)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var out = Out(hop: hop(now))
|
|
85
|
+
switch game {
|
|
86
|
+
case nil:
|
|
87
|
+
break
|
|
88
|
+
case .follow:
|
|
89
|
+
// On the side it is on, a little short of the pointer.
|
|
90
|
+
out.target = inside(p.x + (x >= p.x ? Self.gap : -Self.gap))
|
|
91
|
+
case .pounce:
|
|
92
|
+
let t = now - began
|
|
93
|
+
if t < Self.wind {
|
|
94
|
+
out.target = x
|
|
95
|
+
out.crouch = M.ease(t / (Self.wind * 0.8))
|
|
96
|
+
} else {
|
|
97
|
+
// It jumps at where the pointer was when it let go.
|
|
98
|
+
if jumpTo == nil { jumpTo = inside(p.x) }
|
|
99
|
+
let a = M.span(t, Self.wind, Self.wind + Self.air)
|
|
100
|
+
out.target = jumpTo
|
|
101
|
+
out.crouch = 1 - M.ease(a / 0.25)
|
|
102
|
+
out.hop = Self.high * sin(.pi * a)
|
|
103
|
+
}
|
|
104
|
+
case .flee:
|
|
105
|
+
out.target = abs(p.x - x) < Self.far ? M.aside(x: x, pointer: p.x, clear: Self.far, range: range) : x
|
|
106
|
+
}
|
|
107
|
+
if game != nil && now >= until {
|
|
108
|
+
out.caught = game == .pounce && abs(p.x - x) < Self.grip && abs(p.y) < Self.leap.y
|
|
109
|
+
end(now)
|
|
110
|
+
}
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// The assistant has work, or play is off: no game, and none at once
|
|
115
|
+
/// after. The button is still followed, so one held through it is no click.
|
|
116
|
+
mutating func stop(_ now: Double, down: Bool) {
|
|
117
|
+
if game != nil { end(now) }
|
|
118
|
+
self.down = down
|
|
119
|
+
jolted = nil
|
|
120
|
+
seen = nil
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// A game is over: a rest of five to thirteen seconds before the next.
|
|
124
|
+
private mutating func end(_ now: Double) {
|
|
125
|
+
game = nil
|
|
126
|
+
jumpTo = nil
|
|
127
|
+
next = now + 5 + 8 * M.rnd(games, 3)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// The jump a click gives it.
|
|
131
|
+
private mutating func hop(_ now: Double) -> Double {
|
|
132
|
+
guard let at = jolted else { return 0 }
|
|
133
|
+
let a = (now - at) / Self.start
|
|
134
|
+
if a >= 1 { jolted = nil; return 0 }
|
|
135
|
+
return Self.jolt * sin(.pi * a)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -85,6 +85,7 @@ final class App: NSObject, NSApplicationDelegate {
|
|
|
85
85
|
|
|
86
86
|
// An agent ringing the owner (#321).
|
|
87
87
|
private let callWatcher = CallWatcher()
|
|
88
|
+
private let guideWatcher = GuideWatcher()
|
|
88
89
|
private let ringer = Ringer()
|
|
89
90
|
/// The call ringing on the pill now.
|
|
90
91
|
private var ringingCall: IncomingCall?
|
|
@@ -392,6 +393,7 @@ final class App: NSObject, NSApplicationDelegate {
|
|
|
392
393
|
panel.attach(head: head, visible: visible)
|
|
393
394
|
return panel.isVisible ? panel.frame : nil
|
|
394
395
|
}
|
|
396
|
+
character.bubbleShows = { [weak self] in self?.panel.holdsSomething ?? false }
|
|
395
397
|
panel.onLook = { [weak self] state, tint, colors in
|
|
396
398
|
self?.character.show(state.activity, tint: tint, colors: colors)
|
|
397
399
|
// The assistant has something to show: play gives way.
|
|
@@ -507,6 +509,11 @@ final class App: NSObject, NSApplicationDelegate {
|
|
|
507
509
|
return CallModel.pollQuery(ringing: self.ringingCall?.id ?? self.answeringCall, canRing: self.canRing)
|
|
508
510
|
}
|
|
509
511
|
callWatcher.start()
|
|
512
|
+
// The answering agent sends the character to something (#482).
|
|
513
|
+
guideWatcher.wanted = { [weak self] in self?.character.onScreen == true }
|
|
514
|
+
guideWatcher.onCommand = { [weak self] rect, mark in self?.character.guide(to: rect, mark: mark) }
|
|
515
|
+
guideWatcher.start()
|
|
516
|
+
character.onScreenChanged = { [weak self] in self?.guideWatcher.start() }
|
|
510
517
|
|
|
511
518
|
// The target does not wait for the microphone: the menu shows it
|
|
512
519
|
// either way.
|
package/apps/mac-voice/build.sh
CHANGED
|
@@ -32,10 +32,20 @@ swiftc -O \
|
|
|
32
32
|
|
|
33
33
|
cp Resources/Info.plist "$APP/Contents/Info.plist"
|
|
34
34
|
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
# Signing. Unsigned bundles get a fresh TCC identity on every launch. An
|
|
36
|
+
# ad-hoc signature (the default) holds only for this one build: macOS keys
|
|
37
|
+
# a permission to the build's hash, so every new build is asked again for
|
|
38
|
+
# the microphone and Screen Recording. AGENTX_SIGN_IDENTITY names a
|
|
39
|
+
# code-signing certificate in the keychain (a self-signed one is enough)
|
|
40
|
+
# whose permissions carry over to the next build.
|
|
41
|
+
IDENTITY="${AGENTX_SIGN_IDENTITY:--}"
|
|
42
|
+
if [ "$IDENTITY" = "-" ]; then
|
|
43
|
+
codesign --force --sign - --identifier tn.acme.agentx.voice "$APP" 2>/dev/null \
|
|
44
|
+
|| echo "warning: codesign failed; expect repeated microphone prompts"
|
|
45
|
+
echo "note: ad-hoc signature; macOS asks again for the microphone and Screen Recording after this install (set AGENTX_SIGN_IDENTITY to keep them)"
|
|
46
|
+
else
|
|
47
|
+
codesign --force --sign "$IDENTITY" --identifier tn.acme.agentx.voice "$APP"
|
|
48
|
+
fi
|
|
39
49
|
|
|
40
50
|
echo "built: $APP"
|
|
41
51
|
echo "run: open \"$APP\" (or ./build/AgentX\\ Voice.app/Contents/MacOS/AgentXVoice for logs)"
|