letsgo_shots.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
-- Driver: photograph the LET'S GO capture mode end to end. -- -- One wild encounter under FULL: the battle menu should never be seen -- -- capture mode opens over it with the ball hanging at the player's empty -- cell and the ring pulsing on the foe -- then a synthetic mouse flick -- throws the ball and the beats that follow are photographed: flight, -- the open-mouth suck, the drop-and-wobble, and the outcome. -- -- SHOT_DIR=.scratchpad/letsgo \ -- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/letsgo_shots.lua \ -- "/c/Program Files/LOVE/lovec.exe" . -- -- SHOT_DIR must already exist. DS_LETSGO_MODE=catching runs the same -- beats through the bag-menu route instead of the FULL auto-entry. return function(game) local U = dofile("tests/drivers/util.lua") local DIR = os.getenv("SHOT_DIR") or ".scratchpad" local MODE = os.getenv("DS_LETSGO_MODE") or "full" local Pokemon = require("src.pokemon.Pokemon") local BattleState = require("src.battle.BattleState") local Bag = require("src.inventory.Bag") local exports = game.mods and game.mods.exports local lib = exports and exports.DRAMATIC_SHAPE and exports.DRAMATIC_SHAPE.lib if not lib then U.log("DRAMATIC_SHAPE is not loaded -- enable it and run again") return end local LetsGo = lib.require("LetsGo") local CatchThrow = lib.require("CatchThrow") local BattleScene = lib.require("BattleScene") -- a spread of levels, because the Let's Go award pays every party -- member against its OWN level: a low one should pull far more from -- the same catch than a high one, and a flat payout would hide that game.save.party = { Pokemon.new(game.data, "CHARIZARD", 45), Pokemon.new(game.data, "PIKACHU", 10), Pokemon.new(game.data, "RATTATA", 5), } game.save.player.name = "RED" Bag.add(game.save, "POKE_BALL", 20, game.data) Bag.add(game.save, "GREAT_BALL", 5, game.data) -- DS_BALL forces the ball, so a MASTER_BALL run is a guaranteed catch -- and the experience payout can be read deterministically local FORCE = os.getenv("DS_BALL") if FORCE then Bag.add(game.save, FORCE, 5, game.data) CatchThrow.lastBall = FORCE end local expBefore = {} for i, m in ipairs(game.save.party) do expBefore[i] = { exp = m.exp, level = m.level } end LetsGo.setting:setValue(MODE, game) -- DS_RUNG=cards forces the 2D-3D A rung: the control run for the pic -- measurement path, against the STADIUM models the save may be on if os.getenv("DS_RUNG") == "cards" then lib.require("OverworldBattle").setting:setValue(true, game) U.log("3D-BTL forced to 2D-3D A") end U.log("LET'S GO mode: " .. tostring(LetsGo.mode())) U.teleport(game, "ROUTE_1", 5, 8, "down") U.wait(90) local battle = BattleState.newWild(game, "PIDGEY", 5) battle.onFinish = function() end game.overworld:pushBattle(battle) -- the wipe and the "Wild X appeared!" chatter: tap until the menu (or -- capture mode, which under FULL opens the instant the menu would) U.wait(70) for i = 1, 80 do if battle.phase == "menu" or CatchThrow.session() then break end U.tap(game, "a") U.wait(6) if i % 20 == 0 then U.log(("still driving intro: phase=%s queue=%d anim=%s") :format(tostring(battle.phase), #(battle.queue or {}), tostring(battle.animPlaying))) end end local function phase() local s = CatchThrow.session() return s and s.phase or ("battle:" .. tostring(battle.phase)) end if MODE == "catching" then -- the bag route: ITEM on the menu, then the ball U.wait(20) U.log("menu phase: " .. tostring(battle.phase)) -- cursor to ITEM (menu is 2x2: fight pkmn / item run) -- down once U.tap(game, "down"); U.wait(4) U.tap(game, "a"); U.wait(20) -- the bag opens on the first item; balls were added first U.tap(game, "a"); U.wait(10) else -- FULL: capture mode should have opened on its own U.wait(30) end U.log("capture phase: " .. phase()) do local s = CatchThrow.session() if s then local b = s.artBox U.log(("probe: span=%.1f enemyGB=%.0f,%.0f playerGB=%.0f,%.0f") :format(s.shot.enemySpan or -1, s.shot.enemy[1], s.shot.enemy[2], s.shot.player[1], s.shot.player[2])) U.log(("probe: eye=%.0f,%.0f,%.0f enemyW=%.0f,%.0f playerW=%.0f,%.0f") :format(s.shot.eye[1], s.shot.eye[2], s.shot.eye[3], s.enemyPos[1], s.enemyPos[3], s.playerPos[1], s.playerPos[3])) if b then U.log(("probe: artBox ax=%d ay=%d box=%d..%d,%d..%d body r=%.1f yOff=%.1f") :format(b.ax, b.ay, b.x0, b.x1, b.y0, b.y1, s.body.r, s.body.yOff)) else U.log(("probe: artBox=nil body r=%.1f yOff=%.1f") :format(s.body.r, s.body.yOff)) end end end U.shot(game, DIR .. "/1_aim.png") -- ------- the reach test -- -- Drag the ball to the far corners of the WINDOW -- including well -- below where the battle's text box used to be -- and report where it -- actually ends up in the GB frame. A fence shows up here as a ball -- that stops moving while the pointer keeps going. do local W, H = love.graphics.getDimensions() local s = CatchThrow.session() if s and love.mousepressed then local hx, hy = s.handGB[1], s.handGB[2] love.mousepressed(W * 0.5, H * 0.62, 1, false, 1) U.wait(2) local CORNERS = { { "bottom-centre", 0.50, 0.97 }, { "bottom-left", 0.06, 0.97 }, { "bottom-right", 0.94, 0.97 }, { "top-left", 0.06, 0.10 }, } for _, c in ipairs(CORNERS) do for i = 1, 4 do if love.mousemoved then love.mousemoved(W * c[2], H * c[3], 0, 0, false) end U.wait(1) end local q = CatchThrow.session() U.log(("reach %-14s -> ball GB %.0f,%.0f (frame is 160x144)") :format(c[1], q and q.handGB[1] or -1, q and q.handGB[2] or -1)) if c[1] == "bottom-centre" then U.shot(game, DIR .. "/1b_drag_low.png") end end -- a slow release is a dead flick: nothing is thrown if love.mousereleased then love.mousereleased(W * 0.06, H * 0.10, 1, false, 1) end U.wait(30) U.log("after reach test: " .. phase() .. (" (ball home was %.0f,%.0f)"):format(hx, hy)) end end -- A synthetic flick aimed like a hand: from the session's own geometry, -- pick a release point short of the ring and a sweep speed for a -- comfortable sigma, so the aim model (release + reach along the flick) -- lands the ball on the ring centre. Window units; the session maps -- them back to GB through the live letterbox. local aim = CatchThrow._aimInfo() if aim then local uw, uh = love.graphics.getDimensions() local function toWin(gx, gy) return (aim.lx + gx * aim.scale) * uw / aim.pw, (aim.ly + gy * aim.scale) * uh / aim.ph end -- the throw is the swipe's own velocity now: grab the ball and sweep -- toward the ring at a comfortable ~190 GB px/s for 8 frames. -- DS_SWIPE overrides it, which is how the strength band is swept: -- weak should fall short, comfortable should land, hard should still -- reach rather than sail over. local SPEED = tonumber(os.getenv("DS_SWIPE") or "") or 190 local FRAMES = 8 SPEED_USED = SPEED local hx, hy = aim.hand[1], aim.hand[2] local dx, dy = aim.ring[1] - hx, aim.ring[2] - hy local d = math.sqrt(dx * dx + dy * dy) dx, dy = dx / d, dy / d U.log(("aim: hand %.0f,%.0f ring %.0f,%.0f outer %.0f") :format(hx, hy, aim.ring[1], aim.ring[2], aim.outer)) local step = SPEED / 60 if love.mousepressed then local px, py = toWin(hx, hy) love.mousepressed(px, py, 1, false, 1) end for i = 1, FRAMES do local wx, wy = toWin(hx + dx * step * i, hy + dy * step * i) if love.mousemoved then love.mousemoved(wx, wy, 0, 0, false) end U.wait(1) end if love.mousereleased then local wx, wy = toWin(hx + dx * step * FRAMES, hy + dy * step * FRAMES) love.mousereleased(wx, wy, 1, false, 1) end -- what actually left the hand, before gravity has touched it local s0 = CatchThrow.session() if s0 and s0.vel then local p = s0.ballInst.pos U.log(("LAUNCH fwd/up/lat = %.1f %.1f %.1f from height %.1f, %.1f px out") :format(math.sqrt(s0.vel[1] ^ 2 + s0.vel[3] ^ 2), s0.vel[2], 0, p[2] - s0.groundY, math.sqrt((s0.enemyPos[1] - p[1]) ^ 2 + (s0.enemyPos[3] - p[3]) ^ 2))) else U.log("LAUNCH -- nothing was thrown (flick rejected)") end else U.log("NO AIM INFO -- capture mode did not open") end U.wait(4) -- did it CONNECT? poll the flight out and say plainly which way it -- went, so a strength sweep reads as a table instead of a guess do local verdict, peak = "no throw", 0 for _ = 1, 200 do local s = CatchThrow.session() if not s then verdict = "session gone" break end if s.phase == "flight" then local h = s.ballInst.pos[2] - s.groundY if h > peak then peak = h end verdict = "MISS (fell short / wide)" elseif s.phase == "suck" or s.phase == "drop" or s.phase == "wobble" then verdict = "HIT" .. (s.tier and (" " .. s.tier) or " (outside ring)") break elseif s.phase ~= "aim" then break end U.wait(1) end local body = CatchThrow.session() and CatchThrow.session().body U.log(("THROW swipe=%d -> %s (apex %.1f world px, body centre %.1f)") :format(SPEED_USED or -1, verdict, peak, body and body.yOff or -1)) end U.log("after flick: " .. phase()) U.shot(game, DIR .. "/2_flight.png") -- the suck: ball open at the foe, beam on, mon shrinking for _ = 1, 60 do U.wait(1) local s = CatchThrow.session() if s and s.phase == "suck" then break end if not CatchThrow.session() then break end end U.log("suck check: " .. phase() .. " shrink=" .. tostring(BattleScene.capture and BattleScene.capture.shrink)) U.shot(game, DIR .. "/3_suck.png") -- the drop and the first wobble for _ = 1, 120 do U.wait(1) local s = CatchThrow.session() if s and s.phase == "wobble" then break end if not s then break end end U.wait(30) U.log("wobble check: " .. phase() .. " shrink=" .. tostring(BattleScene.capture and BattleScene.capture.shrink)) U.shot(game, DIR .. "/4_wobble.png") -- the outcome: stars or burst, then the engine's own text for _ = 1, 300 do U.wait(1) local s = CatchThrow.session() if not s or s.phase == "epilogue" or s.phase == "burst" then break end end U.wait(20) U.log("outcome: " .. phase()) U.shot(game, DIR .. "/5_outcome.png") -- under FULL a breakout must land straight back in throw mode -- no -- enemy turn between; a catch plays out its epilogue instead -- keep tapping through the caught chatter so storeCaughtMon (and the -- experience payout hanging off it) actually runs for _ = 1, 60 do U.tap(game, "a"); U.wait(6) end U.log(("after outcome: %s battle=%s balls=%d") :format(phase(), tostring(battle.phase), game.save.inventory.POKE_BALL or 0)) for i, m in ipairs(game.save.party) do local was = expBefore[i] local nm = game.data.pokemon[m.species].name U.log(("EXP %-10s Lv%-3d -> Lv%-3d exp %d -> %d (+%d)") :format(nm, was.level, m.level, was.exp, m.exp, m.exp - was.exp)) end local combo = lib.require("LetsGo").combo() U.log("combo: " .. (combo and (combo.species .. " x" .. combo.count) or "none")) U.shot(game, DIR .. "/6_after.png") U.log("done -- " .. DIR) end