-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpatch.lua
More file actions
208 lines (197 loc) · 8.77 KB
/
Copy pathpatch.lua
File metadata and controls
208 lines (197 loc) · 8.77 KB
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
-- The engine patches (declared engine_internals). Two seams:
--
-- BattleState.update wrapped to feed the mash counter each fixed
-- step, so presses during the wobbles are seen
-- even though the battle queue ignores input
-- while an animation plays
-- BattleState.throwBall replaced for the wild catchable path only:
-- the vanilla roll still happens up front (via
-- catchAttempt, so the catch.rate hook chain is
-- preserved), but on a failed-with-wobbles throw
-- the break-free/redeemed decision is deferred
-- to an act that runs AFTER the last wobble --
-- the window the legend always claimed existed
--
-- Everything else -- trainer block, ghost/noCatch dodge, Safari,
-- caught-outright, clean miss -- delegates to (or mirrors row-for-row)
-- the vanilla flow in src/battle/BattleState.lua:throwBall/ballChain.
-- install() refuses to patch unless every engine function it leans on
-- still exists, so an upstream reshape degrades to vanilla, never to a
-- broken battle.
local Patch = {}
-- deps: enabled(), ceiling(shakes)->max chance, roll()->[0,1),
-- onRedeemed(battle, ball, shakes), counter (mash.lua), log,
-- technique() -> technique table resolved at window-open (optional; nil
-- arms the counter's default A/B mash)
function Patch.install(deps)
local ok, BattleState = pcall(require, "src.battle.BattleState")
if not ok then return nil, "src.battle.BattleState did not load" end
local okR, Runtime = pcall(require, "src.mods.Runtime")
if not okR then return nil, "src.mods.Runtime did not load" end
local okS, Strings = pcall(require, "src.core.Strings")
if not okS then return nil, "src.core.Strings did not load" end
for _, name in ipairs({
"update", "throwBall", "ballChain", "catchAttempt", "tossAnimFor",
"ballMissMessage", "storeCaughtMon", "executeAction", "enemyAction",
"endOfTurn", "say", "act", "animNext", "actNext", "sayNext",
}) do
if type(BattleState[name]) ~= "function" then
return nil, "BattleState." .. name .. " is missing"
end
end
if type(Runtime.emit) ~= "function" then
return nil, "Runtime.emit is missing"
end
if BattleState.__mashCatch then
return nil, "already installed"
end
local counter = deps.counter
local origUpdate = BattleState.update
local origThrowBall = BattleState.throwBall
-- rhythm's wobble clock: the AnimPlayer pre-stamps one SFX_TINK on its
-- event timeline per ball wobble, and elapsed advances a frame per tick.
-- Watch those fire and mark a beat on the counter for each -- passively
-- (the pollEffects cursor belongs to the engine), by scanning events
-- against elapsed. A fresh events table or a rewound clock means the
-- player restarted, so the seen-count resets with it. If any of this
-- surface is missing, no beats are marked and rhythm scoring degrades
-- to plain mash (mash.lua's no-beats fallback).
local beatWatch = { events = nil, elapsed = -1, seen = 0 }
local function watchBeats(battle)
local tech = counter.technique
if not (counter.armed and tech and tech.style == "rhythm") then return end
local ap = battle.animPlayer
if not (ap and type(ap.events) == "table"
and type(ap.elapsed) == "number") then return end
if ap.events ~= beatWatch.events or ap.elapsed < beatWatch.elapsed then
beatWatch.events, beatWatch.seen = ap.events, 0
end
beatWatch.elapsed = ap.elapsed
local fired = 0
for _, ev in ipairs(ap.events) do
if ev.effect == "SFX_TINK" and ev.frame <= ap.elapsed then
fired = fired + 1
end
end
while beatWatch.seen < fired do
counter:markBeat()
beatWatch.seen = beatWatch.seen + 1
end
end
function BattleState:update(dt)
if counter.armed then
counter:step(self.game and self.game.input)
end
local result = origUpdate(self, dt)
watchBeats(self)
return result
end
-- mirrors the vanilla caught tail (throwBall lines after ballChain);
-- a redeemed catch adds one page telling the player their mashing is
-- what did it. Two lines max within the 18-column budget: the battle
-- box shows two lines, and a third \n line scrolls in without waiting
-- for A (the pokewalker walk-report lesson)
local function queueCaughtTail(battle, redeemed)
battle:actNext(function()
require("src.core.Sound").play(battle.data, "Caught_Mon")
end)
battle:sayNext(Strings("All right!\n%s was\ncaught!", battle.enemy.name))
if redeemed then
battle:sayNext(Strings("But the mashing\nheld it shut!"))
end
battle:act(function() battle:storeCaughtMon() end)
end
-- mirrors the vanilla breakout tail (ballChain's POOF+SHOWPIC plus
-- throwBall's miss message / enemy turn / end of turn)
local function queueMissTail(battle, shakes)
battle:animNext("POOF_ANIM", true)
battle:animNext("SHOWPIC_ANIM", true)
battle:sayNext(battle:ballMissMessage(shakes))
battle:act(function()
battle:executeAction(battle.enemy, battle.player, battle:enemyAction())
end)
battle:act(function() battle:endOfTurn() end)
end
function BattleState:throwBall(ball)
-- only a wild, catchable throw carries the legend; everything else
-- (trainer block, ghost dodge, mod disabled) is vanilla's business
if not deps.enabled() or self.kind ~= "wild"
or self.ghost or self.noCatch then
return origThrowBall(self, ball)
end
self:say(Strings("%s used\n%s!", self.game.save.player.name,
self.data.items[ball].name))
self:act(function()
require("src.core.Sound").play(self.data, "Ball_Toss")
self.lastBall = ball
-- the vanilla roll, through the catch.rate hook chain like always;
-- the emitted payload is the physics that just happened -- a later
-- redemption still fires pokemon.caught through storeCaughtMon
local caught, shakes = self:catchAttempt(ball)
Runtime.emit("battle.ball_thrown", {
battle = self, ball = ball, caught = caught, shakes = shakes,
})
-- ItemUseBall's 20-frame beat (vanilla row-for-row)
self.nextInsert = (self.nextInsert or 0) + 1
table.insert(self.queue, self.nextInsert, { wait = 20 })
if caught or shakes == 0 then
-- caught outright or bounced straight off: vanilla, untouched
self:ballChain(self:tossAnimFor(ball), caught, shakes, ball)
if caught then
queueCaughtTail(self)
else
self:sayNext(self:ballMissMessage(shakes))
self:act(function()
self:executeAction(self.enemy, self.player, self:enemyAction())
end)
self:act(function() self:endOfTurn() end)
end
return
end
-- gripped and wobbling: play ballChain's grip prefix, then decide
-- the ending only after the last wobble has been seen (and mashed).
-- The technique resolves here, at window-open, so an options change
-- mid-battle can never straddle a window.
counter:arm(deps.technique and deps.technique() or nil)
beatWatch.events, beatWatch.elapsed, beatWatch.seen = nil, -1, 0
self:animNext(self:tossAnimFor(ball), true, nil, ball)
self:animNext("POOF_ANIM", true)
self:animNext("HIDEPIC_ANIM", true)
self:animNext("SHAKE_ANIM", true, shakes)
self:actNext(function()
local factor = counter:score()
counter:disarm()
local chance = (deps.ceiling(shakes) or 0) * factor
-- no mashing, no roll: the shared battle rng was never touched
-- and this stream isn't drawn either, so an idle player's game
-- is bit-identical to vanilla
if chance > 0 and deps.roll() < chance then
deps.onRedeemed(self, ball, shakes)
-- a screen flash marks the moment the mashing takes hold (the
-- same fx seam updateQueue uses for animation-less flashes)
self.fx = self.fx or {}
self.fx.flash = 16
-- one more wobble, then the capture ending ballChain gives a
-- $43 chain: the resting ball stays on screen (lockedBall)
self:animNext("SHAKE_ANIM", true, 1)
self:actNext(function()
self.lockedBall = self.animPlayer and self.animPlayer:finalSprites()
or nil
end)
queueCaughtTail(self, true)
else
queueMissTail(self, shakes)
end
end)
end)
end
BattleState.__mashCatch = true
local function uninstall()
if not BattleState.__mashCatch then return end
BattleState.update = origUpdate
BattleState.throwBall = origThrowBall
BattleState.__mashCatch = nil
end
return { uninstall = uninstall, watchBeats = watchBeats }
end
return Patch