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.
15 changes: 15 additions & 0 deletions src/battle/BattleState.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2605,6 +2605,7 @@ function BattleState:beginMsgLine()
end

function BattleState:updateQueue()
if self._levelUpStreaks and require('src.battle.Gen3LevelUpStreaks').update(self)then return true end
if self.waitingUI then
if self.game.stack:top() ~= self then return true end
self.waitingUI = nil
Expand Down Expand Up @@ -2728,6 +2729,10 @@ function BattleState:updateQueue()
end
if not self.current then
local item = table.remove(self.queue, 1)
while item and item.gbaLevelUpStreaks do
if require('src.battle.Gen3LevelUpStreaks').start(self,item)then return true end
item=table.remove(self.queue,1)
end
if not item then return false end
if item.fn then
self.nextInsert = 0 -- sayNext inserts right after this item
Expand Down Expand Up @@ -3895,6 +3900,7 @@ end
-- any pop (finish, script teardown) must silence the alarm loop
-- (end_of_battle.asm clears wLowHealthAlarm when a battle ends)
function BattleState:exit()
require('src.battle.Gen3LevelUpStreaks').release(self)
-- 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 @@ -8954,6 +8960,15 @@ function BattleState:awardExp(fallen)
require("src.pokemon.Evolution").changeHappiness(mon, "LEVELUP")
end
self:sayNext(Strings("%s grew\nto level %d!", name, lv))
if GameVersion.isGen3() and self.data.constants.gen3LevelUpStreaks then
for position=0,3 do
local actor=self:battlerAt(position)
if actor and actor.mon==mon then
require('src.battle.Gen3LevelUpStreaks').queue(self,actor)
break
end
end
end
self:uiNext(function()
require("src.core.Sound").play(game.data, "Level_Up")
return StatBox.new(game, mon)
Expand Down
5 changes: 5 additions & 0 deletions src/battle/Gen3Battle.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3104,6 +3104,11 @@ function Gen3Battle.draw(battle)
-- over them. The methods dispatch back here (BattleState:drawHUDs and
-- friends), so the implementation is still this file's; what changed is
-- that there is now a door in front of it.
shaken(function()
local layer=package.loaded['src.core.game3.battle.level_up_streaks']
local draw=function()return require('src.battle.Gen3LevelUpStreaks').draw(battle)end
if layer then layer._nativeDraw(battle,draw)else draw()end
end)
shaken(function() battle:drawHUDs(slide) end)
-- the ball rides with the pics rather than with the windows: it is thrown
-- across the field, and a screen shake moves the field
Expand Down
78 changes: 78 additions & 0 deletions src/battle/Gen3LevelUpStreaks.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
-- Copyright (c) 2026 Roxas2712. See LICENSE-ROXAS2712.md.
-- Pure samples of the native 18-sprite sequence, driven by BattleState.frame.
-- No RNG, timer, GPU object, battle or save is retained here.
-- Interface/timing facts: pinned pret/pokefirered pokemon_special_anim_scene.c
-- and random.h (ce6406d8). Integer low bits avoid floating LCG overflow.
local M={}
local finish=0
for index=1,18 do
local speed=(45*index+51)%64+32
finish=math.max(finish,2*(index-1)+math.floor(1024/speed)+1)
end
function M.busy(frame)return frame<finish end
function M.queue(battle,user)
if not require('src.core.GameVersion').isGen3()or not battle.data.constants.gen3LevelUpStreaks then return false end
battle.nextInsert=(battle.nextInsert or 0)+1
table.insert(battle.queue,battle.nextInsert,{gbaLevelUpStreaks=true,position=user.position,
isPlayer=user.isPlayer})
return true
end
function M.start(battle,row)
if not require('src.core.GameVersion').isGen3()then return false end
local user=row.position~=nil and battle:battlerAt(row.position)
or(row.isPlayer and battle.player or battle.enemy)
local hidden=battle.picFx and user and battle.picFx[user]and battle.picFx[user].hidden
if not(user and user.mon and user.mon.hp>0 and not user.fainted and not hidden
and battle.data.constants.gen3LevelUpStreaks)then return false end
battle._levelUpStreaks={user=user,startFrame=battle.frame}
local options=battle.game and battle.game.save and battle.game.save.options or{}
require('src.core.Sound').playSe(battle.data,'SE_RS_SHOP',{mono=options.stereo==false})
return true
end
function M.update(battle)
local state=battle._levelUpStreaks
if not state then return false end
if M.busy(battle.frame-state.startFrame)then return true end
battle._levelUpStreaks=nil
return false
end
function M.sample(frame,visit)
assert(type(frame)=='number'and frame==frame and frame>=0 and frame%1==0,'invalid native streak frame')
assert(type(visit)=='function','native streak visitor must be callable')
local alive=0
-- Reverse OAM order: the first-created sprite remains in front of siblings.
for index=18,1,-1 do
local age=frame-2*(index-1)
if age>=0 then
local speed=(45*index+51)%64+32
local y=math.floor(-speed*age/16)
if y>=-64 then
alive=alive+1;visit((index*219)%64-32,32+y,index,speed)
end
end
end
return alive>0 or frame<34,alive
end
function M.release(battle)
if battle._levelUpBlend then battle._levelUpBlend:release();battle._levelUpBlend=nil end
battle._levelUpStreaks=nil
end
function M.draw(battle)
local state=battle._levelUpStreaks
if not state then return false end
local record=battle.data.constants.gen3LevelUpStreaks
if not record then return false,'native vertical-streak resource unavailable'end
local x,y=require('src.battle.Gen3Battle').battlerCentre(battle,state.user)
if not x then return false,'native vertical-streak battler has no drawn sprite'end
local image=require('src.render.Assets').image(record.image)
local blend=battle._levelUpBlend
if not blend then blend=require('src.render.NativeGbaAlpha').new();battle._levelUpBlend=blend end
local ok,why=blend:paint(12,6,function()
M.sample(battle.frame-state.startFrame,function(dx,dy)
love.graphics.draw(image,x+dx,y+dy,0,1,1,4,8)
end)
end)
if not ok then error(why,0)end
return true
end
return M
34 changes: 34 additions & 0 deletions src/core/game3/battle/level_up_streaks.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-- Copyright (c) 2026 Roxas2712. See LICENSE-ROXAS2712.md.
-- Borrow the native queue-owned effect. The source facade owns no timer,
-- artwork, battle state or renderer resources.
local Battle=require('src.core.game3.battle')
local Streaks=require('src.battle.Gen3LevelUpStreaks')
local Scope=require('src.render.GraphicsScope')
local Boundary=require('src.compat.gen1.ScriptBoundary')
local Call=require('src.compat.gen1.SourceCall')
local M={}
local unpack=table.unpack or unpack
local function pack(...)return{n=select('#',...),...}end
local function draw(state)
if Boundary.active('battle.levelup.draw')then return Boundary.invoke('battle.levelup.draw')end
local screen=Battle._screen(state)
if not screen then return false,'no admitted native battle'end
return Streaks.draw(screen)
end
M.draw=draw
function M._nativeDraw(screen,body)
if M.draw==draw or Battle._screen()~=screen then return body(screen)end
if Boundary.active('battle.levelup.draw')then return Boundary.invoke('battle.levelup.draw')end
local stack=screen.game.stack;local top=stack:top()
local result=pack(Scope.call(function()
return Boundary.dispatch('battle.levelup.draw',function()
if Battle._screen()~=screen or screen.game.stack~=stack or stack:top()~=top then
return false,'native level-up presentation admission changed'
end
return body(screen)
end,function()return Call.call(M,'draw',Battle.getState())end)
end))
if not result[1]then error(result[2],0)end
return unpack(result,2,result.n)
end
return M
49 changes: 49 additions & 0 deletions src/import/Gen3LevelUpArt.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-- Copyright (c) 2026 Roxas2712. See LICENSE-ROXAS2712.md.
-- FRLG's additional vertical streaks use their own 8x16 cartridge sprite.
-- Offsets were found by matching every decoded pixel/palette word against
-- pret/pokefirered ce6406d8's level_up.png, then checked on both exact ROMs.
-- Publish hashes and addresses only; artwork remains in the user's cartridge.
local Version=require('src.core.GameVersion')
local M={REVISION=1}
local profiles={firered={gfx=4561032,palette=4561000},leafgreen={gfx=4559528,palette=4559496}}
local gfxDigest='0dbfbcf3f4a3fef33ba7449f89c2e510f780dfc063c4dd631c1715d3848152b5'
local palDigest='fbd58ce9122f0aa5058167d5e185820f19ba95d86f5ed52629d19bf2b0937f9a'
function M.supports(edition)return profiles[edition]~=nil end
local function digest(bytes)
return love.data.encode('string','hex',love.data.hash('sha256',bytes))
end
function M.read(x)
local edition=type(x.version)=='table'and x.version.id or x.version
local profile=profiles[edition]
if not profile then return nil,'no native vertical-streak profile for this edition'end
local info=Version.info(edition)
if not(x.manifest and x.manifest.romSha1==info.sha1)
or require('src.import.RomVerification').sha1(x.rom.data)~=info.sha1 then
return nil,'native vertical-streak cartridge fingerprint mismatch'
end
local ok,tiles=pcall(x.rom.lz77,x.rom,profile.gfx)
if not ok or type(tiles)~='table'or #tiles~=64 then return nil,'invalid native vertical-streak tile size'end
local packed={};for i=1,#tiles do packed[i]=string.char(tiles[i])end
if digest(table.concat(packed))~=gfxDigest then return nil,'native vertical-streak tile fingerprint mismatch'end
local palette,raw={},{}
for index=0,15 do
local color=x.rom:u16(profile.palette+index*2)
if color>=32768 then return nil,'invalid native vertical-streak palette'end
palette[index]=color;raw[#raw+1]=string.char(color%256,math.floor(color/256))
end
if digest(table.concat(raw))~=palDigest then return nil,'native vertical-streak palette fingerprint mismatch'end
return {tiles=tiles,palette=palette,width=8,height=16,edition=edition,
romSha1=info.sha1,gfxOffset=profile.gfx,paletteOffset=profile.palette}
end
function M.image(record)
local image=require('src.import.ImageWriter').blank(8,16)
for y=0,15 do for x=0,7 do
local byte=record.tiles[y*4+math.floor(x/2)+1]
local index=math.floor(byte/(x%2==0 and 1 or 16))%16
local color=record.palette[index]
image:setPixel(x,y,(color%32)/31,(math.floor(color/32)%32)/31,
(math.floor(color/1024)%32)/31,index==0 and 0 or 1)
end end
return image
end
return M
18 changes: 18 additions & 0 deletions src/import/RomExtractorGen3.lua
Original file line number Diff line number Diff line change
Expand Up @@ -54302,6 +54302,23 @@ RomExtractorGen3.BATTLE_TEXTBOX = {
CURSOR_AT = { { 128, 120 }, { 184, 120 }, { 128, 136 }, { 184, 136 } },
}

function RomExtractorGen3:extractLevelUpStreaks()
local Art=require('src.import.Gen3LevelUpArt')
local edition=type(self.version)=='table'and self.version.id or self.version
if not Art.supports(edition)then return end
self:beginStage('Gen3 level-up streaks')
local record,why=Art.read(self)
assert(record,why)
local image=Art.image(record)
self:saveImage(image,'battle/level_up_streak.png')
image:release()
self._constants=self._constants or{}
self._constants.gen3LevelUpStreaks={image='assets/generated/battle/level_up_streak.png',
width=record.width,height=record.height,romSha1=record.romSha1,
gfxOffset=record.gfxOffset,paletteOffset=record.paletteOffset}
self:write('constants',self._constants)
end

function RomExtractorGen3:extractBattleTextbox()
self:beginStage("Gen3 battle textbox")
local T = RomExtractorGen3.BATTLE_TEXTBOX
Expand Down Expand Up @@ -58871,6 +58888,7 @@ RomExtractorGen3.ASSET_STAGES = {
"extractItemIcons",
"extractPokenav",
"extractBattleTextbox",
"extractLevelUpStreaks",
"extractPokedexScreen",
"extractBallAnims",
"extractSummaryWindows",
Expand Down
4 changes: 4 additions & 0 deletions src/import/RomImporter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,10 @@ local function markerFor(version)
if version == "red" or version == "blue" or version == "yellow" then
revision = "gen1-trade-art-v1:"
end
local levelArt=require('src.import.Gen3LevelUpArt')
if GameVersion.generation(version)==3 and levelArt.supports(version)then
revision=revision..'gba-levelup-streaks-v'..levelArt.REVISION..':'
end
return CACHE_FORMAT .. revision .. GameVersion.info(version).sha1
end

Expand Down
Loading