Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions LICENSE-ROXAS2712.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Roxas2712 — Fork Contributions

Version 1.0 · Effective 25 October 2026

Copyright © 2026 Roxas2712, to the extent of rights held.
All rights reserved except as expressly granted below or already granted.

## 1. Scope and ownership

This notice applies only to original contributions made by Roxas2712
(GitHub: Roxas2712) to `Roxas2712/Gen2Recomped`, including original additions
within modified files, insofar as he owns the relevant rights. Git history,
pull-request diffs and source provenance identify contributions; a commit
author or a whole-file change alone does not establish ownership of every
part of that file.

It does not claim ownership of Cedric's work, Gen1Recomp / BOIS CLUB GAMES
work, other contributors' work, third-party dependencies, or ROM-derived
content. Their applicable licences and notices remain in force. It does not
grant permission to distribute the complete combined fork where another
rights holder's permission is required.

## 2. General permission and reserved rights

For contributions covered by this notice and not already licensed more
broadly, you may read and study the published source and make private,
non-commercial modifications and builds for your own use. This grants no
general permission to redistribute, sublicense, sell, or offer those
contributions as a hosted service. Other uses require Roxas2712's
written permission, except for the specific permission below and rights
available independently under applicable law.

Keep the relevant copyright, licence and attribution notices. Do not
represent Roxas2712's contributions as exclusively authored by someone
else. Public source availability alone does not put them in the public
domain or grant an open-source licence.

## 3. Specific permission for Cedric / UNDERdecoded

Roxas2712 expressly grants Cedric (Ced; GitHub: UNDERdecoded /
UNDERdecodedHD) a non-exclusive, worldwide, perpetual, royalty-free
permission to use, reproduce, integrate, modify and distribute the covered
contributions that Roxas2712 submits or expressly offers through pull
requests to `UNDERdecoded/Gen2Recomped`, for Cedric's Gen2Recomped /
RetroRecompiled project. This includes its renamed continuation maintained
by Cedric, its official builds, and its commercial operation.

Cedric may license the incorporated contributions to recipients as part of
that project, subject to preserving this permission's applicable credit and
notice requirements. This permission is not a copyright assignment, is not
exclusive, and does not cover unpublished work, separate repositories,
ModCompass service operation, credentials, or content not offered in those
pull requests. Roxas2712 retains his own rights to his contributions.

For uses relying on this permission:

- retain the relevant source copyright and licence notices;
- give visible credit in the project's README or equivalent public project
page, and in the application's existing About / Credits view, if present;
- use at least the following credit, or a substantially equivalent one:

> Contributions by Roxas2712 —
> https://github.com/Roxas2712

A maintained consolidated contributor credit is sufficient; a separate
credit on every screen or for every change is not required. Adapted
contributions must not be attributed exclusively to another author. Other
contributors' credits must also remain intact.

## 4. Earlier licences and permissions remain effective

This notice does not revoke, narrow, or retrospectively add conditions to
any permission already validly granted, including MIT / GPL grants,
separate agreements, or permissions arising from previously submitted
pull requests. Where an earlier grant permits a use independently of this
notice, its existing terms govern that use. For such earlier uses, the
visible credit above is requested to the extent it is not already required.

For subsequent contributions, this is the default statement of Roxas2712's own reserved rights and the specific PR permission, subject to any
applicable existing licence obligations or separately agreed terms. It
does not change another rights holder's licence or guarantee compatibility
of a particular combination of code.

## 5. No warranty

To the extent permitted by applicable law, these contributions are provided
as is, without warranty. No support obligation, endorsement, or rights to
third-party code, game assets, names, or trademarks are granted.
96 changes: 72 additions & 24 deletions src/battle/BattleState.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2604,6 +2604,37 @@ function BattleState:beginMsgLine()
self.shown[#self.shown + 1] = {}
end

function BattleState:runQueueAction(item)
self.nextInsert=0
item.fn()
self.current=nil
local sequence=GameVersion.isGen3()and package.loaded['src.core.game3.battle.switch_seq']
if sequence then sequence._nativeFinish(self,item)end
end

function BattleState:stepGen3Ball(anim)
anim:update()
if anim.sound then
require("src.core.Sound").playId(self.data, anim.sound)
end
-- the trainer is gone by the time his Pokemon is out of the ball
-- ...unless the trainer's own walk-off is still running (#407): the
-- cartridge frees that sprite at the end of the fifty frames, not when
-- the ball opens, and cutting it short hides most of the throw
if anim.sendOut == "player" and self.showPlayerBack
and anim.phase ~= "throw"
and not (self.backWalkOff and self.backWalkOff > 0) then
self.showPlayerBack = false
self:slidePic("back")
end
if anim:isDone() then
self.gen3BallPlaying = false
-- a caught Pokemon leaves the ball resting on screen through the text,
-- exactly as the Game Boy chain does
if not anim.caught then self.gen3Ball = nil end
end
end

function BattleState:updateQueue()
if self.waitingUI then
if self.game.stack:top() ~= self then return true end
Expand Down Expand Up @@ -2637,28 +2668,12 @@ function BattleState:updateQueue()
-- reason: its frames are counted where the hold is counted, or it would
-- stand still for exactly as long as it is meant to be moving.
if self.gen3BallPlaying and self.gen3Ball then
local anim = self.gen3Ball
anim:update()
if anim.sound then
require("src.core.Sound").playId(self.data, anim.sound)
end
-- the trainer is gone by the time his Pokemon is out of the ball
-- ...unless the trainer's own walk-off is still running (#407): the
-- cartridge frees that sprite at the end of the fifty frames, not when
-- the ball opens, and cutting it short hides most of the throw
if anim.sendOut == "player" and self.showPlayerBack
and anim.phase ~= "throw"
and not (self.backWalkOff and self.backWalkOff > 0) then
self.showPlayerBack = false
self:slidePic("back")
end
if anim:isDone() then
self.gen3BallPlaying = false
-- a caught Pokemon leaves the ball resting on screen through the text,
-- exactly as the Game Boy chain does
if not anim.caught then self.gen3Ball = nil end
end
local capture=GameVersion.isGen3()and package.loaded['src.core.game3.battle.catch_seq']
if capture then capture._nativeBallStep(self,self.gen3Ball,BattleState.stepGen3Ball)
else self:stepGen3Ball(self.gen3Ball)end
end
local capture=GameVersion.isGen3()and package.loaded['src.core.game3.battle.catch_seq']
if capture and capture._holdsNativeQueue(self)then return true end
-- a queued hold (faint slide, hit blink) counts down before the next row
if self.waitFrames and self.waitFrames > 0 then
self.waitFrames = self.waitFrames - 1
Expand Down Expand Up @@ -2730,9 +2745,19 @@ function BattleState:updateQueue()
local item = table.remove(self.queue, 1)
if not item then return false end
if item.fn then
self.nextInsert = 0 -- sayNext inserts right after this item
item.fn()
self.current = nil
local sequence=GameVersion.isGen3()and package.loaded['src.core.game3.battle.switch_seq']
if item._sourceCatchStep and GameVersion.isGen3()then sequence=package.loaded['src.core.game3.battle.catch_seq']end
if sequence and(item._sourceSwitchStep or item._sourceCatchStep)then
local consumed=false
local ok,why=pcall(sequence._nativeStep,self,item,function()
consumed=true
self:runQueueAction(item)
return true
end)
-- A held or failing source callback cannot discard a native action.
if not consumed then table.insert(self.queue,1,item)end
if not ok then error(why,0)end
else self:runQueueAction(item)end
return true
end
if item.ui then
Expand Down Expand Up @@ -3806,6 +3831,7 @@ function BattleState:enter()
self.enemySendingOut = false
self:startGrowIn(self.enemy)
end)
if GameVersion.isGen3()then self.queue[#self.queue]._sourceIntroKind='opponent_sendout'end
self:shinyAnimAppend(self.enemy, true)
queueEnemyCry()
elseif self.kind == "link" then
Expand All @@ -3819,6 +3845,7 @@ function BattleState:enter()
self.enemySendingOut = false
self:startGrowIn(self.enemy)
end)
if GameVersion.isGen3()then self.queue[#self.queue]._sourceIntroKind='opponent_sendout'end
self:shinyAnimAppend(self.enemy, true)
queueEnemyCry()
end
Expand Down Expand Up @@ -3858,6 +3885,7 @@ function BattleState:enter()
-- then the POOF plays and the mon appears with its cry
-- (SendOutMon: message -> AnimateSendingOutMon -> PlayCry)
table.insert(self.queue, { anim = "POOF_ANIM", attackerIsPlayer = false })
if GameVersion.isGen3()then self.queue[#self.queue]._sourceIntroKind='player_throw'end
self:shinyAnimAppend(self.player, false)
self:act(function()
self.sendingOut = false
Expand All @@ -3884,6 +3912,8 @@ function BattleState:enter()
self:abilitySwitchIn(self.player)
end })
end
local intro=GameVersion.isGen3()and package.loaded['src.core.game3.battle.intro_seq']
if intro then intro._nativeBind(self)end
Runtime.emit("battle.started", {
battle = self, kind = self:battleKind(),
trainerId = self.trainer and self.trainer.id,
Expand All @@ -3895,6 +3925,12 @@ end
-- any pop (finish, script teardown) must silence the alarm loop
-- (end_of_battle.asm clears wLowHealthAlarm when a battle ends)
function BattleState:exit()
local intro=package.loaded['src.core.game3.battle.intro_seq']
if intro then intro._nativeRelease(self)end
local capture=package.loaded['src.core.game3.battle.catch_seq']
if capture then capture._nativeRelease(self)end
local sequence=package.loaded['src.core.game3.battle.switch_seq']
if sequence then sequence._nativeRelease(self)end
-- Teachy TV temporarily replaces the player's party exactly like
-- SavePlayerParty/LoadPlayerParty in teachy_tv.c. finish() normally
-- restores it before the pop, but exit is the safety net for a forced state
Expand Down Expand Up @@ -5941,6 +5977,7 @@ function BattleState:switchPlayerInto(pos, newMon)
self:markParticipant()
self.sendingOut = true
self:sayNext(self:sendOutText(incoming.name))
local sendMessage=self.queue[self.nextInsert]
self:animNext("POOF_ANIM", false)
self:shinyAnim(incoming, false)
self:actNext(function()
Expand All @@ -5950,9 +5987,12 @@ function BattleState:switchPlayerInto(pos, newMon)
require("src.core.Sound").playCry(self.data, incoming.mon.species)
HeldItems.onEntry(self, incoming)
end)
local sendEntry=self.queue[self.nextInsert]
-- ABILITYEFFECT_ON_SWITCHIN, after the send-out and its cry: an
-- INTIMIDATE drops the foe's attack now, before the free move below
self:actNext(function() self:abilitySwitchIn(incoming) end)
local sequence=GameVersion.isGen3()and package.loaded['src.core.game3.battle.switch_seq']
if sequence then sequence._nativeBind(self,pos,sendMessage,sendEntry,self.queue[self.nextInsert])end
return incoming
end

Expand Down Expand Up @@ -12963,4 +13003,12 @@ function BattleState:drawClassic()
end
end

-- Source capture callbacks borrow the actual native request builder.
local throwBallNative=BattleState.throwBall
function BattleState:throwBall(ball)
local source=GameVersion.isGen3()and package.loaded['src.core.game3.battle.catch_seq']
if source then return source._nativeBegin(self,throwBallNative,ball)end
return throwBallNative(self,ball)
end

return BattleState
22 changes: 22 additions & 0 deletions src/compat/gen1/NativeBallObservation.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Copyright (c) 2026 Roxas2712. See LICENSE-ROXAS2712.md.
-- Shared scalar projection of the actual native ball, not another timeline.
local M={keys={'visible','x','y','frame','ballId','side','battler','rot','scale','alpha'}}
function M.read(screen,ball,key)
if key=='visible'then return not ball.done or ball.caught==true end
if key=='x'then return ball.x+(ball.shakeOffset or 0)end
if key=='y'then return ball.y end
if key=='frame'then return math.max(0,(ball.ballFrame or 1)-1)end
if key=='ballId'then return ball.ball end
if key=='battler'or key=='side'then
local id=1
for i=0,3 do
local b=i==0 and screen.player or i==1 and screen.enemy or screen:battlerAt(i)
if b and ball.battler==b then id=i;break end
end
return key=='battler'and id or(id%2==0 and'player'or'enemy')
end
if key=='rot'then return 0 end -- The existing native painter is unrotated.
if key=='scale'then return 1 end -- Native ball cells draw at their own size.
if key=='alpha'then return 1-(ball.whiteout or 0)end
end
return M
Loading