agentix-cli 0.97.0 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/mac-voice/Sources/AgentXVoice/Character.swift +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/PlayActs.swift +35 -8
- package/apps/mac-voice/Sources/AgentXVoice/PlayMath.swift +17 -9
- package/apps/mac-voice/Sources/AgentXVoice/PlayMotion.swift +110 -0
- package/apps/mac-voice/Sources/AgentXVoice/PlayStage.swift +43 -5
- package/apps/mac-voice/Sources/AgentXVoice/PointerPlay.swift +137 -0
- package/apps/mac-voice/Sources/AgentXVoice/main.swift +7 -0
- package/dist/{App-6R4I6QPE.js → App-XLQDP26J.js} +2 -2
- package/dist/{ChatApp-R3O4BJ24.js → ChatApp-DRDUXZ66.js} +2 -2
- package/dist/{agent-P6GPE7HD.js → agent-QHM3J2XS.js} +2 -2
- package/dist/{board-dashboard-AKQHIY4J.js → board-dashboard-F7ZNW6YA.js} +9 -9
- package/dist/board-dashboard-F7ZNW6YA.js.map +1 -0
- package/dist/{chunk-QPIMPKZ2.js → chunk-4BCAH7XI.js} +2 -2
- package/dist/{chunk-3S4IYAOC.js → chunk-B32A6OOY.js} +2 -2
- package/dist/{chunk-QHAO6U7U.js → chunk-BERHVF3T.js} +2 -2
- package/dist/{chunk-TWC4CYTU.js → chunk-BHKWXFJD.js} +5 -5
- package/dist/{chunk-Q53XKCRL.js → chunk-BTLW2RML.js} +2 -2
- package/dist/{chunk-55HM3SUN.js → chunk-DRUEPGLP.js} +2 -2
- package/dist/{chunk-SZ5TGL25.js → chunk-DXJRMPGH.js} +2 -2
- package/dist/{chunk-ZCQCUA4J.js → chunk-GJ5EJVMZ.js} +2 -2
- package/dist/{chunk-2GFSHO24.js → chunk-IQJ7UZVV.js} +2 -2
- package/dist/{chunk-6W3SCPDK.js → chunk-J4JVKVEK.js} +2 -2
- package/dist/{chunk-MDFUSQ6S.js → chunk-J64VIHL5.js} +2 -2
- package/dist/{chunk-ERPEU2ZR.js → chunk-JREA25GJ.js} +3 -3
- package/dist/chunk-JREA25GJ.js.map +1 -0
- package/dist/{chunk-WLKO6FOL.js → chunk-JZJ7AZJD.js} +2 -2
- package/dist/{chunk-4BG3WJSP.js → chunk-LXJGLNVG.js} +2 -2
- package/dist/{chunk-LXHTVVJV.js → chunk-OFVMUBIP.js} +2 -2
- package/dist/{chunk-TOGRRWBX.js → chunk-QUU5RJ7D.js} +2 -2
- package/dist/{chunk-F3DW5ZJN.js → chunk-S2GPUSBU.js} +2 -2
- package/dist/{chunk-C2POHIWJ.js → chunk-SJMOZY7Y.js} +2 -2
- package/dist/{chunk-3SF7PLHF.js → chunk-WEYMSJGZ.js} +191 -190
- package/dist/chunk-WEYMSJGZ.js.map +1 -0
- package/dist/{chunk-X4S2I233.js → chunk-YYCFI4VP.js} +2 -2
- package/dist/{chunk-DGVTRG5Q.js → chunk-Z4FY64BS.js} +2 -2
- package/dist/cli.js +1 -1
- package/dist/{client-UQKCIEHA.js → client-BLUCLNIS.js} +2 -2
- package/dist/{config-OE23UC5C.js → config-ZUMI4IY4.js} +2 -2
- package/dist/config-mutator-S3CD2IWQ.js +2 -0
- package/dist/{doctor-QQ7YLJK4.js → doctor-XGQW5SMD.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{program-O5RQ6NRG.js → program-3FNVZD26.js} +161 -161
- package/dist/program-3FNVZD26.js.map +1 -0
- package/dist/{promote-AW4TJXXT.js → promote-K5LP5Q6Z.js} +2 -2
- package/dist/{query-24BGIJCZ.js → query-LHXSINM2.js} +2 -2
- package/dist/{registry-GZT2OSBK.js → registry-OQUO6NCW.js} +2 -2
- package/dist/{request-planner-U6HHKWLX.js → request-planner-TW5OCKTF.js} +2 -2
- package/dist/{review-candidates-LJUJCVFA.js → review-candidates-EFELFOSY.js} +2 -2
- package/dist/{routing-WCYPBKFH.js → routing-TSRZLD7B.js} +2 -2
- package/dist/schedule-tool-JUHRVU5D.js +2 -0
- package/dist/scheduler-EHJPTULN.js +2 -0
- package/dist/{seat-5PBTFTGS.js → seat-NIPWHLKC.js} +2 -2
- package/dist/{select-JD7MSY25.js → select-UAKDLN76.js} +2 -2
- package/dist/{set-enabled-MUSO2KJL.js → set-enabled-74DLXMSM.js} +2 -2
- package/dist/{tool-Y7SOGJ2S.js → tool-CEBL57UW.js} +2 -2
- package/dist/{tool-FMKGK474.js → tool-TEGHFORD.js} +2 -2
- package/dist/wiki-WOK4SCB7.js +2 -0
- package/package.json +1 -1
- package/dist/board-dashboard-AKQHIY4J.js.map +0 -1
- package/dist/chunk-3SF7PLHF.js.map +0 -1
- package/dist/chunk-ERPEU2ZR.js.map +0 -1
- package/dist/config-mutator-2UK5GX4B.js +0 -2
- package/dist/program-O5RQ6NRG.js.map +0 -1
- package/dist/schedule-tool-E7ER2XOI.js +0 -2
- package/dist/scheduler-FK3LOWU2.js +0 -2
- package/dist/wiki-SJRTAYMT.js +0 -2
- /package/dist/{App-6R4I6QPE.js.map → App-XLQDP26J.js.map} +0 -0
- /package/dist/{ChatApp-R3O4BJ24.js.map → ChatApp-DRDUXZ66.js.map} +0 -0
- /package/dist/{agent-P6GPE7HD.js.map → agent-QHM3J2XS.js.map} +0 -0
- /package/dist/{chunk-QPIMPKZ2.js.map → chunk-4BCAH7XI.js.map} +0 -0
- /package/dist/{chunk-3S4IYAOC.js.map → chunk-B32A6OOY.js.map} +0 -0
- /package/dist/{chunk-QHAO6U7U.js.map → chunk-BERHVF3T.js.map} +0 -0
- /package/dist/{chunk-TWC4CYTU.js.map → chunk-BHKWXFJD.js.map} +0 -0
- /package/dist/{chunk-Q53XKCRL.js.map → chunk-BTLW2RML.js.map} +0 -0
- /package/dist/{chunk-55HM3SUN.js.map → chunk-DRUEPGLP.js.map} +0 -0
- /package/dist/{chunk-SZ5TGL25.js.map → chunk-DXJRMPGH.js.map} +0 -0
- /package/dist/{chunk-ZCQCUA4J.js.map → chunk-GJ5EJVMZ.js.map} +0 -0
- /package/dist/{chunk-2GFSHO24.js.map → chunk-IQJ7UZVV.js.map} +0 -0
- /package/dist/{chunk-6W3SCPDK.js.map → chunk-J4JVKVEK.js.map} +0 -0
- /package/dist/{chunk-MDFUSQ6S.js.map → chunk-J64VIHL5.js.map} +0 -0
- /package/dist/{chunk-WLKO6FOL.js.map → chunk-JZJ7AZJD.js.map} +0 -0
- /package/dist/{chunk-4BG3WJSP.js.map → chunk-LXJGLNVG.js.map} +0 -0
- /package/dist/{chunk-LXHTVVJV.js.map → chunk-OFVMUBIP.js.map} +0 -0
- /package/dist/{chunk-TOGRRWBX.js.map → chunk-QUU5RJ7D.js.map} +0 -0
- /package/dist/{chunk-F3DW5ZJN.js.map → chunk-S2GPUSBU.js.map} +0 -0
- /package/dist/{chunk-C2POHIWJ.js.map → chunk-SJMOZY7Y.js.map} +0 -0
- /package/dist/{chunk-X4S2I233.js.map → chunk-YYCFI4VP.js.map} +0 -0
- /package/dist/{chunk-DGVTRG5Q.js.map → chunk-Z4FY64BS.js.map} +0 -0
- /package/dist/{client-UQKCIEHA.js.map → client-BLUCLNIS.js.map} +0 -0
- /package/dist/{config-OE23UC5C.js.map → config-ZUMI4IY4.js.map} +0 -0
- /package/dist/{config-mutator-2UK5GX4B.js.map → config-mutator-S3CD2IWQ.js.map} +0 -0
- /package/dist/{doctor-QQ7YLJK4.js.map → doctor-XGQW5SMD.js.map} +0 -0
- /package/dist/{promote-AW4TJXXT.js.map → promote-K5LP5Q6Z.js.map} +0 -0
- /package/dist/{query-24BGIJCZ.js.map → query-LHXSINM2.js.map} +0 -0
- /package/dist/{registry-GZT2OSBK.js.map → registry-OQUO6NCW.js.map} +0 -0
- /package/dist/{request-planner-U6HHKWLX.js.map → request-planner-TW5OCKTF.js.map} +0 -0
- /package/dist/{review-candidates-LJUJCVFA.js.map → review-candidates-EFELFOSY.js.map} +0 -0
- /package/dist/{routing-WCYPBKFH.js.map → routing-TSRZLD7B.js.map} +0 -0
- /package/dist/{schedule-tool-E7ER2XOI.js.map → schedule-tool-JUHRVU5D.js.map} +0 -0
- /package/dist/{scheduler-FK3LOWU2.js.map → scheduler-EHJPTULN.js.map} +0 -0
- /package/dist/{seat-5PBTFTGS.js.map → seat-NIPWHLKC.js.map} +0 -0
- /package/dist/{select-JD7MSY25.js.map → select-UAKDLN76.js.map} +0 -0
- /package/dist/{set-enabled-MUSO2KJL.js.map → set-enabled-74DLXMSM.js.map} +0 -0
- /package/dist/{tool-Y7SOGJ2S.js.map → tool-CEBL57UW.js.map} +0 -0
- /package/dist/{tool-FMKGK474.js.map → tool-TEGHFORD.js.map} +0 -0
- /package/dist/{wiki-SJRTAYMT.js.map → wiki-WOK4SCB7.js.map} +0 -0
|
@@ -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) {
|
|
@@ -5,12 +5,23 @@ import Foundation
|
|
|
5
5
|
/// different play each time. Numbers only, like `PlayMath`.
|
|
6
6
|
extension PlayMath {
|
|
7
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,
|
|
9
|
-
///
|
|
8
|
+
/// part of the picture, where its top-left corner is now, how far it
|
|
9
|
+
/// has turned (radians), and the page's colour around it, which is
|
|
10
|
+
/// left out so only its ink moves.
|
|
10
11
|
struct Piece: Equatable {
|
|
11
12
|
var from: Rect
|
|
12
13
|
var at: Point
|
|
13
14
|
var turn = 0.0
|
|
15
|
+
var paper: UInt32 = 0xFFFFFF
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// How much of a pixel of a moved piece is drawn, 0…1: none of the
|
|
19
|
+
/// page's colour, all of the ink, and the soft edge of a letter in
|
|
20
|
+
/// between. Without it a flying letter carries a box of page with it
|
|
21
|
+
/// and covers the text it passes.
|
|
22
|
+
static func ink(r: UInt8, g: UInt8, b: UInt8, paper: UInt32) -> Double {
|
|
23
|
+
let far = max(abs(Int(r) - Int(paper >> 16 & 0xFF)), abs(Int(g) - Int(paper >> 8 & 0xFF)), abs(Int(b) - Int(paper & 0xFF)))
|
|
24
|
+
return min(max(Double(far - 10) / 40, 0), 1)
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
/// A piece from the moment it leaves its place. Its place is covered
|
|
@@ -36,18 +47,22 @@ extension PlayMath {
|
|
|
36
47
|
static let gravity = 1400.0
|
|
37
48
|
static let lift = 0.35
|
|
38
49
|
|
|
39
|
-
/// Where a thrown piece is `seconds` after it left. It
|
|
40
|
-
/// bottom of the picture and at its sides.
|
|
50
|
+
/// Where a thrown piece is `seconds` after it left. It bounces once
|
|
51
|
+
/// on the bottom of the picture and lies still, and stops at its sides.
|
|
41
52
|
static func thrown(_ source: Rect, v: Point, spin: Double, rise: Double, after seconds: Double,
|
|
42
53
|
width: Double, height: Double) -> Piece {
|
|
43
54
|
let floor = max(height - source.h - rise, source.y)
|
|
44
55
|
let landing = (-v.y + (v.y * v.y + 2 * gravity * (floor - source.y)).squareRoot()) / gravity
|
|
45
56
|
let s = min(max(seconds, 0), landing)
|
|
46
|
-
|
|
57
|
+
// How fast it comes down, and the bounce that makes: a third as
|
|
58
|
+
// fast, sideways too.
|
|
59
|
+
let hit = v.y + gravity * landing
|
|
60
|
+
let more = min(max(seconds - landing, 0), PlayMotion.bounceTime(speed: hit, gravity: gravity))
|
|
61
|
+
let x = min(max(source.x + v.x * (s + more / 3), 0), max(width - source.w, 0))
|
|
47
62
|
// It turns about as fast as `spin`, and lands flat: the right way
|
|
48
63
|
// up or upside down.
|
|
49
64
|
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)
|
|
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)
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
/// What it can do to a line.
|
|
@@ -107,17 +122,29 @@ extension Play {
|
|
|
107
122
|
f.gone.append((cover(cut.source), cut.paper))
|
|
108
123
|
switch cut.path {
|
|
109
124
|
case .thrown(let v, let spin, let rise):
|
|
110
|
-
|
|
125
|
+
var piece = P.thrown(cut.source, v: v, spin: spin, rise: rise, after: f.t - cut.start, width: size.x, height: size.y)
|
|
126
|
+
piece.paper = cut.paper
|
|
127
|
+
f.pieces.append(piece)
|
|
111
128
|
case .held(let until, let to):
|
|
112
129
|
// From its place to above the head, and from there down.
|
|
113
130
|
let head = P.Point(x: f.at.x - cut.source.w / 2, y: f.at.y - P.top - cut.source.h)
|
|
114
131
|
let a = f.t < until ? P.Point(x: cut.source.x, y: cut.source.y) : head, b = f.t < until ? head : to
|
|
115
132
|
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)))
|
|
133
|
+
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), paper: cut.paper))
|
|
117
134
|
}
|
|
118
135
|
}
|
|
119
136
|
}
|
|
120
137
|
|
|
138
|
+
/// How far along a move it is: a jump crouches first and lands at the
|
|
139
|
+
/// end, a walk and a wipe start and stop, the rest keep one pace.
|
|
140
|
+
func body(_ m: Move, at t: Double) -> PlayMotion.Body {
|
|
141
|
+
switch m.kind {
|
|
142
|
+
case .jump, .carry: return PlayMotion.jump(t - m.start, of: m.duration)
|
|
143
|
+
case .walk, .wipe: return PlayMotion.glide(t - m.start, of: m.duration)
|
|
144
|
+
default: return PlayMotion.Body(travel: M.span(t, m.start, m.end))
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
121
148
|
/// Which way it faces during a move, and its pose without the motion.
|
|
122
149
|
func look(_ m: Move) -> (face: Double, pose: M.Pose) {
|
|
123
150
|
let way: Double = m.to.x >= m.from.x ? 1 : -1
|
|
@@ -90,6 +90,7 @@ enum PlayMath {
|
|
|
90
90
|
struct Play {
|
|
91
91
|
typealias P = PlayMath
|
|
92
92
|
typealias M = CharacterMath
|
|
93
|
+
typealias Mo = PlayMotion
|
|
93
94
|
|
|
94
95
|
enum Kind { case jump, walk, eat, wipe, kick, stomp, fall, lift, carry, drop, rest }
|
|
95
96
|
|
|
@@ -131,13 +132,16 @@ struct Play {
|
|
|
131
132
|
self.lines = lines
|
|
132
133
|
self.size = size
|
|
133
134
|
var at = start, now = 0.0
|
|
135
|
+
/// The seconds of a jump over `points`: its crouch, its time in
|
|
136
|
+
/// the air and its landing.
|
|
137
|
+
func leap(_ points: Double) -> Double { Mo.crouch + 0.34 + points / 900 + Mo.landing }
|
|
134
138
|
func add(_ kind: Kind, to: P.Point, _ seconds: Double, line: Int = 0, words: Range<Int> = 0..<0) -> Move {
|
|
135
139
|
Move(kind: kind, from: at, to: to, start: now, duration: seconds, line: line, words: words)
|
|
136
140
|
}
|
|
137
141
|
/// Get to `to` first: a jump, unless it is already there.
|
|
138
142
|
func reach(_ to: P.Point, into list: inout [Move]) {
|
|
139
143
|
guard at != to else { return }
|
|
140
|
-
let m = add(.jump, to: to,
|
|
144
|
+
let m = add(.jump, to: to, leap(hypot(to.x - at.x, to.y - at.y)))
|
|
141
145
|
list.append(m); at = to; now = m.end
|
|
142
146
|
}
|
|
143
147
|
var list: [Move] = [], cuts: [P.Cut] = []
|
|
@@ -183,8 +187,10 @@ struct Play {
|
|
|
183
187
|
let word = lines[i].words[w]
|
|
184
188
|
reach(P.Point(x: word.rect.x - P.mouth - 4, y: lines[i].rect.midY + P.middle), into: &list)
|
|
185
189
|
let m = stay(.kick, 0.6, line: i)
|
|
186
|
-
|
|
187
|
-
|
|
190
|
+
// The letters leave one after the other, the nearest first.
|
|
191
|
+
let letters = P.letters(of: word)
|
|
192
|
+
for (k, letter) in letters.enumerated() {
|
|
193
|
+
throwing(letter, line: i, at: m.start + 0.25 + Mo.stagger(k, of: letters.count), v: P.Point(x: 150 + 40 * Double(k), y: -320 - 30 * Double(k % 3)),
|
|
188
194
|
spin: 4 + Double(k % 3))
|
|
189
195
|
}
|
|
190
196
|
case .stomp(let i):
|
|
@@ -207,7 +213,7 @@ struct Play {
|
|
|
207
213
|
let word = lines[i].words[w].rect, home = lines[j].rect
|
|
208
214
|
reach(P.Point(x: word.x - P.mouth - 4, y: lines[i].rect.midY + P.middle), into: &list)
|
|
209
215
|
let up = stay(.lift, P.lift, line: i)
|
|
210
|
-
let m = add(.carry, to: P.Point(x: home.maxX - 4, y: home.y),
|
|
216
|
+
let m = add(.carry, to: P.Point(x: home.maxX - 4, y: home.y), leap(hypot(home.maxX - 4 - at.x, home.y - at.y)), line: j)
|
|
211
217
|
list.append(m); at = m.to; now = m.end
|
|
212
218
|
let down = stay(.drop, P.lift, line: j)
|
|
213
219
|
// After the line, or on top of its end when the page stops there.
|
|
@@ -228,12 +234,13 @@ struct Play {
|
|
|
228
234
|
let m = moves[i], p = M.span(t, m.start, m.end)
|
|
229
235
|
var f = Frame(t: t, done: t >= duration)
|
|
230
236
|
|
|
231
|
-
|
|
237
|
+
let body = body(m, at: t)
|
|
238
|
+
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)
|
|
232
239
|
switch m.kind {
|
|
233
240
|
case .jump, .carry:
|
|
234
241
|
// An arc over both ends, kept inside the picture.
|
|
235
242
|
let rise = 30 + hypot(m.to.x - m.from.x, m.to.y - m.from.y) * 0.12
|
|
236
|
-
f.at.y -= min(rise, max(min(m.from.y, m.to.y) - P.top, 0)) *
|
|
243
|
+
f.at.y -= min(rise, max(min(m.from.y, m.to.y) - P.top, 0)) * body.air
|
|
237
244
|
// Three hops on the spot.
|
|
238
245
|
case .stomp: f.at.y -= min(22, max(m.from.y - P.top, 0)) * abs(sin(3 * .pi * p))
|
|
239
246
|
// Faster and faster.
|
|
@@ -249,10 +256,11 @@ struct Play {
|
|
|
249
256
|
f.face = before.face + (now.face - before.face) * into
|
|
250
257
|
f.pose = M.blend(before.pose, now.pose, eyes: into, body: into, marks: into)
|
|
251
258
|
switch m.kind {
|
|
252
|
-
|
|
259
|
+
// A step every 50 points, smaller while it starts and stops.
|
|
260
|
+
case .walk: f.pose.lift += 3 * body.speed * abs(sin(.pi * abs(f.at.x - m.from.x) / 50))
|
|
253
261
|
case .jump, .carry:
|
|
254
|
-
|
|
255
|
-
f.pose.
|
|
262
|
+
f.pose.sy *= body.sy; f.pose.sx *= body.sx
|
|
263
|
+
f.pose.lift *= 1 - 0.5 * body.dip
|
|
256
264
|
case .eat:
|
|
257
265
|
let bite = into * (0.5 + 0.5 * sin(2 * .pi * t / P.perLetter))
|
|
258
266
|
f.pose.sy *= 1 - 0.08 * bite; f.pose.sx *= 1 + 0.05 * bite
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// How things move in play mode (#505), after the rules the videos are
|
|
4
|
+
/// made with: nothing starts or stops at full speed, a jump is announced
|
|
5
|
+
/// by a crouch and ends in a soft landing, letters leave one after the
|
|
6
|
+
/// other, and what falls bounces once before it lies still. Numbers only.
|
|
7
|
+
enum PlayMotion {
|
|
8
|
+
typealias M = CharacterMath
|
|
9
|
+
|
|
10
|
+
/// Fast start, slow stop; and slow start, fast end.
|
|
11
|
+
static func out(_ p: Double) -> Double { 1 - into(1 - p) }
|
|
12
|
+
static func into(_ p: Double) -> Double {
|
|
13
|
+
let p = min(max(p, 0), 1)
|
|
14
|
+
return p * p
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// The body at one moment of a move: how far along its way (0…1),
|
|
18
|
+
/// how high in its arc (0…1), its stretch, how deep it sits on its
|
|
19
|
+
/// feet (0…1) and how fast it goes (0…1).
|
|
20
|
+
struct Body: Equatable {
|
|
21
|
+
var travel = 0.0, air = 0.0
|
|
22
|
+
var sx = 1.0, sy = 1.0
|
|
23
|
+
var dip = 0.0
|
|
24
|
+
var speed = 0.0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Seconds: the crouch before a jump, the landing after it, and the
|
|
28
|
+
/// start and the stop of a walk.
|
|
29
|
+
static let crouch = 0.16
|
|
30
|
+
static let landing = 0.26
|
|
31
|
+
static let walkUp = 0.2
|
|
32
|
+
static let walkDown = 0.26
|
|
33
|
+
/// How much taller the body is at full stretch, and how much shorter
|
|
34
|
+
/// at the deepest of a crouch and of a landing.
|
|
35
|
+
static let stretch = 0.14
|
|
36
|
+
static let squat = 0.16
|
|
37
|
+
static let squash = 0.2
|
|
38
|
+
|
|
39
|
+
/// A jump of `duration` seconds, `t` seconds in. It crouches first
|
|
40
|
+
/// and goes nowhere, leaves stretched, is round at the top, comes
|
|
41
|
+
/// down stretched, and lands: squashed, a little too tall once, then
|
|
42
|
+
/// still. It has arrived when the landing begins.
|
|
43
|
+
static func jump(_ t: Double, of duration: Double) -> Body {
|
|
44
|
+
guard duration > 0 else { return Body(travel: 1) }
|
|
45
|
+
let t = min(max(t, 0), duration)
|
|
46
|
+
let before = min(crouch, duration * 0.25), after = min(landing, duration * 0.3)
|
|
47
|
+
// A jump too short for a whole landing changes shape less, not faster.
|
|
48
|
+
let long = stretch * after / landing, flat = squash * after / landing
|
|
49
|
+
if t < before {
|
|
50
|
+
// Down slowly, up fast: the push.
|
|
51
|
+
let q = t / before
|
|
52
|
+
let deep = q < 0.65 ? out(q / 0.65) : 1 - into((q - 0.65) / 0.35)
|
|
53
|
+
return Body(sx: 1 + squat * 0.7 * deep, sy: 1 - squat * deep, dip: deep)
|
|
54
|
+
}
|
|
55
|
+
if t <= duration - after {
|
|
56
|
+
let u = M.span(t, before, duration - after)
|
|
57
|
+
// Stretched while it moves fast up or down, round at the top.
|
|
58
|
+
let more = long * abs(1 - 2 * u) * min(u / 0.12, 1)
|
|
59
|
+
return Body(travel: u, air: 4 * u * (1 - u), sx: 1 - more * 0.6, sy: 1 + more, speed: 1)
|
|
60
|
+
}
|
|
61
|
+
let more = keys([(0, long), (0.4, -flat), (0.72, 0.04), (1, 0)], at: (t - (duration - after)) / after)
|
|
62
|
+
return Body(travel: 1, sx: 1 - more * 0.6, sy: 1 + more, dip: max(-more / flat, 0))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// A walk, or a wipe, of `duration` seconds, `t` seconds in: it gets
|
|
66
|
+
/// up to speed, keeps it, and slows to a stop.
|
|
67
|
+
static func glide(_ t: Double, of duration: Double) -> Body {
|
|
68
|
+
guard duration > 0 else { return Body(travel: 1) }
|
|
69
|
+
let t = min(max(t, 0), duration)
|
|
70
|
+
let up = min(walkUp, duration / 3), down = min(walkDown, duration / 3)
|
|
71
|
+
// Full speed, in ways a second.
|
|
72
|
+
let top = 1 / (duration - up / 2 - down / 2)
|
|
73
|
+
if t < up { return Body(travel: top * t * t / (2 * up), speed: t / up) }
|
|
74
|
+
if t <= duration - down { return Body(travel: top * (t - up / 2), speed: 1) }
|
|
75
|
+
let left = duration - t
|
|
76
|
+
return Body(travel: 1 - top * left * left / (2 * down), speed: left / down)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// When the `k`th of `count` things starts after the first: one after
|
|
80
|
+
/// the other, and all gone within 0.4 seconds, so it reads as one go.
|
|
81
|
+
static func stagger(_ k: Int, of count: Int) -> Double {
|
|
82
|
+
guard count > 1 else { return 0 }
|
|
83
|
+
return Double(k) * min(0.035, 0.4 / Double(count - 1))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// How high something is above where it landed, `seconds` after it
|
|
87
|
+
/// hit at `speed` (points a second) under `gravity`: one bounce, a
|
|
88
|
+
/// third as fast as it came down, then it lies still.
|
|
89
|
+
static func bounce(_ seconds: Double, speed: Double, gravity: Double) -> Double {
|
|
90
|
+
let up = abs(speed) / 3
|
|
91
|
+
guard seconds > 0, gravity > 0 else { return 0 }
|
|
92
|
+
return max(up * seconds - gravity * seconds * seconds / 2, 0)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// The seconds that bounce lasts.
|
|
96
|
+
static func bounceTime(speed: Double, gravity: Double) -> Double {
|
|
97
|
+
gravity > 0 ? 2 * abs(speed) / 3 / gravity : 0
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// The value at `p` between (place, value) keys in order, eased from
|
|
101
|
+
/// each key to the next.
|
|
102
|
+
static func keys(_ keys: [(at: Double, value: Double)], at p: Double) -> Double {
|
|
103
|
+
guard let first = keys.first, let last = keys.last else { return 0 }
|
|
104
|
+
if p <= first.at { return first.value }
|
|
105
|
+
for (a, b) in zip(keys, keys.dropFirst()) where p <= b.at {
|
|
106
|
+
return a.value + (b.value - a.value) * M.ease(M.span(p, a.at, b.at))
|
|
107
|
+
}
|
|
108
|
+
return last.value
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -4,7 +4,7 @@ import AppKit
|
|
|
4
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], cutouts: PlayCutouts?) {
|
|
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,
|
|
@@ -13,7 +13,7 @@ enum PlayDraw {
|
|
|
13
13
|
}
|
|
14
14
|
for piece in frame.pieces {
|
|
15
15
|
let from = cg(piece.from)
|
|
16
|
-
guard let part =
|
|
16
|
+
guard let part = cutouts?.image(from, paper: piece.paper) else { continue }
|
|
17
17
|
ctx.saveGState()
|
|
18
18
|
ctx.translateBy(x: piece.at.x + from.width / 2, y: piece.at.y + from.height / 2)
|
|
19
19
|
ctx.rotate(by: piece.turn)
|
|
@@ -40,6 +40,41 @@ enum PlayDraw {
|
|
|
40
40
|
private static func cg(_ r: PlayMath.Rect) -> CGRect { CGRect(x: r.x, y: r.y, width: r.w, height: r.h) }
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/// The pieces of the picture that move, each cut out once: the ink alone,
|
|
44
|
+
/// the page's colour around it left out (`PlayMath.ink`).
|
|
45
|
+
final class PlayCutouts {
|
|
46
|
+
private let picture: CGImage
|
|
47
|
+
/// Pixels of the picture per point.
|
|
48
|
+
private let scale: CGFloat
|
|
49
|
+
private var made: [[CGFloat]: CGImage] = [:]
|
|
50
|
+
|
|
51
|
+
init(picture: CGImage, scale: CGFloat) {
|
|
52
|
+
self.picture = picture
|
|
53
|
+
self.scale = scale
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// The part of the picture at `from` (points), without its paper.
|
|
57
|
+
func image(_ from: CGRect, paper: UInt32) -> CGImage? {
|
|
58
|
+
let key = [from.minX, from.minY, from.width, from.height]
|
|
59
|
+
if let done = made[key] { return done }
|
|
60
|
+
guard let part = picture.cropping(to: from.applying(CGAffineTransform(scaleX: scale, y: scale))),
|
|
61
|
+
let ctx = CGContext(data: nil, width: part.width, height: part.height, bitsPerComponent: 8, bytesPerRow: part.width * 4,
|
|
62
|
+
space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue),
|
|
63
|
+
let data = ctx.data else { return nil }
|
|
64
|
+
ctx.draw(part, in: CGRect(x: 0, y: 0, width: part.width, height: part.height))
|
|
65
|
+
let px = data.bindMemory(to: UInt8.self, capacity: part.width * part.height * 4)
|
|
66
|
+
for i in stride(from: 0, to: part.width * part.height * 4, by: 4) {
|
|
67
|
+
let a = PlayMath.ink(r: px[i], g: px[i + 1], b: px[i + 2], paper: paper)
|
|
68
|
+
// Premultiplied: the colour fades with it.
|
|
69
|
+
for k in 0..<3 { px[i + k] = UInt8(Double(px[i + k]) * a) }
|
|
70
|
+
px[i + 3] = UInt8(255 * a)
|
|
71
|
+
}
|
|
72
|
+
let image = ctx.makeImage()
|
|
73
|
+
made[key] = image
|
|
74
|
+
return image
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
43
78
|
/// The stage: a frozen picture of the screen laid over everything, with
|
|
44
79
|
/// the character playing on it. The real page is never touched: nothing
|
|
45
80
|
/// is clicked, typed or changed, and what looks eaten or wiped is only
|
|
@@ -157,7 +192,8 @@ private final class PlayView: NSView {
|
|
|
157
192
|
var shown = Play.Frame()
|
|
158
193
|
var stops = CharacterDraw.stops(tint: Brand.accent, colors: nil)
|
|
159
194
|
/// The picture under it: moved pieces are cut from it.
|
|
160
|
-
var picture: CGImage?
|
|
195
|
+
var picture: CGImage? { didSet { cutouts = nil } }
|
|
196
|
+
private var cutouts: PlayCutouts?
|
|
161
197
|
var onInput: (() -> Void)?
|
|
162
198
|
|
|
163
199
|
override var isFlipped: Bool { true }
|
|
@@ -166,8 +202,10 @@ private final class PlayView: NSView {
|
|
|
166
202
|
|
|
167
203
|
override func draw(_ dirtyRect: NSRect) {
|
|
168
204
|
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
|
|
169
|
-
|
|
170
|
-
|
|
205
|
+
if cutouts == nil, let picture {
|
|
206
|
+
cutouts = PlayCutouts(picture: picture, scale: CGFloat(picture.width) / max(bounds.width, 1))
|
|
207
|
+
}
|
|
208
|
+
PlayDraw.draw(shown, in: ctx, stops: stops, cutouts: cutouts)
|
|
171
209
|
}
|
|
172
210
|
|
|
173
211
|
override func keyDown(with event: NSEvent) { onInput?() }
|
|
@@ -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.
|