From 45742481bf441cafecbb15150f63d1134f2e033f Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Tue, 1 Sep 2026 13:45:00 -0400 Subject: [PATCH] CLOSES #2025, CLOSES #2029, CLOSES #2030, CLOSES #2031, CLOSES #2033, CLOSES #2034, CLOSES #2035, CLOSES #2038, CLOSES #2040, CLOSES #2041, CLOSES #2042, CLOSES #2043, CLOSES #2044, CLOSES #2045, CLOSES #2046, CLOSES #2048, CLOSES #2049, CLOSES #2050, CLOSES #2051, CLOSES #2052, CLOSES #2053, CLOSES #2054 --- data/scripts/story2.lua | 39 +- data/scripts/story3.lua | 46 +- data/scripts/story4.lua | 94 +- docs/linux-arm-sbc.md | 2 +- docs/shaderfx.md | 32 +- src/battle/BattleState.lua | 36 +- src/battle/MoveEffects.lua | 32 +- src/battle/gen2/Battle.lua | 40 +- src/battle/gen2/Encounter.lua | 81 +- src/battle/gen2/HpBar.lua | 57 +- src/core/ChipAudio.lua | 120 ++- src/core/ChipSynth.lua | 47 + src/core/FaithfulRes.lua | 28 +- src/core/Game2.lua | 26 +- src/core/IssueReport.lua | 21 + src/core/Music.lua | 9 +- src/core/RomText.lua | 8 +- src/core/SaveData.lua | 69 +- src/core/Timing.lua | 3 + src/core/VideoMode.lua | 12 +- src/core/chip_worker.lua | 17 +- src/core/gen2/CommonText.lua | 7 + src/core/gen2/ItemEffects.lua | 2 + src/import/CacheContract.lua | 6 +- src/import/LauncherView.lua | 122 ++- src/import/RomExtractorGen2.lua | 104 +- src/import/RomImporter.lua | 74 +- src/mods/Gen2Compat.lua | 20 +- src/mods/Schemas.lua | 6 +- src/online/TeamPick.lua | 12 +- src/render/GbcPalette.lua | 2 + src/render/MonAnim.lua | 23 +- src/render/MonAnimView.lua | 59 ++ src/render/ShaderFX.lua | 156 ++- src/render/ShaderFixup.lua | 13 +- src/render/TextBox.lua | 26 +- src/save_convert/GenSave.lua | 5 +- src/script/Commands.lua | 5 + src/script/gen2/Vm.lua | 31 +- src/sync/SyncEngine.lua | 162 ++- src/ui/BagMenu.lua | 18 +- src/ui/EvolutionState.lua | 130 ++- src/ui/OptionsMenu.lua | 7 + src/ui/Screens.lua | 5 +- src/ui/ShaderFXScreen.lua | 13 +- src/ui/SkinStudio.lua | 24 +- src/ui/gen2/BattleHud.lua | 10 +- src/ui/gen2/BattleState.lua | 307 +++++- src/ui/gen2/BattleTransition.lua | 282 +++-- src/ui/gen2/BlankScreen.lua | 6 +- src/ui/gen2/CenterPcMenu.lua | 63 +- src/ui/gen2/DayCareMenu.lua | 22 +- src/ui/gen2/DecorationMenu.lua | 39 +- src/ui/gen2/EggHatchAnim.lua | 35 +- src/ui/gen2/EvolutionAnim.lua | 122 ++- src/ui/gen2/HallOfFame.lua | 52 +- src/ui/gen2/ItemPcMenu.lua | 58 +- src/ui/gen2/MainMenu.lua | 58 +- src/ui/gen2/MartMenu.lua | 20 +- src/ui/gen2/MenuFade.lua | 165 +++ src/ui/gen2/OptionsMenu.lua | 2 + src/ui/gen2/PackMenu.lua | 9 +- src/ui/gen2/PartyMenu.lua | 107 +- src/ui/gen2/SaveMenu.lua | 79 +- src/ui/gen2/SummaryMenu.lua | 43 +- src/ui/gen2/TradeAnim.lua | 39 +- src/ui/gen2/TradeMenu.lua | 1 + src/ui/gen2/Typer.lua | 161 +++ src/ui/kit/Kit.lua | 2 +- src/world/OverworldController.lua | 25 +- src/world/gen2/FieldMoves.lua | 81 +- src/world/gen2/MapNameSign.lua | 50 +- src/world/gen2/World.lua | 118 ++- src/world/gen2/WorldAPI.lua | 73 ++ tests/data/shaderfx/es/bevel.lua | 75 ++ tests/data/shaderfx/es/gameboy.lua | 983 ++++++++++++++++++ tests/data/shaderfx/es/gb-palette-dmg.lua | 64 ++ tests/data/shaderfx/es/lcd3x.lua | 73 ++ tests/data/shaderfx/es/pixel_transparency.lua | 503 +++++++++ tests/data/shaderfx/es/zfast-lcd.lua | 78 ++ tests/data/shaderfx/gl/bevel.lua | 75 ++ tests/data/shaderfx/gl/gameboy.lua | 973 +++++++++++++++++ tests/data/shaderfx/gl/gb-palette-dmg.lua | 64 ++ tests/data/shaderfx/gl/lcd3x.lua | 73 ++ tests/data/shaderfx/gl/pixel_transparency.lua | 501 +++++++++ tests/data/shaderfx/gl/zfast-lcd.lua | 78 ++ .../battle_substitute_bug2030_test.lua | 101 ++ .../drivers/battle_transform_bug2030_test.lua | 102 ++ tests/drivers/crystal_battle_text.lua | 138 +++ .../drivers/crystal_evolution_anim_shots.lua | 90 ++ .../drivers/crystal_fly_bird_bug2015_test.lua | 10 + tests/drivers/crystal_fly_bug1960_test.lua | 10 + .../crystal_fly_flash_bug2020_test.lua | 23 +- .../drivers/crystal_fly_hold_bug2051_test.lua | 248 +++++ tests/drivers/crystal_headbutt_probe.lua | 109 ++ tests/drivers/crystal_hp_bar_cadence.lua | 103 ++ .../crystal_map_name_sign_speed_bug1994.lua | 41 +- tests/drivers/crystal_menu_whites_s6.lua | 175 ++++ .../drivers/crystal_music_worker_bug2002.lua | 81 ++ tests/drivers/crystal_rock_smash_menu_s5.lua | 94 ++ tests/drivers/crystal_sendout_pic.lua | 119 +++ .../crystal_softboiled_field_bug2041.lua | 121 +++ tests/drivers/crystal_trade_anim_shots.lua | 79 ++ .../drivers/crystal_trainer_arrow_bug2045.lua | 51 + tests/drivers/evolution_black_bug279_test.lua | 36 +- .../drivers/evolution_cancel_bug213_test.lua | 16 +- tests/drivers/evolution_flip_bug1412_test.lua | 7 +- tests/drivers/evolution_move_bug12_test.lua | 5 +- tests/drivers/evolution_timing_bug2033.lua | 103 ++ tests/drivers/fossil_gift_bug2034_test.lua | 90 ++ .../drivers/gen2_pc_save_overlay_bug2049.lua | 120 +++ tests/drivers/gen2_save_order_s5.lua | 102 ++ tests/drivers/gen2_text_speed_bug2046.lua | 93 ++ tests/drivers/gold_center_pc.lua | 12 + tests/drivers/gold_dig_warp.lua | 10 +- tests/drivers/gold_evolution_shots.lua | 16 +- tests/drivers/gold_exp_bar.lua | 97 +- tests/drivers/gold_field_items.lua | 42 +- .../drivers/gold_fishing_rod_bug1708_test.lua | 7 +- tests/drivers/gold_fly_anim_bug1711_test.lua | 13 + .../gold_flymap_cursor_bug1712_test.lua | 13 + tests/drivers/gold_halloffame_shots.lua | 18 +- tests/drivers/gold_menu_shots.lua | 7 + tests/drivers/gold_naming_and_trade_item.lua | 9 + tests/drivers/gold_party_held_item.lua | 9 + tests/drivers/gold_party_submenu.lua | 9 + tests/drivers/gold_radio_persist.lua | 15 +- .../gold_stats_page_tint_bug1693_test.lua | 9 + .../gold_stats_text_paper_bug1858_test.lua | 9 + tests/drivers/gold_transition_shots.lua | 16 +- tests/drivers/launcher_skins_perf.lua | 69 ++ tests/drivers/launcher_spinner_shot.lua | 59 ++ tests/drivers/launcher_sync_shot.lua | 13 + tests/drivers/oaks_aide_jingle_2031.lua | 58 ++ .../drivers/prize_full_party_bug2034_test.lua | 116 +++ .../drivers/rare_candy_statbox_hold_test.lua | 102 ++ tests/drivers/shader_gles_validate.lua | 130 +++ .../battle_substitute_doll_timing_bug2030.lua | 137 +++ .../battle_transform_pic_timing_bug2030.lua | 105 ++ tests/engine/cart_saves.lua | 48 + tests/engine/chip_audio_preroll_test.lua | 7 +- tests/engine/chip_audio_stats_test.lua | 142 +++ tests/engine/chip_mono_fastpath_test.lua | 114 ++ tests/engine/chip_sample_rate_test.lua | 166 +++ .../engine/crystal_map_name_sign_bug1943.lua | 34 +- .../crystal_map_name_sign_speed_bug1994.lua | 119 ++- tests/engine/evolution_hold_b_bug968_test.lua | 4 +- tests/engine/evolution_timing_bug2033.lua | 274 +++++ tests/engine/faithful_res_nx.lua | 104 ++ tests/engine/fanfare_music_hold.lua | 5 +- tests/engine/gen2_battle_text_bug2046.lua | 193 ++++ .../gen2_enemy_move_fail_text_bug1296.lua | 8 +- .../gen2_evolution_cry_order_bug2033.lua | 144 +++ .../gen2_fly_bird_offscreen_bug2015.lua | 4 + tests/engine/gen2_fly_hold_frames_bug2051.lua | 87 ++ .../gen2_main_menu_translation_test.lua | 25 +- tests/engine/gen2_menu_whites_s6.lua | 210 ++++ .../gen2_pack_switch_cursor_bug2042.lua | 100 ++ tests/engine/gen2_save_order_bug2053.lua | 136 +++ .../engine/gen2_softboiled_field_bug2041.lua | 164 +++ tests/engine/gen2_text_terminator_bug2045.lua | 190 ++++ .../gen2_trainer_ball_lifetime_bug2021.lua | 112 +- .../engine/gen2_trainer_ball_tile_bug2044.lua | 202 ++++ .../gen2_trainer_battle_pal_bug1970.lua | 8 +- tests/engine/gen2_treemon_extract_bug2043.lua | 154 +++ tests/engine/gen2_typer_bug2046.lua | 210 ++++ .../engine/gen2_waitbutton_arrow_bug2045.lua | 123 +++ tests/engine/kit_spinner_trail_bug2048.lua | 48 + tests/engine/launcher_cart_ring_bug2003.lua | 19 +- .../launcher_skins_options_reads_bug2038.lua | 97 ++ tests/engine/launcher_sync_modal.lua | 25 + tests/engine/launcher_update_all.lua | 70 ++ .../engine/oaks_aide_requirement_bug1006.lua | 74 +- tests/engine/prize_full_party_bug2034.lua | 211 ++++ tests/engine/rare_candy_bag_open_bug796.lua | 13 +- .../shader_no_default_precision_test.lua | 48 + tests/engine/shaderfx_activate_compiles.lua | 140 +++ tests/engine/shaderfx_es_dialect.lua | 88 ++ tests/engine/shaderfx_no_bridge_ui.lua | 24 + tests/engine/sync_cart_saves_test.lua | 413 ++++++++ tests/gen2_audio_test.lua | 21 + tests/gen2_battle_items_test.lua | 2 +- tests/gen2_battle_test.lua | 4 +- tests/gen2_battle_ui_test.lua | 393 ++++++- tests/gen2_breeding_test.lua | 9 + tests/gen2_catch_tutorial_test.lua | 4 + tests/gen2_crystal_anim_test.lua | 71 ++ tests/gen2_fly_map_test.lua | 42 +- tests/gen2_halloffame_test.lua | 50 + tests/gen2_menus_test.lua | 47 +- tests/gen2_pc_screens_test.lua | 9 + tests/gen2_rom_text_test.lua | 15 +- tests/gen2_screen_layout_test.lua | 113 ++ tests/gen2_trade_gfx_test.lua | 40 + tests/gen2_unown_test.lua | 3 +- tests/gen2_version_fixes_test.lua | 3 +- tests/gen2_vm_test.lua | 15 + tests/gen2_world_test.lua | 70 ++ tests/modkit/cases/gen2_text_terminator.lua | 44 + tests/parity_E.lua | 43 +- tests/parity_J.lua | 17 +- tests/parity_fighting_dojo_gift.lua | 148 +++ tests/parity_oaks_aide_repeat.lua | 6 +- tests/parity_true_color_ui.lua | 2 + tests/rom_importer_choose_version_test.lua | 61 +- tools/crystal_symbol_deltas.py | 2 + tools/rom_manifest_crystal.json | 20 +- 207 files changed, 15247 insertions(+), 1151 deletions(-) create mode 100644 src/render/MonAnimView.lua create mode 100644 src/ui/gen2/MenuFade.lua create mode 100644 src/ui/gen2/Typer.lua create mode 100644 tests/data/shaderfx/es/bevel.lua create mode 100644 tests/data/shaderfx/es/gameboy.lua create mode 100644 tests/data/shaderfx/es/gb-palette-dmg.lua create mode 100644 tests/data/shaderfx/es/lcd3x.lua create mode 100644 tests/data/shaderfx/es/pixel_transparency.lua create mode 100644 tests/data/shaderfx/es/zfast-lcd.lua create mode 100644 tests/data/shaderfx/gl/bevel.lua create mode 100644 tests/data/shaderfx/gl/gameboy.lua create mode 100644 tests/data/shaderfx/gl/gb-palette-dmg.lua create mode 100644 tests/data/shaderfx/gl/lcd3x.lua create mode 100644 tests/data/shaderfx/gl/pixel_transparency.lua create mode 100644 tests/data/shaderfx/gl/zfast-lcd.lua create mode 100644 tests/drivers/battle_substitute_bug2030_test.lua create mode 100644 tests/drivers/battle_transform_bug2030_test.lua create mode 100644 tests/drivers/crystal_battle_text.lua create mode 100644 tests/drivers/crystal_evolution_anim_shots.lua create mode 100644 tests/drivers/crystal_fly_hold_bug2051_test.lua create mode 100644 tests/drivers/crystal_headbutt_probe.lua create mode 100644 tests/drivers/crystal_hp_bar_cadence.lua create mode 100644 tests/drivers/crystal_menu_whites_s6.lua create mode 100644 tests/drivers/crystal_music_worker_bug2002.lua create mode 100644 tests/drivers/crystal_rock_smash_menu_s5.lua create mode 100644 tests/drivers/crystal_sendout_pic.lua create mode 100644 tests/drivers/crystal_softboiled_field_bug2041.lua create mode 100644 tests/drivers/crystal_trade_anim_shots.lua create mode 100644 tests/drivers/crystal_trainer_arrow_bug2045.lua create mode 100644 tests/drivers/evolution_timing_bug2033.lua create mode 100644 tests/drivers/fossil_gift_bug2034_test.lua create mode 100644 tests/drivers/gen2_pc_save_overlay_bug2049.lua create mode 100644 tests/drivers/gen2_save_order_s5.lua create mode 100644 tests/drivers/gen2_text_speed_bug2046.lua create mode 100644 tests/drivers/launcher_skins_perf.lua create mode 100644 tests/drivers/launcher_spinner_shot.lua create mode 100644 tests/drivers/oaks_aide_jingle_2031.lua create mode 100644 tests/drivers/prize_full_party_bug2034_test.lua create mode 100644 tests/drivers/rare_candy_statbox_hold_test.lua create mode 100644 tests/drivers/shader_gles_validate.lua create mode 100644 tests/engine/battle_substitute_doll_timing_bug2030.lua create mode 100644 tests/engine/battle_transform_pic_timing_bug2030.lua create mode 100644 tests/engine/chip_audio_stats_test.lua create mode 100644 tests/engine/chip_mono_fastpath_test.lua create mode 100644 tests/engine/chip_sample_rate_test.lua create mode 100644 tests/engine/evolution_timing_bug2033.lua create mode 100644 tests/engine/faithful_res_nx.lua create mode 100644 tests/engine/gen2_battle_text_bug2046.lua create mode 100644 tests/engine/gen2_evolution_cry_order_bug2033.lua create mode 100644 tests/engine/gen2_fly_hold_frames_bug2051.lua create mode 100644 tests/engine/gen2_menu_whites_s6.lua create mode 100644 tests/engine/gen2_pack_switch_cursor_bug2042.lua create mode 100644 tests/engine/gen2_save_order_bug2053.lua create mode 100644 tests/engine/gen2_softboiled_field_bug2041.lua create mode 100644 tests/engine/gen2_text_terminator_bug2045.lua create mode 100644 tests/engine/gen2_trainer_ball_tile_bug2044.lua create mode 100644 tests/engine/gen2_treemon_extract_bug2043.lua create mode 100644 tests/engine/gen2_typer_bug2046.lua create mode 100644 tests/engine/gen2_waitbutton_arrow_bug2045.lua create mode 100644 tests/engine/kit_spinner_trail_bug2048.lua create mode 100644 tests/engine/launcher_skins_options_reads_bug2038.lua create mode 100644 tests/engine/prize_full_party_bug2034.lua create mode 100644 tests/engine/shader_no_default_precision_test.lua create mode 100644 tests/engine/shaderfx_activate_compiles.lua create mode 100644 tests/engine/shaderfx_es_dialect.lua create mode 100644 tests/engine/sync_cart_saves_test.lua create mode 100644 tests/modkit/cases/gen2_text_terminator.lua create mode 100644 tests/parity_fighting_dojo_gift.lua diff --git a/data/scripts/story2.lua b/data/scripts/story2.lua index a28ae8a9..db05c9dd 100644 --- a/data/scripts/story2.lua +++ b/data/scripts/story2.lua @@ -875,25 +875,28 @@ M.CINNABAR_LAB_FOSSIL_ROOM = { "Where were you?\fYour fossil is\nback to life!\fIt was {RAM:x}\nlike I think!", subs), function() - if species then - local Commands = require("src.script.Commands") - local ctx = { save = game.save, game = game, overworld = ow } - Commands.give_pokemon(ctx, species, 30) - if not ctx.lastCheck then - -- GivePokemon failed (party+box full): pokered's - -- `jr nc, .done` leaves the quest pending so the - -- scientist re-offers the mon next visit instead of - -- destroying it. - game.stack:push(TextBox.new(game, - t._BoxIsFullText or "Box is full!", done)) - return - end + if not species then + game.save.labFossilMon = nil + f.EVENT_GAVE_FOSSIL_TO_LAB = nil + f.EVENT_LAB_STILL_REVIVING_FOSSIL = nil + f.EVENT_LAB_HANDING_OVER_FOSSIL_MON = nil + done() + return end - game.save.labFossilMon = nil - f.EVENT_GAVE_FOSSIL_TO_LAB = nil - f.EVENT_LAB_STILL_REVIVING_FOSSIL = nil - f.EVENT_LAB_HANDING_OVER_FOSSIL_MON = nil - done() + -- ../pokered/scripts/CinnabarLabFossilRoom.asm:74-83 + ow.runner:run({ + { "give_pokemon", species, 30, false, true }, + { "jump_if_false", "boxfull" }, + { "set_field", "labFossilMon" }, + { "clear_flag", "EVENT_GAVE_FOSSIL_TO_LAB" }, + { "clear_flag", "EVENT_LAB_STILL_REVIVING_FOSSIL" }, + { "clear_flag", "EVENT_LAB_HANDING_OVER_FOSSIL_MON" }, + { "jump", "out" }, + { "label", "boxfull" }, + -- ../pokered/engine/events/give_pokemon.asm:40-42 + { "show_text", "_BoxIsFullText" }, + { "label", "out" }, + }, { onDone = done }) end)) return end diff --git a/data/scripts/story3.lua b/data/scripts/story3.lua index 311126d3..10444588 100644 --- a/data/scripts/story3.lua +++ b/data/scripts/story3.lua @@ -803,7 +803,6 @@ end local function prizeCounter(window) return function(game, ow, npc, done) local PrizeCounter = require("src.ui.PrizeCounter") - local Commands = require("src.script.Commands") local TextBox = require("src.render.TextBox") local t = game.data.text -- IsItemInBag COIN_CASE: without the case, deny and open no window @@ -826,9 +825,10 @@ local function prizeCounter(window) prize = p, } end + local whichBox, menu local function close() - game.stack:pop() - game.stack:pop() + local st = game.stack + while st:top() == menu or st:top() == whichBox do st:pop() end end local function finish(msg) close() @@ -842,20 +842,26 @@ local function prizeCounter(window) -- HasEnoughCoins passed, so hand the prize over first and only -- subtract once it landed: the asm rets before .subtractCoins when -- the bag is full, or when both the party and every box are full - local roomless = t._OopsYouDontHaveEnoughRoomText - or "Oops! You don't\nhave enough room." if p.kind == "mon" then - -- no runner here, so give_pokemon reports through ctx.lastCheck - -- and skips the AskName prompt (Commands.give_pokemon) - local ctx = { save = game.save, game = game } - Commands.give_pokemon(ctx, p.species, p.level) - if not ctx.lastCheck then - finish(roomless) - return - end - elseif not require("src.inventory.Bag").add( + -- engine/events/prize_menu.asm:217-243 + close() + ow.runner:run({ + { "give_pokemon", p.species, p.level, false, true }, + { "jump_if_false", "boxfull" }, + { "take_coins", p.cost }, + { "jump", "out" }, + { "label", "boxfull" }, + -- engine/events/give_pokemon.asm:40-42 + { "show_text", "_BoxIsFullText" }, + { "label", "out" }, + }, { onDone = done }) + return + end + if not require("src.inventory.Bag").add( game.save, p.item, 1, game.data) then - finish(roomless) + -- engine/events/prize_menu.asm:245-246 + finish(t._OopsYouDontHaveEnoughRoomText + or "Oops! You don't\nhave enough room.") return end game.save.coins = game.save.coins - p.cost @@ -864,11 +870,11 @@ local function prizeCounter(window) close() done() end - game.stack:push(TextBox.new(game, + whichBox = TextBox.new(game, t._WhichPrizeText or "Which prize do\nyou want?", nil, { instant = true, stay = { onShown = function() - game.stack:push(PrizeCounter.new(game, rows, { + menu = PrizeCounter.new(game, rows, { onCancel = function() close() done() @@ -889,9 +895,11 @@ local function prizeCounter(window) end, })) end, - })) + }) + game.stack:push(menu) end }, - })) + }) + game.stack:push(whichBox) end, { instant = true })) end end diff --git a/data/scripts/story4.lua b/data/scripts/story4.lua index 75435f85..7ab92ea3 100644 --- a/data/scripts/story4.lua +++ b/data/scripts/story4.lua @@ -51,15 +51,20 @@ local function countOwned(save) return n end -local function oaksAide(threshold, itemId, repeatText) +local function oaksAide(threshold, itemId, repeatText, flagName) return function(game, ow, npc, done) local t = text(game) local flags = game.save.flags local itemName = game.data.items[itemId].name - local flag = "EVENT_GOT_" .. itemId - if flags[flag] then + local flag = flagName or ("EVENT_GOT_" .. itemId) + local portFlag = "EVENT_GOT_" .. itemId + -- ../pokered/scripts/Route2Gate.asm:28-30 + local function explain() push(game, t[repeatText] or fill("I already gave\nyou the {RAM:}!", { ram = itemName }), done) + end + if flags[flag] or flags[portFlag] then + explain() return end ask(game, fill(t._OaksAideHiText or @@ -73,18 +78,23 @@ local function oaksAide(threshold, itemId, repeatText) end local owned = countOwned(game.save) if owned >= threshold then - if not require("src.inventory.Bag").add(game.save, itemId, 1) then - push(game, fill(t._OaksAideNoRoomText or - "No room for the\n{RAM:}!", { ram = itemName }), done) - return - end - flags[flag] = true + -- ../pokered/engine/events/oaks_aide.asm:18-29 push(game, fill(t._OaksAideHereYouGoText or "Here you go!", { num = owned, ram = itemName }), function() + if not require("src.inventory.Bag").add( + game.save, itemId, 1, game.data) then + push(game, fill(t._OaksAideNoRoomText or + "No room for the\n{RAM:}!", { ram = itemName }), done) + return + end + flags[flag] = true + -- ../pokered/engine/events/oaks_aide.asm:64-67 push(game, fill(t._OaksAideGotItemText or - "{PLAYER} got the\n{RAM:}!", - { ram = itemName, player = game.save.player.name }), done) + "{PLAYER} got the\n{RAM:}!", + { ram = itemName, player = game.save.player.name }), + explain, + require("src.render.TextBox").soundOpts(game, "Get_Item1")) end) else -- .notEnoughOwnedMons prints owned then requirement, two counts @@ -100,7 +110,7 @@ end M.ROUTE_2_GATE = { talk = { TEXT_ROUTE2GATE_OAKS_AIDE = oaksAide(10, "HM_FLASH", - "_Route2GateOaksAideFlashExplanationText") }, + "_Route2GateOaksAideFlashExplanationText", "EVENT_GOT_HM05") }, } M.ROUTE_11_GATE_2F = { talk = { TEXT_ROUTE11GATE2F_OAKS_AIDE = oaksAide(30, "ITEMFINDER", @@ -185,14 +195,20 @@ local function dojoBall(species, ownBall, otherBall, askKey) game.stack:push(DexEntryMenu.new(game, species, function() ask(game, t[askKey] or ("You want\n" .. species .. "?"), function(yes) if not yes then done() return end - flags["EVENT_GOT_" .. species] = true - flags.EVENT_DEFEATED_FIGHTING_DOJO = true - Commands.give_pokemon(ctx, species, 30) - -- Hide ONLY the chosen ball; the other stays (FightingDojo.asm hides - -- just the picked object's index) and routes to the greedy line above - -- when talked to (#197). - Commands.hide_object(ctx, "FIGHTING_DOJO", ownBall) - push(game, ("%s got\n%s!"):format(game.save.player.name, species), done) + -- ../pokered/scripts/FightingDojo.asm:238-252 + ow.runner:run({ + { "give_pokemon", species, 30, false, true }, + { "jump_if_false", "boxfull" }, + -- ../pokered/scripts/FightingDojo.asm:248-251 + { "hide_object", "FIGHTING_DOJO", ownBall }, + { "set_flag", "EVENT_GOT_" .. species }, + { "set_flag", "EVENT_DEFEATED_FIGHTING_DOJO" }, + { "jump", "out" }, + { "label", "boxfull" }, + -- ../pokered/engine/events/give_pokemon.asm:40-42 + { "show_text", "_BoxIsFullText" }, + { "label", "out" }, + }, { onDone = done }) end) end)) end @@ -332,19 +348,26 @@ M.MR_PSYCHICS_HOUSE = { talk = { TEXT_MRPSYCHICSHOUSE_MR_PSYCHIC = function(game, ow, npc, done) local t = text(game) + local tmName = (game.data.items.TM_PSYCHIC_M or {}).name or "TM29" if game.save.flags.EVENT_GOT_TM29 then - push(game, t._MrPsychicsHouseMrPsychicNoMoreText - or "...Hmm...", done) + push(game, t._MrPsychicsHouseMrPsychicTM29ExplanationText + or "TM29 is PSYCHIC!", done) return end - push(game, t._MrPsychicsHouseMrPsychicText - or "...Wait!\nDon't say a word!\fYou came to get\nTM29!", function() - if not require("src.inventory.Bag").add(game.save, "TM_PSYCHIC_M", 1) then - push(game, "You don't have\nroom for TM29!", done) + push(game, t._MrPsychicsHouseMrPsychicYouWantedThisText + or "...Wait! Don't\nsay a word!\fYou wanted this!", function() + if not require("src.inventory.Bag").add( + game.save, "TM_PSYCHIC_M", 1, game.data) then + push(game, t._MrPsychicsHouseMrPsychicTM29NoRoomText + or "Where do you plan\nto put this?", done) return end game.save.flags.EVENT_GOT_TM29 = true - push(game, ("%s got\nTM29!"):format(game.save.player.name), done) + -- ../pokered/scripts/MrPsychicsHouse.asm:35-38 + push(game, fill(t._MrPsychicsHouseMrPsychicReceivedTM29Text + or "{PLAYER} received\n{RAM:}!", + { player = game.save.player.name, ram = tmName }), + done, require("src.render.TextBox").soundOpts(game, "Get_Item1")) end) end, }, @@ -514,10 +537,12 @@ M.CELADON_MART_ROOF = { function(yes) if not yes then done() return end local Menu = require("src.ui.Menu") + local TextBox = require("src.render.TextBox") + local whichBox, menu -- scripts/CeladonMartRoof.asm:242 local function closeAll() - game.stack:pop() - game.stack:pop() + local st = game.stack + while st:top() == menu or st:top() == whichBox do st:pop() end done() end local function give(g) @@ -559,18 +584,21 @@ M.CELADON_MART_ROOF = { }) end -- scripts/CeladonMartRoof.asm:45 - push(game, t._CeladonMartRoofLittleGirlGiveHerWhichDrinkText + whichBox = TextBox.new(game, + t._CeladonMartRoofLittleGirlGiveHerWhichDrinkText or "Give her which\ndrink?", nil, { instant = true, stay = { onShown = function() -- scripts/CeladonMartRoof.asm:56 - game.stack:push(Menu.new(game, items, { + menu = Menu.new(game, items, { tx = 0, ty = 0, tw = 14, th = #have * 2 + 2, itemY = 2, noWrap = true, - onCancel = function() game.stack:pop() done() end, - })) + onCancel = closeAll, + }) + game.stack:push(menu) end }, }) + game.stack:push(whichBox) end) end, }, diff --git a/docs/linux-arm-sbc.md b/docs/linux-arm-sbc.md index d4499aa4..e37bcb73 100644 --- a/docs/linux-arm-sbc.md +++ b/docs/linux-arm-sbc.md @@ -40,7 +40,7 @@ The handheld build applies several optimizations to reduce power draw while keep - **PresentSync / driver vsync**: Handheld builds keep driver vsync enabled so the unified present-sync stack can probe cadence and pace through the panel (KMSDRM page-flip wait). Disabling vsync here bypassed that path and forced the FrameCap 1 ms polling loop at 60 FPS. - **Idle Render Governor**: After `POKEPORT_IDLE_AFTER` seconds without input on static in-game screens, presentation drops to `POKEPORT_IDLE_FPS` while game logic and audio stay at full speed. Any button press restores full framerate on the next frame. -- **Sample Rate Scaling**: Audio synthesis is tuned to 22.05 kHz by default (`POKEPORT_AUDIO_RATE=22050`), halving synthesis CPU overhead on Cortex-A53 cores with no audible quality loss on handheld speakers. +- **Sample Rate Scaling**: Audio synthesis is tuned to 22.05 kHz by default (`POKEPORT_AUDIO_RATE=22050`), halving synthesis CPU overhead on Cortex-A53 cores with no audible quality loss on handheld speakers. The synth engine and its queueable source are built for one rate, so changing PERFORMANCE across the LOW boundary in OPTIONS switches the rate live and replays the current song from the start of its section; the rate never changes on its own during play. - **Dynamic CPU Governor**: Defaults to `schedutil` instead of pinning `performance` on all cores, reducing thermals and extending battery life. Environment variables for fine-tuning (configured in `gen1recomp-sbc.sh`): diff --git a/docs/shaderfx.md b/docs/shaderfx.md index d9ebda96..111ab352 100644 --- a/docs/shaderfx.md +++ b/docs/shaderfx.md @@ -408,12 +408,32 @@ identifier" and "unknown swizzle selection" errors on real Android hardware came from. **Fragment entry point.** `void main()` becomes LOVE's `effect()` signature. -The parameter list is qualified by an `EFFECT_PREC` define rather than a literal -precision, because LOVE forward-declares `effect()`'s prototype under its own -header's precision default before this source runs, which can mismatch whatever -the precision guard above raises the default to. `Fixup.PREC_HEADS` holds the -two variants (`mediump`, then unqualified) and the caller tries them in order -against `validateShader`, taking the first that passes. +The parameter list *and the return type* are qualified by an `EFFECT_PREC` +define rather than a literal precision, because LOVE forward-declares +`effect()`'s prototype under its own header's precision default (`mediump` on +ES) before this source runs, which can mismatch whatever the precision guard +above raises the default to. glslang only checks the parameters, but GLSL ES +3.00 ยง6.1 lets a driver reject a definition whose return precision differs from +the prototype's too, so both carry the same head. `Fixup.PREC_HEADS` holds the +two variants (`mediump`, then unqualified) and `ShaderFX.activate` tries them in +order against `validateShader`, taking the first that passes. Every pass is +compiled at activate time; a preset whose GLSL the device rejects fails +`activate` (the picker row reads FAILED, the option is not persisted) and the +error is kept in `ShaderFX.lastError()` and written to `shaderfx-error.log` in +the save directory, next to `lua-error.log`. `validateShader` runs the same +glslang the Android/iOS builds do, so `tests/drivers/shader_gles_validate.lua` +runs every fixture artifact under `tests/data/shaderfx/{es,gl}` and every +inline shader in `src/` through it in both dialects and both +`getSupported().glsl3` states; that is the gate any ES-facing GLSL change has to +pass on a desktop. + +**GL 1.20 gaps.** For the desktop dialect the bridge declares the UBO's +`FrameCount` as `uint` and appends `#extension GL_EXT_gpu_shader4 : require` +after the flattened uniforms. Under LOVE's `#version 330 core` glsl1on3 header +both stages fail (`uint` is a syntax error, then the extension is unsupported), +which silently took bevel, lcd3x, zfast-lcd, the gb-palette trio and +gameboy-dark-mode's last pass out on macOS. `rewriteGpuShader4` strips the +extension line and rewrites the `uint` token to `int` in both stages. **Fragment output.** `gl_FragData` does not exist in LOVE's `effect()` convention, so every `gl_FragData[0]` occurrence is rewritten to a local diff --git a/src/battle/BattleState.lua b/src/battle/BattleState.lua index aa00a460..ae9ee847 100644 --- a/src/battle/BattleState.lua +++ b/src/battle/BattleState.lua @@ -551,14 +551,16 @@ end local StatBox = {} StatBox.__index = StatBox -function StatBox.new(game, mon, onDone) - return setmetatable({ game = game, mon = mon, onDone = onDone }, StatBox) +-- ../pokered/engine/items/item_effects.asm:1403-1411 +function StatBox.new(game, mon, onDone, keepOpen) + return setmetatable({ game = game, mon = mon, onDone = onDone, + keepOpen = keepOpen or nil }, StatBox) end function StatBox:update() local input = self.game.input if input:wasPressed("a") or input:wasPressed("b") then - self.game.stack:pop() + if not self.keepOpen then self.game.stack:pop() end if self.onDone then self.onDone() end end end @@ -1090,6 +1092,17 @@ function BattleState:animBeforeMove(name, isPlayer) hit = { animType = isPlayer and 6 or 3 } }) end +-- ../pokered/engine/battle/move_effects/substitute.asm:2-3 +function BattleState:waitBeforeMoveAnim(frames) + if not frames or frames <= 0 then return end + local at + for i, item in ipairs(self.queue) do + if item == self.moveAnimRow then at = i break end + end + self.nextInsert = (self.nextInsert or 0) + 1 + table.insert(self.queue, at or self.nextInsert, { wait = frames }) +end + -- insert an act right after the current queue item function BattleState:actNext(fn) self.nextInsert = (self.nextInsert or 0) + 1 @@ -3399,7 +3412,6 @@ function BattleState:applyAnimEffect(ev) if pf then pf.kind, pf.hidden, pf.minimized = nil, nil, nil end elseif e == "SE_TRANSFORM_MON" then -- AnimationTransformMon redraws the user as the opposing species - -- (MoveEffects.TRANSFORM_EFFECT swaps the rest when it applies) local user = self:animFxBattler(false) local target = self:animFxBattler(true) if user and target and self.speciesSprite then @@ -3408,9 +3420,15 @@ function BattleState:applyAnimEffect(ev) local pf = self:picFxFor(user) if pf then pf.minimized = nil end end + elseif e == "SE_SUBSTITUTE_MON" then + -- ../pokered/engine/battle/animations.asm:1936-1973 + local b = self:animFxBattler(false) + if b then + b.substitutePending = nil + local pf = self:picFxFor(b) + if pf then pf.kind, pf.hidden, pf.ox, pf.oy = nil, nil, 0, 0 end + end end - -- SE_SUBSTITUTE_MON needs no visual here: the doll is drawn while - -- battler.substituteHP is set (MoveEffects raises it with the move) end -- engine/battle/animations.asm:2639 PlayApplyingAttackSound @@ -4392,6 +4410,7 @@ end -- amount that counts as dealt (for recoil/drain). function BattleState:applyDamage(target, dmg) if target.substituteHP then + target.substitutePending = nil target.substituteHP = target.substituteHP - dmg if target.substituteHP <= 0 then target.substituteHP = nil @@ -5876,7 +5895,7 @@ end function BattleState:faintPicKind(battler) local pf = self.picFx and self.picFx[battler] if pf and pf.minimized then return "blob" end - if battler.substituteHP then return "doll" end + if battler.substituteHP and not battler.substitutePending then return "doll" end return "pic" end @@ -5890,7 +5909,8 @@ end function BattleState:drawBattlerPic(battler, x, y, scale, shakeX, shakeY) local img = self:picImage(battler.sprite) shakeX, shakeY = shakeX or 0, shakeY or 0 - if battler.substituteHP and not self:fxFaintActive(battler) + if battler.substituteHP and not battler.substitutePending + and not self:fxFaintActive(battler) and not battler.fainted then self:drawSubstituteDoll(battler, shakeX, shakeY) return diff --git a/src/battle/MoveEffects.lua b/src/battle/MoveEffects.lua index d6abf537..1a8b25d4 100644 --- a/src/battle/MoveEffects.lua +++ b/src/battle/MoveEffects.lua @@ -16,6 +16,7 @@ local Logger = require("src.core.Logger") local StatusRegistry = require("src.battle.StatusRegistry") local TurnOrder = require("src.battle.TurnOrder") local TypeChart = require("src.battle.TypeChart") +local Timing = require("src.core.Timing") local romText = require("src.core.RomText") local Strings = require("src.core.Strings") @@ -287,6 +288,10 @@ MoveEffects.primary = { -- battle (#644). The failed flag rides the message list so performMove can -- peel the announcement-time anim row without matching on printed text. SUBSTITUTE_EFFECT = function(battle, user) + -- ../pokered/engine/battle/move_effects/substitute.asm:2-3 + if battle.waitBeforeMoveAnim then + battle:waitBeforeMoveAnim(Timing.SUBSTITUTE_ENTRY) + end if user.substituteHP then return { romText(battle.data, "_HasSubstituteText", "%s\nhas a SUBSTITUTE!", displayName(user)), failed = true } @@ -302,6 +307,15 @@ MoveEffects.primary = { end user.mon.hp = user.mon.hp - cost user.substituteHP = cost + 1 + -- ../pokered/engine/battle/move_effects/substitute.asm:47-55 + user.substitutePending = true + if battle.animationsOn and battle.cancelMoveAnim + and not battle:animationsOn() then + battle:cancelMoveAnim() + end + if battle.actNext then + battle:actNext(function() user.substitutePending = nil end) + end -- _SubstituteText return { romText(battle.data, "_SubstituteText", "It created a\nSUBSTITUTE!") } end, @@ -322,12 +336,18 @@ MoveEffects.primary = { -- returned strings can't express. TRANSFORM_EFFECT = function(battle, user, target) - -- transform.asm:31-53 (AnimationTransformMon) morphs the user's - -- on-screen pic into the target species; the port swaps user.sprite - -- via the same getImage/monPalette path makeBattler uses so the - -- change is visible (the renderer draws battler.sprite directly). - user.sprite = battle:speciesSprite(target.mon.species, user.isPlayer) - or user.sprite + -- transform.asm:37-45 + local pic = battle:speciesSprite(target.mon.species, user.isPlayer) + if battle.animationsOn and battle.cancelMoveAnim and not battle:animationsOn() then + battle:cancelMoveAnim() + end + if pic and battle.actNext then + battle:actNext(function() + user.sprite = pic + local pf = battle.picFxFor and battle:picFxFor(user) + if pf then pf.minimized = nil end + end) + end user.curStats = { hp = user.mon.stats.hp, -- HP is kept attack = target.curStats.attack, defense = target.curStats.defense, diff --git a/src/battle/gen2/Battle.lua b/src/battle/gen2/Battle.lua index ab57aac4..963a9588 100644 --- a/src/battle/gen2/Battle.lua +++ b/src/battle/gen2/Battle.lua @@ -172,6 +172,39 @@ Battle.BATTLETYPE_SUICUNE = 12 -- LostBattle's .canlose arm (engine/battle/core.asm:2766): the only battle -- type whose loss still prints the trainer's own line instead of a whiteout. Battle.BATTLETYPE_CANLOSE = 1 +-- ../pokecrystal/constants/battle_constants.asm:91-103 +Battle.BATTLETYPE_NORMAL = 0 +Battle.BATTLETYPE_DEBUG = 2 +Battle.BATTLETYPE_TUTORIAL = 3 +Battle.BATTLETYPE_FISH = 4 +Battle.BATTLETYPE_ROAMING = 5 +Battle.BATTLETYPE_CONTEST = 6 +Battle.BATTLETYPE_TREE = 8 +Battle.BATTLETYPE_FORCEITEM = 10 + +local BATTLETYPE_NAMES = { + normal = Battle.BATTLETYPE_NORMAL, + canlose = Battle.BATTLETYPE_CANLOSE, + debug = Battle.BATTLETYPE_DEBUG, + tutorial = Battle.BATTLETYPE_TUTORIAL, + fish = Battle.BATTLETYPE_FISH, + roaming = Battle.BATTLETYPE_ROAMING, + contest = Battle.BATTLETYPE_CONTEST, + forceshiny = Battle.BATTLETYPE_FORCESHINY, + tree = Battle.BATTLETYPE_TREE, + trap = Battle.BATTLETYPE_TRAP, + forceitem = Battle.BATTLETYPE_FORCEITEM, + celebi = Battle.BATTLETYPE_CELEBI, + suicune = Battle.BATTLETYPE_SUICUNE, +} + +function Battle.battleTypeId(value) + if value == nil then return nil end + if type(value) == "string" then + return BATTLETYPE_NAMES[value:lower()] or tonumber(value) + end + return value +end -- BadgeStatBoosts (engine/battle/core.asm:6534): each of these Johto badges -- raises the PLAYER's in-battle stat by 1/8. The routine walks every other @@ -249,7 +282,7 @@ function Battle.new(opts) -- wBattleType, when the caller knows it: "fish" gates the Lure Ball's x3 -- (BATTLETYPE_FISH is the one condition LureBallMultiplier reads), and the -- FORCESHINY / TRAP no-escape rules will hang off the same field. - self.battleType = opts.battleType + self.battleType = Battle.battleTypeId(opts.battleType) -- wInBattleTowerBattle (../pokecrystal/constants/ram_constants.asm:38), set -- around the Tower's own StartBattle (engine/events/battle_tower/ -- battle_tower.asm:220-223) and cleared again at :253-254. @@ -2364,8 +2397,6 @@ Battle.MOVE_EFFECTS.EFFECT_TRANSFORM = function(self, attacker, defender) stats[key] = theirs[key] or stats[key] end attacker.stats = stats - self:emit({ kind = "message", text = self:monName(attacker) - .. " TRANSFORMED into " .. (defender.species or "?") .. "!" }) -- The moment itself, for the screen. src/ui/gen2/BattleState.lua draws each -- side's pic and HUD from `shownMon`, which follows the EVENT QUEUE rather -- than the battle -- a whole round is resolved by Battle:takeTurn before its @@ -2375,9 +2406,12 @@ Battle.MOVE_EFFECTS.EFFECT_TRANSFORM = function(self, attacker, defender) -- `shownHp` and a switch has the `send` event for exactly this; a transform -- is the third identity swap and this is its event. `mon` is the battler -- whose pic changes (the same key `send` carries) and `from` is what it was. + -- ../pokecrystal/engine/battle/move_effects/transform.asm:118-136 self:emit({ kind = "transform", side = self:sideOf(attacker), mon = attacker, species = attacker.species, from = state.preTransform.species }) + self:emit({ kind = "message", text = self:monName(attacker) + .. " TRANSFORMED into " .. (defender.species or "?") .. "!" }) end -- SwitchOutMon / PokeBallEffect's reload: the copy Transform wrote lives in diff --git a/src/battle/gen2/Encounter.lua b/src/battle/gen2/Encounter.lua index fdfca97c..cc23f905 100644 --- a/src/battle/gen2/Encounter.lua +++ b/src/battle/gen2/Encounter.lua @@ -166,20 +166,52 @@ function Encounter.treeSet(encounters, mapId) return encounters and encounters.trees and encounters.trees[mapId] or nil end --- A headbutt on the tree at (cx, cy). Whether the COMMON or the RARE list is --- rolled comes from the tree's own coordinates on the cart -- GetTreeMons --- hashes them so the same tree always behaves the same way -- which is what --- keeps a player from re-rolling one tree for a rare mon. -function Encounter.treeIsRare(cx, cy) - return ((cx or 0) * 5 + (cy or 0) * 7) % 10 < 1 +-- ../pokecrystal/engine/events/treemons.asm:199 GetTreeScore +Encounter.TREEMON_SCORE_BAD = 0 +Encounter.TREEMON_SCORE_GOOD = 1 +Encounter.TREEMON_SCORE_RARE = 2 + +-- ../pokecrystal/engine/overworld/player_object.asm:102 RefreshPlayerCoords +function Encounter.treeScore(cx, cy, otId) + local d = ((cx or 0) + 4) % 256 + local e = ((cy or 0) + 4) % 256 + local coord = math.floor(((e * (d + 1) + d) % 65536) / 5) % 10 + local ot = math.floor(otId or 0) % 10 + local diff = (coord - ot) % 10 + if diff == 0 then return Encounter.TREEMON_SCORE_RARE end + if diff < 5 then return Encounter.TREEMON_SCORE_GOOD end + return Encounter.TREEMON_SCORE_BAD end -function Encounter.treeSlot(encounters, mapId, cx, cy, random) +-- ../pokegold/engine/events/treemons.asm:94 GetTreeMons +local GS_DEAD_SETS = { + TREEMON_SET_NONE = true, + TREEMON_SET_UNUSED = true, + TREEMON_SET_CITY = true, +} + +-- ../pokecrystal/engine/events/treemons.asm:96 GetTreeMons +function Encounter.treeSetUsable(setName, engine) + if not setName or setName == "TREEMON_SET_NONE" then return false end + if engine == "gs" and GS_DEAD_SETS[setName] then return false end + return true +end + +-- ../pokecrystal/engine/events/treemons.asm:126 GetTreeMon +function Encounter.treeSlot(encounters, mapId, cx, cy, random, opts) + opts = opts or {} local setName = Encounter.treeSet(encounters, mapId) - if not setName then return nil end + if not Encounter.treeSetUsable(setName, opts.engine) then return nil end local set = encounters and encounters.treeSets and encounters.treeSets[setName] if not set then return nil end - local list = Encounter.treeIsRare(cx, cy) and set.rare or set.common + local score = Encounter.treeScore(cx, cy, opts.otId) + local list, gate = set.common, 1 + if score == Encounter.TREEMON_SCORE_GOOD then + gate = 5 + elseif score == Encounter.TREEMON_SCORE_RARE then + list, gate = set.rare, 8 + end + if roll(random, 10) >= gate then return nil end if not list or #list == 0 then return nil end local value = roll(random, 100) local total = 0 @@ -193,4 +225,35 @@ function Encounter.treeSlot(encounters, mapId, cx, cy, random) return nil end +-- ../pokecrystal/data/wild/treemons_asleep.asm:3 AsleepTreeMonsNite / Day / Morn +Encounter.ASLEEP_TREEMONS = { + NITE = { CATERPIE = true, METAPOD = true, BUTTERFREE = true, WEEDLE = true, + KAKUNA = true, BEEDRILL = true, SPEAROW = true, EKANS = true, + EXEGGCUTE = true, LEDYBA = true, AIPOM = true }, + DAY = { VENONAT = true, HOOTHOOT = true, NOCTOWL = true, SPINARAK = true, + HERACROSS = true }, +} +Encounter.ASLEEP_TREEMONS.MORN = Encounter.ASLEEP_TREEMONS.DAY +Encounter.ASLEEP_TREEMONS.DARK = Encounter.ASLEEP_TREEMONS.NITE + +-- ../pokecrystal/constants/battle_constants.asm:15 TREEMON_SLEEP_TURNS +Encounter.TREEMON_SLEEP_TURNS = 7 + +-- ../pokecrystal/engine/battle/core.asm:6422 CheckSleepingTreeMon +function Encounter.treeMonAsleep(species, daytime, engine, encounters) + if engine ~= "crystal" then return false end + daytime = daytime or "DAY" + local extracted = encounters and encounters.treeMonsAsleep + if type(extracted) == "table" then + local key = (daytime == "MORN" and "MORN") or (daytime == "DAY" and "DAY") + or "NITE" + for _, name in ipairs(extracted[key] or {}) do + if name == species then return true end + end + return false + end + local list = Encounter.ASLEEP_TREEMONS[daytime] + return (list and list[species]) == true +end + return Encounter diff --git a/src/battle/gen2/HpBar.lua b/src/battle/gen2/HpBar.lua index 20b7a67f..5c740125 100644 --- a/src/battle/gen2/HpBar.lua +++ b/src/battle/gen2/HpBar.lua @@ -78,10 +78,10 @@ end -- The cart builds it out of tiles -- $62 is an empty bar cell and $63..$6a are -- the eight partial fills, so the fill really does move one pixel at a time -- inside a fixed 48px frame, and the *unfilled* part is white, not tinted. -function HpBar.draw(palettes, hp, maxHp, px, py) +function HpBar.draw(palettes, hp, maxHp, px, py, pixels) local G = love and love.graphics if not G then return end - local pixels = HpBar.pixels(hp, maxHp) + pixels = pixels or HpBar.pixels(hp, maxHp) local _, fill = HpBar.colors(palettes, HpBar.palette(pixels)) -- Frame: one pixel of black around the 48x2 channel the fill lives in. G.setColor(0, 0, 0, 1) @@ -106,13 +106,13 @@ end -- -- Returns the tile column just past the bar, so a caller can put the bar's end -- cap or the frame stub there. -function HpBar.drawWithLabel(palettes, hp, maxHp, tx, ty, font) +function HpBar.drawWithLabel(palettes, hp, maxHp, tx, ty, font, pixels) if font then love.graphics.setColor(0, 0, 0, 1) font.draw("HP:", tx * 8, ty * 8) end -- The bar's channel sits in the middle of the tile row, matching the tiles. - HpBar.draw(palettes, hp, maxHp, (tx + 2) * 8, ty * 8 + 2) + HpBar.draw(palettes, hp, maxHp, (tx + 2) * 8, ty * 8 + 2, pixels) return tx + 2 + HpBar.LENGTH_TILES end @@ -153,6 +153,55 @@ function HpBar.stepToward(shown, target, maxHp) return math.max(target, shown - step) end +-- ../pokecrystal/engine/battle/anim_hp_bar.asm:359 +function HpBar.shortFrameHp(pixels, maxHp, lowHp, highHp) + if pixels >= HpBar.LENGTH_PX then return maxHp end + if pixels <= 0 then return 0 end + local hp = math.floor(maxHp * pixels / HpBar.LENGTH_PX) + 1 + if lowHp >= hp then return lowHp end + if highHp < hp then return highHp end + return hp +end + +-- ../pokecrystal/engine/battle/anim_hp_bar.asm:56 +function HpBar.newAnim(from, to, maxHp) + from, to, maxHp = from or 0, to or 0, maxHp or 0 + return { + hp = from, to = to, maxHp = maxHp, + px = HpBar.pixels(from, maxHp), toPx = HpBar.pixels(to, maxHp), + dir = (to < from) and -1 or 1, + long = maxHp >= HpBar.LENGTH_PX, + low = math.min(from, to), high = math.max(from, to), + } +end + +-- ../pokecrystal/engine/battle/anim_hp_bar.asm:129 +local function shortStep(anim) + if anim.px == anim.toPx then return true end + anim.px = anim.px + anim.dir + anim.hp = HpBar.shortFrameHp(anim.px, anim.maxHp, anim.low, anim.high) + return false +end + +-- ../pokecrystal/engine/battle/anim_hp_bar.asm:145 +local function longStep(anim) + while true do + if anim.hp == anim.to then return true end + anim.hp = anim.hp + anim.dir + local px = HpBar.pixels(anim.hp, anim.maxHp) + if px ~= anim.px then + anim.px = px + return false + end + end +end + +-- ../pokecrystal/engine/battle/anim_hp_bar.asm:1 +function HpBar.animStep(anim) + if anim.long then return longStep(anim) end + return shortStep(anim) +end + -- How far along its current level a mon is, for the exp bar. function HpBar.expFraction(mon, growth, levelFor) if not (mon and growth and levelFor) then return 0 end diff --git a/src/core/ChipAudio.lua b/src/core/ChipAudio.lua index 4caaef17..2a7d47f5 100644 --- a/src/core/ChipAudio.lua +++ b/src/core/ChipAudio.lua @@ -21,7 +21,9 @@ local ChipSynth = require("src.core.ChipSynth") local ChipAudio = {} -local SAMPLE_RATE = ChipSynth.SAMPLE_RATE +local function sampleRate() + return ChipSynth.SAMPLE_RATE +end local MUSIC_BUFFER_SAMPLES = ChipSynth.MUSIC_BUFFER_SAMPLES local MUSIC_BUFFER_COUNT = ChipSynth.MUSIC_BUFFER_COUNT @@ -70,6 +72,10 @@ local suspended = false local STATS = os.getenv("POKEPORT_AUDIO_STATS") == "1" local statFrames, statUnderruns, statRestarts = 0, 0, 0 +local statDepthMin, statDepthSum, statDepthFrames = nil, 0, 0 +local statWorkerJit, statWorkerXrt = nil, nil +local statXrtSum, statXrtCount, statXrtMax = 0, 0, nil +local lastCosted -- --------------------------------------------------------------------------- -- worker management @@ -176,7 +182,7 @@ local function playMusicSync(data, header, allowLoops) { allowLoops = allowLoops }) if not ok then return nil, engine end local ok2, source = pcall( - love.audio.newQueueableSource, SAMPLE_RATE, 16, 2, MUSIC_BUFFER_COUNT) + love.audio.newQueueableSource, sampleRate(), 16, 2, MUSIC_BUFFER_COUNT) if not ok2 then return nil, source end ChipAudio.stopMusic() currentMusic = { source = source, engine = engine, threaded = false, @@ -221,7 +227,7 @@ function ChipAudio.playMusic(data, header, allowLoops) if not ok then return nil, engine end -- build the new source before tearing the old song down local ok2, source = pcall( - love.audio.newQueueableSource, SAMPLE_RATE, 16, 2, MUSIC_BUFFER_COUNT) + love.audio.newQueueableSource, sampleRate(), 16, 2, MUSIC_BUFFER_COUNT) if not ok2 then return nil, source end ChipAudio.stopMusic() musicGen = musicGen + 1 @@ -231,6 +237,7 @@ function ChipAudio.playMusic(data, header, allowLoops) channelVolumes = ChipSynth.getChannelVolumes(), channelPitches = ChipSynth.getChannelPitches(), stereo = ChipSynth.getStereo(), + sampleRate = sampleRate(), stereoEpoch = stereoEpoch }) currentMusic = { source = source, gen = gen, threaded = true, started = false, finished = false, @@ -274,6 +281,16 @@ local function updateThreaded() require("src.core.Logger").warn("chip audio: %s", tostring(buf.error)) m.finished = true elseif buf.sd then + if buf.jit ~= nil then statWorkerJit = buf.jit end + if type(buf.xrt) == "number" and buf ~= lastCosted then + lastCosted = buf + statWorkerXrt = buf.xrt + statXrtSum = statXrtSum + buf.xrt + statXrtCount = statXrtCount + 1 + if statXrtMax == nil or buf.xrt > statXrtMax then + statXrtMax = buf.xrt + end + end if free > 0 then if not pcall(m.source.queue, m.source, buf.sd) then return end else @@ -289,18 +306,65 @@ local function updateThreaded() end local function noteStats(m) + if not m.started or m.finished then return end statFrames = statFrames + 1 local depth = m.source and queuedBuffers(m.source) or nil + if depth then + statDepthSum = statDepthSum + depth + statDepthFrames = statDepthFrames + 1 + if statDepthMin == nil or depth < statDepthMin then statDepthMin = depth end + end if depth == 0 then statUnderruns = statUnderruns + 1 end - if statFrames % 60 == 0 then + if STATS and statFrames % 60 == 0 then require("src.core.Logger").info( "chipaudio: depth=%d/%d out=%d underruns=%d restarts=%d rate=%d", depth or -1, MUSIC_BUFFER_COUNT, (m.threaded and outCh) and outCh:getCount() or -1, - statUnderruns, statRestarts, SAMPLE_RATE) + statUnderruns, statRestarts, sampleRate()) end end +function ChipAudio.stats() + local m = currentMusic + local depth = (m and m.source) and queuedBuffers(m.source) or nil + local worker + if workerReady == nil then worker = "none" + elseif workerReady == false then worker = "sync" + elseif statWorkerJit == true then worker = "jit" + elseif statWorkerJit == false then worker = "interp" + else worker = "starting" end + local average = statDepthFrames > 0 + and (statDepthSum / statDepthFrames) or nil + local stats = { + rate = sampleRate(), + worker = worker, + depth = depth, + depthMax = MUSIC_BUFFER_COUNT, + depthMin = statDepthMin, + depthAvg = average, + frames = statFrames, + underruns = statUnderruns, + restarts = statRestarts, + xrt = statWorkerXrt, + xrtAvg = statXrtCount > 0 and (statXrtSum / statXrtCount) or nil, + xrtMax = statXrtMax, + buffers = statXrtCount, + } + local function num(value, places) + if type(value) ~= "number" then return "-" end + return string.format("%." .. places .. "f", value) + end + stats.line = string.format( + "rate=%d worker=%s depth=%s/%d min=%s avg=%s underruns=%d restarts=%d " + .. "xrt=%s/%s/%s n=%d", + stats.rate, worker, depth and tostring(depth) or "-", MUSIC_BUFFER_COUNT, + statDepthMin and tostring(statDepthMin) or "-", num(average, 1), + statUnderruns, statRestarts, + num(statWorkerXrt, 3), num(stats.xrtAvg, 3), num(statXrtMax, 3), + statXrtCount) + return stats +end + function ChipAudio.update() if suspended then return end local m = currentMusic @@ -310,7 +374,7 @@ function ChipAudio.update() else fillSync() end - if STATS then noteStats(m) end + noteStats(m) end -- Recover from a queue underrun caused by a long render stall. Called after @@ -419,7 +483,7 @@ function ChipAudio.rebuildPlayback() if not m then return true end if not (love.audio and love.audio.newQueueableSource) then return false end local ok, source = pcall( - love.audio.newQueueableSource, SAMPLE_RATE, 16, 2, MUSIC_BUFFER_COUNT) + love.audio.newQueueableSource, sampleRate(), 16, 2, MUSIC_BUFFER_COUNT) if not ok or not source then return false end pendingBuf = nil local old = m.source @@ -458,7 +522,7 @@ function ChipAudio.setStereo(enabled) -- (mixed under the previous pan) do not have to play out first (#1471) if not love.audio then return end local ok, source = pcall( - love.audio.newQueueableSource, SAMPLE_RATE, 16, 2, MUSIC_BUFFER_COUNT) + love.audio.newQueueableSource, sampleRate(), 16, 2, MUSIC_BUFFER_COUNT) if not ok or not source then return end local old = m.source m.source = source @@ -475,6 +539,37 @@ function ChipAudio.getStereo() return ChipSynth.getStereo() end +local envRate = os.getenv("POKEPORT_AUDIO_RATE") + +function ChipAudio._setEnvRateForTest(value) + envRate = value +end + +function ChipAudio.selectSampleRate(options) + local forced = tonumber(envRate) + if forced and forced >= 8000 and forced <= 48000 then + return math.floor(forced) + end + local tier = require("src.core.Performance") + .resolve(options and options.performance) + if tier == "low" then return 22050 end + local osName = love and love.system and love.system.getOS + and love.system.getOS() or nil + if osName == "Android" and tier ~= "high" then return 22050 end + return 44100 +end + +function ChipAudio.setSampleRate(rate) + local before = sampleRate() + if ChipSynth.setSampleRate(rate) == before then return false end + ChipAudio.stopMusic() + return true +end + +function ChipAudio.applyOptions(options) + return ChipAudio.setSampleRate(ChipAudio.selectSampleRate(options)) +end + -- Runtime mix for one hardware channel (1..4). Takes effect on the next -- synthesized buffer (live music) and on any SFX/cry rendered after the call. function ChipAudio.setChannelVolume(hw, scale) @@ -569,6 +664,7 @@ end -- frame 0 and DangerSoundLow on the `cp 16 / jr z, .halfway` frame 16, so the -- high tone owns 0..15 and the low tone 16..29. function ChipAudio.newLowHealthAlarm() + local SAMPLE_RATE = sampleRate() local samples = math.floor(SAMPLE_RATE * 60 / 60) local data = love.sound.newSoundData(samples, SAMPLE_RATE, 16, 2) local phase = 0 @@ -591,6 +687,9 @@ end function ChipAudio._setAudioStatsForTest(flag) STATS = not not flag statFrames, statUnderruns, statRestarts = 0, 0, 0 + statDepthMin, statDepthSum, statDepthFrames = nil, 0, 0 + statWorkerJit, statWorkerXrt = nil, nil + statXrtSum, statXrtCount, statXrtMax, lastCosted = 0, 0, nil, nil end function ChipAudio._audioStatsForTest() @@ -614,10 +713,11 @@ end function ChipAudio._renderMusicForTest(data, header, seconds) local engine = ChipSynth.newEngine(data, header, { allowLoops = true }) - return ChipSynth.soundData(engine, math.floor(seconds * SAMPLE_RATE), 2) + return ChipSynth.soundData(engine, math.floor(seconds * sampleRate()), 2) end function ChipAudio._renderMusicChannelForTest(data, header, seconds, number) + local SAMPLE_RATE = sampleRate() local engine = ChipSynth.newEngine(data, header, { allowLoops = true }) local samples = math.floor(seconds * SAMPLE_RATE) local result = love.sound.newSoundData(samples, SAMPLE_RATE, 16, 1) @@ -678,7 +778,7 @@ function ChipAudio._renderSfxForTest(data, header, seconds) sfx = true, allowLoops = false, }) - return ChipSynth.soundData(engine, math.floor(seconds * SAMPLE_RATE), 1) + return ChipSynth.soundData(engine, math.floor(seconds * sampleRate()), 1) end return ChipAudio diff --git a/src/core/ChipSynth.lua b/src/core/ChipSynth.lua index 587e1880..2f843746 100644 --- a/src/core/ChipSynth.lua +++ b/src/core/ChipSynth.lua @@ -49,6 +49,12 @@ function ChipSynth.getStereo() return stereoEnabled end +local monoFastPath = true + +function ChipSynth._setMonoFastPathForTest(enabled) + monoFastPath = not not enabled +end + -- Runtime mix per hardware channel (1 pulse, 2 pulse, 3 wave, 4 noise). -- Volume: 1 = authentic GB, 0 = mute. Pitch: 1 = authentic, 2 = +1 octave, -- 0.5 = -1 octave. Applied at sample time so a live change reaches the next @@ -153,6 +159,20 @@ local function snapTicks(ticks) return math.floor((ticks * SAMPLE_RATE + TICKS_PER_SECOND / 2) / TICKS_PER_SECOND) end +function ChipSynth.setSampleRate(rate) + rate = tonumber(rate) + if rate then + rate = math.floor(rate) + if rate < 8000 then rate = 8000 elseif rate > 48000 then rate = 48000 end + if rate ~= SAMPLE_RATE then + SAMPLE_RATE = rate + HPF_CHARGE = 0.999958 ^ (GB_CLOCK / SAMPLE_RATE) + ChipSynth.SAMPLE_RATE = SAMPLE_RATE + end + end + return SAMPLE_RATE +end + local cachedProgramFile local cachedBanks @@ -789,6 +809,7 @@ function Channel:nextEventGen2() local default = bit.bor(bit.lshift(mask, 4), mask) self.tracks = bit.band(packed, default) self.forcePanning = true + self.engine:refreshMonoMix() elseif command == 0xE5 then -- volume (global master; ignored for mix) self:byte() elseif command == 0xE6 then -- pitch_offset (big-endian) @@ -1306,9 +1327,26 @@ function Engine.new(data, header, options) plainFrames = options.plainFrames, }) end + engine:refreshMonoMix() return engine end +function Engine:refreshMonoMix() + if self.generation ~= 2 or stereoEnabled then + self.monoMix = false + return + end + for _, channel in ipairs(self.channels) do + local mask = bit.lshift(1, channel.hardware - 1) + if channel.tracks ~= nil + and channel.tracks ~= bit.bor(bit.lshift(mask, 4), mask) then + self.monoMix = false + return + end + end + self.monoMix = true +end + function Engine:finished() for _, channel in ipairs(self.channels) do if not channel.ended or channel.event then return false end @@ -1333,6 +1371,14 @@ function Engine:sample() end function Engine:sampleStereo() + if monoFastPath and self.monoMix and not stereoEnabled then + local value = 0 + local channels = self.channels + for index = 1, #channels do value = value + channels[index]:sample() end + local out = analogOut(self, value, "hpfCapLeft", "lpfLeft") + self.hpfCapRight, self.lpfRight = self.hpfCapLeft, self.lpfLeft + return out, out + end local left, right = 0, 0 for _, channel in ipairs(self.channels) do local value = channel:sample() @@ -1358,6 +1404,7 @@ function Engine:applyStereo() for _, channel in ipairs(self.channels) do channel:applyStereoMix() end + self:refreshMonoMix() end function Engine:sampleChannel(number) diff --git a/src/core/FaithfulRes.lua b/src/core/FaithfulRes.lua index e1535c77..ceb949cd 100644 --- a/src/core/FaithfulRes.lua +++ b/src/core/FaithfulRes.lua @@ -41,6 +41,7 @@ FaithfulRes.MIN_W, FaithfulRes.MIN_H = 480, 360 -- whether this module currently owns the window size FaithfulRes.locked = false +FaithfulRes.prevSize = nil -- The highest level this display can actually show. -- @@ -59,7 +60,7 @@ function FaithfulRes.maxLevel() -- means a different fraction of every device, and every level below the top -- is just a smaller picture for no reason. ON means one thing instead: -- lock the viewport to the Game Boy's 10:9 and size it to this screen. - if FaithfulRes.isMobile() then return 1 end + if FaithfulRes.fixedDisplay() then return 1 end return 4 end @@ -82,7 +83,7 @@ function FaithfulRes.label(v) v = FaithfulRes.normalize(v) if v == 0 then return "OFF" end -- mobile has one ON: the level is chosen from the display, not the player - if FaithfulRes.isMobile() then return "ON" end + if FaithfulRes.fixedDisplay() then return "ON" end return tostring(v) .. "X" end @@ -95,7 +96,7 @@ function FaithfulRes.cycle(v, dir) return levels[(cur - 1 + (dir or 1)) % #levels + 1] end -function FaithfulRes.isMobile() +function FaithfulRes.fixedDisplay() -- POKEPORT_FORCE_MOBILE=1: take the mobile branch on a desktop build, so the -- scale lock can be seen and driven without a device. The window is still -- resizable, which is the point -- drag it to a phone aspect, rotate it by @@ -104,9 +105,11 @@ function FaithfulRes.isMobile() if os.getenv("POKEPORT_FORCE_MOBILE") == "1" then return true end if not love or not love.system or not love.system.getOS then return false end local osName = love.system.getOS() - return osName == "Android" or osName == "iOS" + return osName == "Android" or osName == "iOS" or osName == "NX" end +FaithfulRes.isMobile = FaithfulRes.fixedDisplay + -- Physical pixels per LOVE unit for the CURRENT window. -- -- Deliberately NOT love.window.getDPIScale: that reports the display's @@ -165,7 +168,7 @@ end -- phone. function FaithfulRes.scaleCap() if not FaithfulRes.locked then return nil end - if not FaithfulRes.isMobile() then return nil end + if not FaithfulRes.fixedDisplay() then return nil end return FaithfulRes.deviceScale() end @@ -175,7 +178,7 @@ function FaithfulRes.apply(v) -- comes from the display (deviceScale), not from v -- v only says whether -- the lock is on. Nothing to restore on release: the renderer simply goes -- back to filling the display. - if FaithfulRes.isMobile() then + if FaithfulRes.fixedDisplay() then FaithfulRes.locked = v > 0 FaithfulRes.mobileScale = FaithfulRes.locked and FaithfulRes.deviceScale() or 0 return FaithfulRes.locked @@ -191,14 +194,23 @@ function FaithfulRes.apply(v) -- only touch the window if we were the one holding it: an OFF setting on -- boot must not resize a window the player sized themselves if not FaithfulRes.locked then return false end + local prev = FaithfulRes.prevSize flags.resizable = true - flags.minwidth, flags.minheight = FaithfulRes.MIN_W, FaithfulRes.MIN_H - love.window.setMode(curW, curH, flags) + flags.minwidth = (prev and prev.minwidth) or FaithfulRes.MIN_W + flags.minheight = (prev and prev.minheight) or FaithfulRes.MIN_H + love.window.setMode((prev and prev.w) or curW, (prev and prev.h) or curH, + flags) + FaithfulRes.prevSize = nil FaithfulRes.locked = false return false end local w, h = FaithfulRes.size(v) + if not FaithfulRes.locked then + FaithfulRes.prevSize = { w = curW, h = curH, + minwidth = flags.minwidth, + minheight = flags.minheight } + end -- An exact size and a desktop-fullscreen mode cannot both hold. The lock -- is the more specific request, so it wins and drops fullscreen; VIDEO MODE -- reads BORDERLESS until the player changes it, which then releases this. diff --git a/src/core/Game2.lua b/src/core/Game2.lua index 51da5b60..2176cca6 100644 --- a/src/core/Game2.lua +++ b/src/core/Game2.lua @@ -33,6 +33,7 @@ local TextBox = require("src.render.TextBox") -- finds it in the same place in Gold. local TouchControls = require("src.core.TouchControls") local World = require("src.world.gen2.World") +local MapNameSign = require("src.world.gen2.MapNameSign") -- The mod event/hook buses. Gold reaches them through Runtime like every -- other engine file, so a call site here is the same call site Gen 1 has. @@ -442,8 +443,29 @@ function Game2:openStartMenu() }) end +-- ../pokecrystal/engine/menus/start_menu.asm:444-518 function Game2:openStartMenuItem(id) - local function back() self.stack:pop() end + local MenuFade = require("src.ui.gen2.MenuFade") + local party = self.save and self.save.party + local white = MenuFade.openWhite(id, party and #party or 0) + if not white then return self:pushStartMenuItem(id) end + Screens.push(self, "Gen2MenuFade", { + kind = "out", white = white, + onDone = function() self:pushStartMenuItem(id) end, + }) +end + +-- ../pokecrystal/home/map.asm:1919-1925 +function Game2:closeStartMenuItem(id) + local MenuFade = require("src.ui.gen2.MenuFade") + self.stack:pop() + local white = MenuFade.closeWhite(id) + if not white then return end + Screens.push(self, "Gen2MenuFade", { kind = "in", white = white }) +end + +function Game2:pushStartMenuItem(id) + local function back() self:closeStartMenuItem(id) end if id == "pokedex" then Screens.push(self, "Gen2PokedexMenu", { onClose = back }) elseif id == "pokemon" then @@ -1258,6 +1280,8 @@ function Game2:update(dt) while self.audioAccum >= step do self.audioAccum = self.audioAccum - step Music.update(self.data) + -- ../pokecrystal/engine/overworld/events.asm:177-191 + if self.world and self.world.map then MapNameSign.frame(self.world) end end -- TILT eases toward its new angle in real time, not on the logic clock, so -- fast-forward does not fling the camera over. diff --git a/src/core/IssueReport.lua b/src/core/IssueReport.lua index 1b6b5979..2b189620 100644 --- a/src/core/IssueReport.lua +++ b/src/core/IssueReport.lua @@ -240,6 +240,27 @@ local function metadata(options, context) if guardOk and guardPresent == false then add("Loader chain", "love searcher missing") end + local audioOk, audioLine = pcall(function() + return require("src.core.ChipAudio").stats().line + end) + if audioOk then add("Audio", audioLine) end + local function firstLine(s) + if type(s) ~= "string" then return nil end + for line in s:gmatch("[^\r\n]+") do + if line:find("ERROR", 1, true) then return line end + end + return s:match("[^\r\n]+") + end + local launcher = package.loaded["src.import.LauncherView"] + if type(launcher) == "table" and type(launcher.cartShaderError) == "function" then + local okCart, cartErr = pcall(launcher.cartShaderError) + if okCart and cartErr then add("Cart shader", firstLine(cartErr)) end + end + local shaderFx = package.loaded["src.render.ShaderFX"] + if type(shaderFx) == "table" and type(shaderFx.lastError) == "function" then + local okFx, fxErr = pcall(shaderFx.lastError) + if okFx and fxErr then add("Shader FX", firstLine(fxErr)) end + end return { rawOS = rawOS, os = formOS(rawOS), diff --git a/src/core/Music.lua b/src/core/Music.lua index 3144a18d..6fa0915e 100644 --- a/src/core/Music.lua +++ b/src/core/Music.lua @@ -313,7 +313,7 @@ function Music.play(data, song, loop, ctx) -- (update() starts it, like the paused-song resume) if fanfareActive() then state.fanfareResume = true - else + elseif not isChip then pcall(src.play, src) end local previous = state.current @@ -519,6 +519,13 @@ function Music.applyOptions(opts) Music.setFilterLevel(opts and opts.musicFilter or 0) -- engine/menus/options_menu.asm SOUND row (wOptions STEREO bit) local ChipAudio = require("src.core.ChipAudio") + local data, song, loop = state.data, state.current, state.loop + local tempo, start, wasChip = state.tempo, state.start, state.chip + if ChipAudio.applyOptions(opts) and wasChip and data and song then + Music.stop() + Music.play(data, song, loop, { reason = "audiorate", selected = true, + tempo = tempo, start = start }) + end ChipAudio.setStereo(opts and opts.sound == "STEREO") -- setStereo may swap the queueable source so the new pan is not sitting -- behind already-mixed buffers; re-bind so volume/filter follow (#1471) diff --git a/src/core/RomText.lua b/src/core/RomText.lua index 5274bd46..8d841573 100644 --- a/src/core/RomText.lua +++ b/src/core/RomText.lua @@ -41,10 +41,13 @@ return function(data, label, fallback, ...) end)) end + local ENDINGS = { ["{DONE}"] = true, ["{PROMPT}"] = true } local slots, named = 0, 0 for token in text:gmatch("%b{}") do - slots = slots + 1 - if token == "{PLAYER}" or token == "{RIVAL}" then named = named + 1 end + if not ENDINGS[token] then + slots = slots + 1 + if token == "{PLAYER}" or token == "{RIVAL}" then named = named + 1 end + end end local fillNamed if #args == slots then @@ -57,6 +60,7 @@ return function(data, label, fallback, ...) local index = 0 return (text:gsub("%b{}", function(token) + if ENDINGS[token] then return token end if not fillNamed and (token == "{PLAYER}" or token == "{RIVAL}") then return token end diff --git a/src/core/SaveData.lua b/src/core/SaveData.lua index 5dcdbbf0..2da97507 100644 --- a/src/core/SaveData.lua +++ b/src/core/SaveData.lua @@ -1189,11 +1189,10 @@ function SaveData.listSlots(version) return listSlotsIn(version) end -function SaveData.readSlotSource(version, slotId, injectedFs) - version = version or GameVersion.get() - if not knownVersion(version) or type(slotId) ~= "string" then return nil end +local function readSlotSourceIn(key, slotId, injectedFs) + if type(slotId) ~= "string" then return nil end local fs = persistFs(injectedFs) - local main, bak, tmp = slotNames(version, slotId) + local main, bak, tmp = slotNames(key, slotId) for _, name in ipairs({ main, tmp, bak }) do if fs.getInfo(name) then local body = fs.read(name) @@ -1205,6 +1204,12 @@ function SaveData.readSlotSource(version, slotId, injectedFs) return nil end +function SaveData.readSlotSource(version, slotId, injectedFs) + version = version or GameVersion.get() + if not knownVersion(version) then return nil end + return readSlotSourceIn(version, slotId, injectedFs) +end + -- Give a registered slot a custom label (#205: "a way to name save slots so -- you can see that in the launcher"). The label lives in the options -- registry next to list/active, never in the save file itself, so renaming @@ -1507,6 +1512,26 @@ function SaveData.listCartSlots(cartId) return listSlotsIn(key) end +function SaveData.cartsWithSlots(injectedFs) + local opts = SaveData.loadOptions(injectedFs) + local root = type(opts.cartSlots) == "table" and opts.cartSlots or {} + local out = {} + for id, reg in pairs(root) do + if cartKey(id) and type(reg) == "table" and type(reg.list) == "table" + and #reg.list > 0 then + out[#out + 1] = id + end + end + table.sort(out) + return out +end + +function SaveData.readCartSlotSource(cartId, slotId, injectedFs) + local key = cartKey(cartId or activeCart) + if not key then return nil end + return readSlotSourceIn(key, slotId, injectedFs) +end + function SaveData.createCartSlot(cartId) local key = cartKey(cartId or activeCart) if not key then return nil end @@ -1543,10 +1568,14 @@ function SaveData.writeCartSlot(cartId, slotId, saveTable) if not key then return false, "unknown cart" end local ok, err = writeSlotIn(key, slotId, saveTable) if not ok then return ok, err end - local hash = type(saveTable.meta) == "table" and saveTable.meta.cartHash or nil + local meta = type(saveTable.meta) == "table" and saveTable.meta or nil + local hash = meta and meta.cartHash or nil if type(hash) == "string" and hash ~= "" then SaveData.setSlotCartHash(cartId or activeCart, slotId, hash) end + if meta and meta.sealBroken == true then + SaveData.markSlotSealBroken(cartId or activeCart, slotId) + end return true end @@ -1742,26 +1771,36 @@ function SaveData.ensurePlaythroughId(save, injectedFs) return id end -function SaveData.slotPlaythroughId(version, slotId, save, injectedFs) - version = version or GameVersion.get() - if not knownVersion(version) then return nil end +local function slotPlaythroughIdIn(key, slotId, save, injectedFs) if type(slotId) ~= "string" or slotId == "" then return nil end local meta = type(save) == "table" and save.meta local stamped = type(meta) == "table" and meta.playthroughId local opts = SaveData.loadOptions(injectedFs) - local byVersion = opts.playthroughIds and opts.playthroughIds[version] - local mapped = byVersion and byVersion[slotId] + local byScope = opts.playthroughIds and opts.playthroughIds[key] + local mapped = byScope and byScope[slotId] local id = (type(stamped) == "string" and stamped ~= "") and stamped or mapped if type(id) ~= "string" or id == "" then id = SaveData.newPlaythroughId() end if mapped ~= id then opts.playthroughIds = opts.playthroughIds or {} - opts.playthroughIds[version] = opts.playthroughIds[version] or {} - opts.playthroughIds[version][slotId] = id + opts.playthroughIds[key] = opts.playthroughIds[key] or {} + opts.playthroughIds[key][slotId] = id SaveData.saveOptions(opts, injectedFs) end return id end +function SaveData.slotPlaythroughId(version, slotId, save, injectedFs) + version = version or GameVersion.get() + if not knownVersion(version) then return nil end + return slotPlaythroughIdIn(version, slotId, save, injectedFs) +end + +function SaveData.cartSlotPlaythroughId(cartId, slotId, save, injectedFs) + local key = cartKey(cartId or activeCart) + if not key then return nil end + return slotPlaythroughIdIn(key, slotId, save, injectedFs) +end + -- Resolve the already-selected playthrough without changing the supplied save -- or allocating a replacement id. Title tools use this before a normal SAVE: -- Game:load intentionally owns a fresh skeleton there, while the selected @@ -1851,6 +1890,7 @@ function SaveData.buildMeta(mods, previous, sessionStart) savedAt = savedAt, sessionStart = started, playthroughId = type(previous) == "table" and previous.playthroughId or nil, + cartId = type(previous) == "table" and previous.cartId or nil, cartHash = type(previous) == "table" and previous.cartHash or nil, sealBroken = (type(previous) == "table" and previous.sealBroken == true) or nil, mods = list, @@ -2084,8 +2124,9 @@ function SaveData.save(data, mods) local opts, changed = rememberPlaythroughId(data) if changed then SaveData.saveOptions(opts) end end - if activeCart and activeCartHash and type(data.meta) == "table" then - data.meta.cartHash = activeCartHash + if activeCart and type(data.meta) == "table" then + data.meta.cartId = activeCart + if activeCartHash then data.meta.cartHash = activeCartHash end end if sealBroken and type(data.meta) == "table" then data.meta.sealBroken = true diff --git a/src/core/Timing.lua b/src/core/Timing.lua index ad6c6f5a..86e60703 100644 --- a/src/core/Timing.lua +++ b/src/core/Timing.lua @@ -101,6 +101,9 @@ Timing.MOVE_ANIM_OFF = 30 -- beat is paid on a large fraction of all turns. Timing.MOVE_STATUS_OR_MISS = 30 +-- ../pokered/engine/battle/move_effects/substitute.asm:2-3 +Timing.SUBSTITUTE_ENTRY = 50 + -- PlayApplyingAttackAnimation's six types (AnimationTypePointerTable, -- engine/battle/animations.asm:490-524). The two shake families are -- `AnimationShakeScreenHorizontallySlow`, whose double push/pop makes each diff --git a/src/core/VideoMode.lua b/src/core/VideoMode.lua index 39dc715a..c4956b24 100644 --- a/src/core/VideoMode.lua +++ b/src/core/VideoMode.lua @@ -1,8 +1,4 @@ -- Windowed vs borderless (desktop) fullscreen. --- --- Persisted as save.options.videoMode. Applied from OptionsMenu and on --- boot via Game:applyOptions. No-ops on mobile and in headless stubs --- that lack love.window. local VideoMode = {} @@ -23,12 +19,14 @@ function VideoMode.modeLabel(mode) return LABELS[VideoMode.normalize(mode)] or LABELS[VideoMode.DEFAULT] end -function VideoMode.isMobile() +function VideoMode.fixedDisplay() if not love or not love.system or not love.system.getOS then return false end local osName = love.system.getOS() - return osName == "Android" or osName == "iOS" + return osName == "Android" or osName == "iOS" or osName == "NX" end +VideoMode.isMobile = VideoMode.fixedDisplay + -- Cycle windowed <-> borderless (dir ignored; two modes). function VideoMode.cycle(mode, _dir) return VideoMode.normalize(mode) == "borderless" and "windowed" or "borderless" @@ -36,7 +34,7 @@ end -- Push the mode into the live window. Safe when love.window is missing. function VideoMode.apply(mode) - if VideoMode.isMobile() then return end + if VideoMode.fixedDisplay() then return end if not love or not love.window or not love.window.setFullscreen then return end mode = VideoMode.normalize(mode) if mode == "borderless" then diff --git a/src/core/chip_worker.lua b/src/core/chip_worker.lua index 88de0199..4e839444 100644 --- a/src/core/chip_worker.lua +++ b/src/core/chip_worker.lua @@ -23,6 +23,12 @@ require("love.timer") require("love.sound") require("love.filesystem") +local jitEnabled = false +if os.getenv("POKEPORT_AUDIO_JIT") == "1" and jit and jit.on then + pcall(jit.on) + jitEnabled = (jit.status and jit.status()) == true +end + -- Load the synth explicitly via love.filesystem (a fresh thread Lua state does -- not necessarily carry the package searcher that resolves "src.core..."): local ChipSynth = assert(love.filesystem.load("src/core/ChipSynth.lua"))() @@ -31,6 +37,7 @@ local cmdCh = love.thread.getChannel("chipaudio_cmd") local outCh = love.thread.getChannel("chipaudio_out") local BUF = ChipSynth.MUSIC_BUFFER_SAMPLES +local BUF_SECONDS = BUF / ChipSynth.SAMPLE_RATE -- how many finished buffers may sit in the hand-off channel before the worker -- pauses. The deep (~6s) playback depth lives in the main-thread Source; this -- only bounds the worker's look-ahead (and its memory) between drains. @@ -49,6 +56,9 @@ local function handle(cmd) engine = nil outCh:clear() -- drop any buffers left from the previous song data = { audio = cmd.audio } + if cmd.sampleRate ~= nil then + BUF_SECONDS = BUF / ChipSynth.setSampleRate(cmd.sampleRate) + end if cmd.channelVolumes ~= nil then ChipSynth.setChannelVolumes(cmd.channelVolumes) end @@ -104,13 +114,16 @@ while true do if engine and not finished and gen and outCh:getCount() < LOOKAHEAD then idleWait = false local activeGen = gen + local began = love.timer.getTime() local ok, sd = pcall(ChipSynth.soundData, engine, BUF, 2) if not ok then outCh:push({ gen = activeGen, error = tostring(sd), - stereoEpoch = stereoEpoch }) + stereoEpoch = stereoEpoch, jit = jitEnabled }) finished = true else - outCh:push({ gen = activeGen, sd = sd, stereoEpoch = stereoEpoch }) + outCh:push({ gen = activeGen, sd = sd, stereoEpoch = stereoEpoch, + jit = jitEnabled, + xrt = (love.timer.getTime() - began) / BUF_SECONDS }) if engine:finished() then outCh:push({ gen = activeGen, done = true, stereoEpoch = stereoEpoch }) finished = true diff --git a/src/core/gen2/CommonText.lua b/src/core/gen2/CommonText.lua index f6aa704d..858b646a 100644 --- a/src/core/gen2/CommonText.lua +++ b/src/core/gen2/CommonText.lua @@ -37,8 +37,15 @@ function CommonText.get(text, label) return body end +-- ../pokecrystal/home/text.asm:548 PromptText, :566 DoneText +function CommonText.plain(body) + if type(body) ~= "string" then return body end + return (body:gsub("{DONE}", ""):gsub("{PROMPT}", "")) +end + -- One page is an array of one or two lines. function CommonText.pages(body) + body = CommonText.plain(body) if type(body) ~= "string" or body == "" then return nil end local out = {} local top, bottom = "", nil diff --git a/src/core/gen2/ItemEffects.lua b/src/core/gen2/ItemEffects.lua index 3090e980..b0b8a8a6 100644 --- a/src/core/gen2/ItemEffects.lua +++ b/src/core/gen2/ItemEffects.lua @@ -74,6 +74,8 @@ local BITTER = { -- _ItemWontHaveEffectText / _ItemCantUseOnEggText (data/text/common_3.asm). ItemEffects.TEXT_NO_EFFECT = "It won't have any\neffect." ItemEffects.TEXT_CANT_USE_ON_EGG = "That can't be used\non an EGG." +-- _ItemCantUseOnMonText (data/text/common_3.asm:1265). +ItemEffects.TEXT_CANT_USE_ON_MON = "That can't be used\non this #MON." -- _PPRestoredText (data/text/common_3.asm). ItemEffects.TEXT_PP_RESTORED = "PP was restored." -- _PPIsMaxedOutText / _PPsIncreasedText (data/text/common_3.asm). diff --git a/src/import/CacheContract.lua b/src/import/CacheContract.lua index e8718104..d0331abf 100644 --- a/src/import/CacheContract.lua +++ b/src/import/CacheContract.lua @@ -14,9 +14,9 @@ CacheContract.VERSION_FORMAT = { -- export re-anchoring a save onto another map writes back into -- wCurMapObjectEventsPointer. A v10 cache has no address to write, and -- such an export is refused until the ROM re-imports. - gold = "rom-cache-v11:", - silver = "rom-cache-v11:", - crystal = "rom-cache-v11-crystal4:", + gold = "rom-cache-v12:", + silver = "rom-cache-v12:", + crystal = "rom-cache-v12-crystal4:", } CacheContract.MARKER_PATH = "rom-cache.complete" diff --git a/src/import/LauncherView.lua b/src/import/LauncherView.lua index 092fb732..7d9d22b4 100644 --- a/src/import/LauncherView.lua +++ b/src/import/LauncherView.lua @@ -693,29 +693,35 @@ local function cartLabelMesh(imp, key, label, points) end local CART_HOVER_SHADER = [[ +#if defined(GL_ES) && defined(PIXEL) && !defined(GL_FRAGMENT_PRECISION_HIGH) +#define CART_HP mediump +#else +#define CART_HP highp +#endif + +varying CART_HP vec2 cart_screen_pos; + +#ifdef VERTEX extern vec2 mouse_screen_pos; extern float hovering; extern float screen_scale; -#ifdef VERTEX vec4 position(mat4 transform_projection, vec4 vertex_position) { + vec4 clip = transform_projection * vertex_position; + cart_screen_pos = (clip.xy / clip.w * 0.5 + 0.5) * love_ScreenSize.xy; if (hovering <= 0.) { - return transform_projection * vertex_position; + return clip; } float mid_dist = length(vertex_position.xy - 0.5 * love_ScreenSize.xy) / length(love_ScreenSize.xy); vec2 mouse_offset = (vertex_position.xy - mouse_screen_pos.xy) / screen_scale; float scale = 0.2 * (-0.03 - 0.3 * max(0., 0.3 - mid_dist)) * hovering * (length(mouse_offset) * length(mouse_offset)) / (2. - mid_dist); - return transform_projection * vertex_position + vec4(0.0, 0.0, 0.0, scale); + return clip + vec4(0.0, 0.0, 0.0, scale); } #endif #ifdef PIXEL -#if defined(GL_ES) && defined(GL_FRAGMENT_PRECISION_HIGH) -precision highp float; -#endif - // finish: 0 plain, 1 sparkle (glitter suspended in the shell), 2 holographic // label sweep. Both are ours, not ported from anywhere. extern float finish; @@ -725,8 +731,8 @@ extern float finish_wave; const float CART_TAU = 6.2831853; -float sparkHash(vec2 p) { - float h = fract(p.x * 0.1031 + p.y * 0.3711 + 0.137); +CART_HP float sparkHash(CART_HP vec2 p) { + CART_HP float h = fract(p.x * 0.1031 + p.y * 0.3711 + 0.137); h = fract(h * (h + 47.13)); h = fract(h * (h + 19.77)); return h; @@ -734,18 +740,18 @@ float sparkHash(vec2 p) { // Flecks live on a jittered lattice so they read as suspended grains rather // than a regular grid, and each one twinkles on its own phase. -vec3 sparkle(vec2 screen_coords) { - vec2 cell = screen_coords / 19.0; - vec2 id = mod(floor(cell), 512.0); - vec2 f = fract(cell); +vec3 sparkle(CART_HP vec2 sc) { + CART_HP vec2 cell = sc / 19.0; + CART_HP vec2 id = mod(floor(cell), 512.0); + CART_HP vec2 f = fract(cell); float peak = 0.0; for (int oy = -1; oy <= 1; oy++) { for (int ox = -1; ox <= 1; ox++) { - vec2 n = vec2(float(ox), float(oy)); - vec2 h = vec2(sparkHash(id + n), sparkHash(id + n + 17.0)); - float phase = sparkHash(id + n + 71.0) * CART_TAU; + CART_HP vec2 n = vec2(float(ox), float(oy)); + CART_HP vec2 h = vec2(sparkHash(id + n), sparkHash(id + n + 17.0)); + CART_HP float phase = sparkHash(id + n + 71.0) * CART_TAU; float tw = sin(finish_twinkle + phase) * 0.5 + 0.5; - float d = length(f - (n + h)); + CART_HP float d = length(f - (n + h)); peak = max(peak, (1.0 - smoothstep(0.0, 0.13, d)) * pow(max(tw, 0.0), 16.0)); } } @@ -769,13 +775,21 @@ vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords) { px.rgb = mix(px.rgb, px.rgb * (0.65 + sheen), 0.30 * (0.35 + 0.65 * lum)); px.rgb += sheen * 0.05 * px.a; } else if (finish > 0.5) { - px.rgb += sparkle(screen_coords) * 0.70 * px.a; + px.rgb += sparkle(cart_screen_pos) * 0.70 * px.a; } return px; } #endif ]] +local cartShaderError = nil + +local function cartShaderIsEs() + if not (love.system and love.system.getOS) then return false end + local osName = love.system.getOS() + return osName == "Android" or osName == "iOS" +end + local function cartHoverShader(imp) if imp._cartHoverShader ~= nil then return imp._cartHoverShader or nil @@ -784,7 +798,20 @@ local function cartHoverShader(imp) imp._cartHoverShader = false return nil end - local ok, sh = pcall(love.graphics.newShader, CART_HOVER_SHADER) + local ok, err = true, nil + if love.graphics.validateShader then + ok, err = love.graphics.validateShader(cartShaderIsEs(), CART_HOVER_SHADER) + end + local sh + if ok then + ok, sh = pcall(love.graphics.newShader, CART_HOVER_SHADER) + if not ok then err, sh = sh, nil end + end + if not ok then + cartShaderError = tostring(err) + imp._cartShaderError = cartShaderError + require("src.core.Logger").error("cart shader: %s", cartShaderError) + end imp._cartHoverShader = ok and sh or false return imp._cartHoverShader or nil end @@ -818,6 +845,7 @@ local function cartSendFinish(shader, mode, spin) end LauncherView.CART_HOVER_SHADER = CART_HOVER_SHADER +LauncherView.cartShaderError = function() return cartShaderError end LauncherView.cartFinishPhases = cartFinishPhases LauncherView.cartHull = cartHull @@ -2516,15 +2544,15 @@ local function cartsWithUpdates(imp) return (ok and type(rows) == "table") and rows or {} end +local function updateAllRows(imp) + if not imp._updateAllRows then return {} end + local ok, rows = pcall(imp._updateAllRows, imp) + return (ok and type(rows) == "table") and rows or {} +end + local function modsWithUpdates(imp) local names = {} - for _, m in ipairs(imp.mods or {}) do - local info = imp._modUpdateInfo and imp:_modUpdateInfo(m.id) - if info and info.status == "available" and info.best then - names[#names + 1] = m.name or m.id - end - end - for _, row in ipairs(cartsWithUpdates(imp)) do + for _, row in ipairs(updateAllRows(imp)) do names[#names + 1] = row.name or row.id end return names @@ -2533,18 +2561,17 @@ end local function modsWithUpdatesCount(imp) local mods = imp.mods or {} local rev = imp._modUpdateRev or 0 - local carts = #cartsWithUpdates(imp) + local cartCache = imp._cartUpdateCache local cache = imp._modUpdateCountCache - if cache and cache.src == mods and cache.n == #mods and cache.rev == rev then - return cache.count + carts + if cache and cache.src == mods and cache.n == #mods and cache.rev == rev + and cartCache and cache.carts == cartCache.rows then + return cache.count end - local n = 0 - for _, m in ipairs(mods) do - local info = imp._modUpdateInfo and imp:_modUpdateInfo(m.id) - if info and info.status == "available" and info.best then n = n + 1 end - end - imp._modUpdateCountCache = { src = mods, n = #mods, rev = rev, count = n } - return n + carts + local count = #updateAllRows(imp) + cartCache = imp._cartUpdateCache + imp._modUpdateCountCache = { src = mods, n = #mods, rev = rev, + carts = cartCache and cartCache.rows, count = count } + return count end local function askUpdateAllMods(imp) @@ -2573,6 +2600,7 @@ local function buildModsPanel(imp, x, y, w, availH, m) if imp.modCartPlan then cartId, cartReport = imp:modCartPlan() end -- a cart owns its mod set: only the pins it already ships may be switched local bulkOk = not safeMode and cartId == nil + local updateOk = (imp.updateAllAvailable and imp:updateAllAvailable()) == true -- header: progressive action cluster. Surfaces primary/frequent actions -- (Import, Updates, Sort) directly on the bar across screen sizes, placing @@ -2615,7 +2643,7 @@ local function buildModsPanel(imp, x, y, w, availH, m) btn(imp, place(updateAllW), cy, updateAllW, bh, "mods-update-all", Strings("Update all"), { kind = (modsWithUpdatesCount(imp) > 0) and "warn" or "ghost", - font = "small", enabled = bulkOk and imp._updateAll == nil, + font = "small", enabled = updateOk, action = function() askUpdateAllMods(imp) end }) btn(imp, place(sortW), cy, sortW, bh, "mods-sort", Strings("Sort"), { font = "small", @@ -2662,7 +2690,7 @@ local function buildModsPanel(imp, x, y, w, availH, m) btn(imp, place(updateAllW), cy, updateAllW, bh, "mods-update-all", Strings("Update all"), { kind = "warn", font = "small", - enabled = bulkOk and imp._updateAll == nil, + enabled = updateOk, action = function() askUpdateAllMods(imp) end }) end end @@ -4177,13 +4205,16 @@ local function buildModHeaderActionsModal(imp, m) local w = math.floor(380 * m.s) local gap = math.floor(8 * m.s) -- same gate as the header cluster: a cart's mod set is not bulk-editable - local bulkOk = not imp.safeMode - and not (imp.modCartPlan and imp:modCartPlan()) + local cartId, cartReport + if imp.modCartPlan then cartId, cartReport = imp:modCartPlan() end + local bulkOk = not imp.safeMode and cartId == nil + local updateOk = (imp.updateAllAvailable and imp:updateAllAvailable()) == true + local note = (cartId and imp.cartPinsNote) and imp:cartPinsNote(cartId, cartReport) or nil local btns = { { label = Strings("Mod profiles..."), action = function() imp._profilesPopup = true end }, { label = Strings("Check for updates"), action = function() imp:_syncModUpdateInfo(true) end }, { label = Strings("Update all"), kind = "warn", - enabled = bulkOk and imp._updateAll == nil, + enabled = updateOk, action = function() askUpdateAllMods(imp) end }, { label = Strings("Enable all mods"), kind = "good", enabled = bulkOk, action = function() imp:_setAllMods(true) end }, @@ -4191,12 +4222,19 @@ local function buildModHeaderActionsModal(imp, m) action = function() imp:_setAllMods(false) end }, { label = Strings("Sort mods..."), action = function() imp._sortPopup = "mods" end }, } - local h = pad + Kit.textHeight("button") + math.floor(12 * m.s) + local noteW = math.floor(math.min(w, m.W - 2 * m.pad)) - 2 * pad + local noteH = note + and (Kit.wrapHeight("small", note, noteW, 3) + gap) or 0 + local h = pad + Kit.textHeight("button") + math.floor(12 * m.s) + noteH + #btns * (m.btnH + gap) + m.btnH + pad local px, py, pw = modalPanel(m, w, h) local cy = py + pad Kit.text("button", Strings("More Mod Actions"), px + pad, cy, PAL.heading) cy = cy + Kit.textHeight("button") + math.floor(12 * m.s) + if note then + Kit.textWrapped("small", note, px + pad, cy, noteW, PAL.muted, 3) + cy = cy + noteH + end for i, b in ipairs(btns) do btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "modheadact-" .. i, b.label, { diff --git a/src/import/RomExtractorGen2.lua b/src/import/RomExtractorGen2.lua index d560d28c..cf3404f6 100644 --- a/src/import/RomExtractorGen2.lua +++ b/src/import/RomExtractorGen2.lua @@ -2898,7 +2898,11 @@ function RomExtractorGen2:decodeGen2Text(bank, address, charmap, buffers) if b == 0x50 then if not inString then break end -- TX_END inString = false -- `@`: end of this chunk - elseif b == 0x57 or b == 0x58 then -- DONE / PROMPT, both PlaceString's + elseif b == 0x57 or b == 0x58 then + -- ../pokecrystal/home/text.asm:548 PromptText, :566 DoneText + if #out > 0 then + out[#out + 1] = (b == 0x58) and "{PROMPT}" or "{DONE}" + end break elseif b == 0x00 then inString = true -- TX_START @@ -4734,6 +4738,65 @@ function RomExtractorGen2:readContestMons() return out end +-- ../pokecrystal/data/wild/treemons.asm:1 TreeMons +function RomExtractorGen2:readTreeMons() + local symbol = self:symbol("TreeMons") + local setOrder = self.manifest.constants.treeMonSetOrder or {} + local speciesOrder = self.manifest.constants.speciesOrder or {} + local out = {} + for index, name in ipairs(setOrder) do + local at = self.rom:word(symbol.bank, symbol.address + (index - 1) * 2) + local function list() + local rows = {} + for _ = 1, 16 do + if not romAddrOk(symbol.bank, at + 2) then break end + local chance = self.rom:byte(symbol.bank, at) + if chance == 0xff then + at = at + 1 + break + end + rows[#rows + 1] = { + chance = chance, + species = speciesOrder[self.rom:byte(symbol.bank, at + 1)], + level = self.rom:byte(symbol.bank, at + 2), + } + at = at + 3 + end + return rows + end + -- ../pokecrystal/engine/events/treemons.asm:28 RockMonEncounter + if name == "TREEMON_SET_ROCK" then + out[name] = { common = list() } + else + local common = list() + out[name] = { common = common, rare = list() } + end + end + return out +end + +-- ../pokecrystal/data/wild/treemons_asleep.asm:3 AsleepTreeMonsNite +function RomExtractorGen2:readAsleepTreeMons() + local speciesOrder = self.manifest.constants.speciesOrder or {} + local labels = { MORN = "AsleepTreeMonsMorn", DAY = "AsleepTreeMonsDay", + NITE = "AsleepTreeMonsNite" } + local out = {} + for key, label in pairs(labels) do + local location = self.symbols[label] + if not location then return nil end + local bank, address = location[1], location[2] + local species = {} + for offset = 0, 63 do + if not romAddrOk(bank, address + offset) then break end + local b = self.rom:byte(bank, address + offset) + if b == 0xff then break end + if speciesOrder[b] then species[#species + 1] = speciesOrder[b] end + end + out[key] = species + end + return out +end + function RomExtractorGen2:extractEncounters() self:beginStage("Wild encounters") local grass = {} @@ -4842,41 +4905,9 @@ function RomExtractorGen2:extractEncounters() local rocks = self.symbols.RockMonMaps and self:readTreeMonMaps("RockMonMaps") or nil - -- TreeMons: a pointer per TREEMON_SET_*, each aiming at TWO `db %, species, - -- level` lists back to back -- the common one and the rare one -- with a - -- -1 between them. Which of the two is rolled comes from how hard the tree - -- was hit (engine/events/treemons.asm), so both are carried here. - local treeSets = {} - local treeMonsSymbol = self.symbols["TreeMons"] and self:symbol("TreeMons") - if treeMonsSymbol then - local setOrder = self.manifest.constants.treeMonSetOrder or {} - for index, name in ipairs(setOrder) do - local pointer = self.rom:word(treeMonsSymbol.bank, - treeMonsSymbol.address + (index - 1) * 2) - local at = pointer - local lists = {} - for _ = 1, 2 do - local rows = {} - for _ = 1, 16 do - local chance = self.rom:byte(treeMonsSymbol.bank, at) - if chance == 0xff then - at = at + 1 - break - end - local species = self.rom:byte(treeMonsSymbol.bank, at + 1) - local level = self.rom:byte(treeMonsSymbol.bank, at + 2) - rows[#rows + 1] = { - chance = chance, - species = (self.manifest.constants.speciesOrder or {})[species], - level = level, - } - at = at + 3 - end - lists[#lists + 1] = rows - end - treeSets[name] = { common = lists[1] or {}, rare = lists[2] or {} } - end - end + local treeSets = self.symbols.TreeMons and self:readTreeMons() or {} + local treeMonsAsleep = self.symbols.AsleepTreeMonsNite + and self:readAsleepTreeMons() or nil -- The Bug Catching Contest's own table. It sits beside the grass rather -- than inside it because the park's encounters come from HERE for the @@ -4915,6 +4946,7 @@ function RomExtractorGen2:extractEncounters() trees = trees, rocks = rocks, treeSets = treeSets, + treeMonsAsleep = treeMonsAsleep, bugContest = bugContest, swarmGrass = swarmGrass, swarmWater = swarmWater, diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index f012533a..2d70d87e 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -985,6 +985,13 @@ local function findPendingRom(ready, wanted) return nil end +local function importPendingRom(self) + local name, data = findPendingRom(self.ready, self.chooseVersion) + if not name then return false end + self:startData(data, name) + return true +end + -- GameActivity always writes the SAF pick to picked_rom.gb, so a leftover -- under that exact basename is the file the player just chose and -- findPendingRom silently refused: wrong size, or a hacked/overdumped cart @@ -2742,6 +2749,7 @@ function RomImporter:choose(version) or os.getenv("MUOS") == "1" or os.getenv("KNULLI") == "1" if isHandheld then + if importPendingRom(self) then return end local okKit, Kit = pcall(require, "src.ui.kit.Kit") if okKit and Kit.FileBrowser then self._padCursorActive = false @@ -2761,6 +2769,7 @@ function RomImporter:choose(version) self:startPath(path) return end + if importPendingRom(self) then return end local okKit, Kit = pcall(require, "src.ui.kit.Kit") if okKit and Kit.FileBrowser then self._padCursorActive = false @@ -2773,17 +2782,6 @@ function RomImporter:choose(version) }) return end - -- Handheld Linux (Anbernic stock OS / PortMaster) rarely has zenity or - -- kdialog. Fall back to the same "drop a .gb/.gbc next to the game" scan - -- used on Android, which works when the game is launched as an unpacked - -- directory (see build-rg34xxsp.sh). Narrowed to the chosen version: with - -- four dumps in the folder the unnarrowed scan answered in listing order, - -- so Choose Red imported and decoded Blue (#1274). - local name, data = findPendingRom(self.ready, self.chooseVersion) - if name then - self:startData(data, name) - return - end if love.system.getOS() == "Linux" then local where = love.filesystem.getSourceBaseDirectory and love.filesystem.getSourceBaseDirectory() @@ -3167,6 +3165,7 @@ end function RomImporter:prepareOverlayHandoff() resetPointerCursor(self) self._padCursorActive = false + self:_forgetActiveSkin() -- Avoid requiring LauncherView from headless unit tests (no luautf8). In -- a real session draw() has already loaded it, so detach runs normally. if self._flex and package.loaded["src.import.LauncherView"] then @@ -3888,10 +3887,17 @@ function RomImporter:_ensureSkins(force) return out end +function RomImporter:_forgetActiveSkin() + self._activeSkinCache = nil +end + function RomImporter:_activeSkin() - local opts = require("src.core.SaveData").loadOptions() - local tc = type(opts.touchControls) == "table" and opts.touchControls or {} - return tc.enabled == false and nil or tc.skin + if self._activeSkinCache == nil then + local opts = require("src.core.SaveData").loadOptions() + local tc = type(opts.touchControls) == "table" and opts.touchControls or {} + self._activeSkinCache = (tc.enabled ~= false and tc.skin) or false + end + return self._activeSkinCache or nil end function RomImporter:_useSkin(id) @@ -3902,6 +3908,7 @@ function RomImporter:_useSkin(id) tc.skin = id opts.touchControls = tc SaveData.saveOptions(opts) + self:_forgetActiveSkin() self._skinNotice = { ok = true, text = id and ("Now using " .. id) or "Now using the built-in pad", @@ -3919,6 +3926,7 @@ function RomImporter:_disableSkins() tc.enabled, tc.skin = true, nil opts.touchControls = tc SaveData.saveOptions(opts) + self:_forgetActiveSkin() self._skinNotice = { ok = true, text = "Skins are off. Mobile will use the built-in pad when needed." } end @@ -4204,11 +4212,21 @@ end function RomImporter:_syncNoteDownload(row) local version = type(row) == "table" and row.version if type(version) ~= "string" or not GameVersion.VERSIONS[version] then return end - self:_refreshSlots(version) - if row.created then self.slotScroll[version] = math.huge end + local cart = type(row.cart) == "string" and row.cart ~= "" and row.cart or nil + local scope = cart and (CART_SCOPE .. cart) or version + self:_refreshSlots(scope) + if row.created then self.slotScroll[scope] = math.huge end local from = row.device and (" from " .. tostring(row.device)) or "" - self.saveNotice[version] = { ok = true, - text = ("Downloaded a save%s into %s."):format(from, tostring(row.slot)) } + local what + if cart then + local ok, found = pcall(self._cartById, self, version, cart) + what = (ok and type(found) == "table" and found.title) or cart + end + self.saveNotice[scope] = { ok = true, + text = what + and ("Downloaded a %s save%s into %s."):format( + tostring(what), from, tostring(row.slot)) + or ("Downloaded a save%s into %s."):format(from, tostring(row.slot)) } end function RomImporter:_openSync() @@ -4460,6 +4478,7 @@ function RomImporter:_closeSettings() model.save() end self._settings = nil + self:_forgetActiveSkin() end function RomImporter:_safeModeEnabled() @@ -5625,6 +5644,13 @@ function RomImporter:modCartPlan(listed) return id, report, version end +function RomImporter:cartPinsNote(cartId, cartReport) + if not cartId then return nil end + return Strings( + "%s decides which mods run. Switch its pins one at a time, or pick the base game above.", + (cartReport and cartReport.title) or cartId) +end + local function missingPinRow(imp, id, pin) local version = type(pin) == "table" and pin.version or nil return { id = id, name = id, version = version, badge = "MOD", @@ -5915,9 +5941,7 @@ function RomImporter:_setAllMods(want, confirmed) end local cartId, cartReport = self:modCartPlan() if cartId then - self.modNotice = { ok = false, text = Strings( - "%s decides which mods run. Switch its pins one at a time, or pick the base game above.", - (cartReport and cartReport.title) or cartId) } + self.modNotice = { ok = false, text = self:cartPinsNote(cartId, cartReport) } return end local LauncherMods = require("src.mods.LauncherMods") @@ -6607,9 +6631,15 @@ function RomImporter:_updateAllRows() return rows end -function RomImporter:pressUpdateAllMods() +function RomImporter:updateAllAvailable() + if self.safeMode then return false end if self._updateAll or self._modInstall or self._cartInstall or self._cartFill then return false end + return true +end + +function RomImporter:pressUpdateAllMods() + if not self:updateAllAvailable() then return false end if not Platform.canFetchRemote() then self.modNotice = { ok = false, text = "Remote mod download is unavailable on this platform. Install a mod .zip from storage instead." } diff --git a/src/mods/Gen2Compat.lua b/src/mods/Gen2Compat.lua index a5ed0145..008b9520 100644 --- a/src/mods/Gen2Compat.lua +++ b/src/mods/Gen2Compat.lua @@ -121,6 +121,21 @@ local DATA_UNBACKED = { } local dataProxies = setmetatable({}, { __mode = "kv" }) +local textPlain = setmetatable({}, { __mode = "kv" }) + +-- ../pokecrystal/home/text.asm:548 PromptText, :566 DoneText +local function plainText(text) + if type(text) ~= "table" then return text end + local hit = textPlain[text] + if hit then return hit end + local CommonText = rawRequire("src.core.gen2.CommonText") + hit = {} + for key, value in pairs(text) do + hit[key] = (type(value) == "string") and CommonText.plain(value) or value + end + textPlain[text] = hit + return hit +end local function dataProxy(data) if not data then return nil end @@ -129,7 +144,10 @@ local function dataProxy(data) hit = setmetatable({}, { __index = function(_, key) local renamed = DATA_RENAMES[key] - if renamed then return data[renamed] end + if renamed then + if key == "text" then return plainText(data[renamed]) end + return data[renamed] + end local why = DATA_UNBACKED[key] if why then warnOnce("data." .. key, "[%s] game.data.%s is Gen 1 only: %s", diff --git a/src/mods/Schemas.lua b/src/mods/Schemas.lua index 64c3bb83..03340bd5 100644 --- a/src/mods/Schemas.lua +++ b/src/mods/Schemas.lua @@ -1098,7 +1098,11 @@ R.encounters = { trees = f.map(f.str, f.str), rocks = f.map(f.str, f.str), treeSets = f.map(f.str, f.rec{ common = f.list(gen2TreeSlot), - rare = f.list(gen2TreeSlot) }), + rare = f.opt(f.list(gen2TreeSlot)) }), + -- ../pokecrystal/data/wild/treemons_asleep.asm:3 AsleepTreeMonsNite + treeMonsAsleep = f.opt(f.rec{ MORN = f.list(f.id("pokemon")), + DAY = f.list(f.id("pokemon")), + NITE = f.list(f.id("pokemon")) }), -- the Bug-Catching Contest pool (min/max level, not one level per slot) bugContest = f.list(f.rec{ species = f.id("pokemon"), min = f.int(1), max = f.int(1), diff --git a/src/online/TeamPick.lua b/src/online/TeamPick.lua index 6a549438..ebbaefa9 100644 --- a/src/online/TeamPick.lua +++ b/src/online/TeamPick.lua @@ -17,16 +17,8 @@ local function readCartSource(cartId, slotId) if row.id == slotId then registered = row.exists end end if not registered then return nil, "no save in that slot" end - local fs = SaveData.persistenceFs() - local main = "saves/cart_" .. cartId .. "/" .. slotId .. ".lua" - for _, name in ipairs({ main, main .. ".tmp", main .. ".bak" }) do - if fs.getInfo(name) then - local body = fs.read(name) - if type(body) == "string" and body ~= "" and SaveData.decode(body) then - return body - end - end - end + local body = SaveData.readCartSlotSource(cartId, slotId) + if type(body) == "string" and body ~= "" then return body end return nil, "no save in that slot" end diff --git a/src/render/GbcPalette.lua b/src/render/GbcPalette.lua index 1cf0f3f0..37ac59b2 100644 --- a/src/render/GbcPalette.lua +++ b/src/render/GbcPalette.lua @@ -399,6 +399,8 @@ function GbcPalette.remapTable(bgPalettes, byte, objPalettes, ramp, objRamped) local resolved = GbcPalette.resolve(colors) add(resolved, forcedBg or GbcPalette.remap(resolved, byte)) end + -- ../pokecrystal/engine/battle/battle_transition.asm:100-122 + if forced then add(forced, forcedBg) end for index, colors in ipairs(objPalettes or {}) do local resolved = GbcPalette.resolve(colors) if forced and objRamped and objRamped[index] then diff --git a/src/render/MonAnim.lua b/src/render/MonAnim.lua index 15c8b52e..e3e6c16d 100644 --- a/src/render/MonAnim.lua +++ b/src/render/MonAnim.lua @@ -46,24 +46,33 @@ function MonAnim.duration(param, speed) return (scaled + param) % 256 end --- The PokeAnims programs less their cry commands; "wait" is SetWait's 18. -- ../pokecrystal/engine/gfx/pic_animation.asm:69-77, :150-164 local SCENES = { battle = { "setup", "play" }, battleSlow = { "setup2", "play" }, menu = { "setup", "play", "wait", "idle", "play" }, + -- ../pokecrystal/engine/gfx/pic_animation.asm:72 + trade = { "idle", "play2", "idle", "play", "wait", "cry", "setup", "play" }, + -- ../pokecrystal/engine/gfx/pic_animation.asm:73 + evolve = { "idle", "play", "wait", "cryNoWait", "setup", "play" }, + -- ../pokecrystal/engine/gfx/pic_animation.asm:74 + hatch = { "idle", "play", "cryNoWait", "setup", "play", "wait", "idle", + "play" }, + -- ../pokecrystal/engine/gfx/pic_animation.asm:75 + hof = { "setup", "play", "wait", "idle", "play" }, } MonAnim.SCENE_WAIT = 18 function MonAnim.scenes() return SCENES end -function MonAnim.new(data, scene) +function MonAnim.new(data, scene, onCry) local steps = SCENES[scene or "battle"] if not (data and steps and data.play and #data.play > 0) then return nil end return setmetatable({ data = data, steps = steps, + onCry = onCry, step = 1, frame = 0, speed = 0, @@ -154,9 +163,15 @@ function MonAnim:update() end return end + if step == "cry" or step == "cryNoWait" then + -- ../pokecrystal/engine/gfx/pic_animation.asm:230-244 + if self.onCry then self.onCry(step) end + self.step = self.step + 1 + return + end if self:runScript() then - -- PokeAnim_Play redraws the base picture as the script ends (:196-205). - self.frame = 0 + -- ../pokecrystal/engine/gfx/pic_animation.asm:196-215 + if step ~= "play2" then self.frame = 0 end self.step = self.step + 1 end end diff --git a/src/render/MonAnimView.lua b/src/render/MonAnimView.lua new file mode 100644 index 00000000..1962d2b3 --- /dev/null +++ b/src/render/MonAnimView.lua @@ -0,0 +1,59 @@ +-- ../pokecrystal/engine/gfx/pic_animation.asm:79-89 AnimateFrontpic + +local MonAnim = require("src.render.MonAnim") +local Unown = require("src.core.gen2.Unown") + +local MonAnimView = {} +MonAnimView.__index = MonAnimView + +-- ../pokecrystal/engine/events/halloffame.asm:225-238 +function MonAnimView.animData(def, mon) + if not def then return nil end + local data = def.anim + if mon and mon.species == Unown.SPECIES and def.letters then + local entry = def.letters[Unown.name(Unown.monLetter(mon))] + if entry and entry.anim then data = entry.anim end + end + return data +end + +function MonAnimView.start(def, mon, scene, imageFn, onCry) + local data = MonAnimView.animData(def, mon) + if not (data and data.tiles and imageFn) then return nil end + local sheet = imageFn(data.sheet) + if not sheet then return nil end + local runner = MonAnim.new(data, scene, onCry) + if not runner then return nil end + return setmetatable({ + runner = runner, + sheet = sheet, + size = data.tiles * 8, + quads = {}, + }, MonAnimView) +end + +function MonAnimView:step() + self.runner:update() + return self.runner:finished() +end + +function MonAnimView:finished() return self.runner:finished() end + +function MonAnimView:currentFrame() return self.runner:currentFrame() end + +-- ../pokecrystal/engine/gfx/pic_animation.asm:431-435 +function MonAnimView:frame() + local frame = self.runner:currentFrame() + if frame <= 0 then return nil end + local quad = self.quads[frame] + if not quad then + local w, h = self.sheet:getDimensions() + if (frame + 1) * self.size > h then return nil end + quad = love.graphics.newQuad(0, frame * self.size, self.size, self.size, + w, h) + self.quads[frame] = quad + end + return self.sheet, quad, self.size +end + +return MonAnimView diff --git a/src/render/ShaderFX.lua b/src/render/ShaderFX.lua index 4a4a7739..b4b6110d 100644 --- a/src/render/ShaderFX.lua +++ b/src/render/ShaderFX.lua @@ -614,36 +614,9 @@ local function runPass(state, i, pass, outputs, frameSource, lutByName, viewport -- Compiled once per (state, pass): a pass's GLSL and manifest depend only on -- the preset, never on per-frame input. fragManifest is cached alongside it. - local cached = state.shaderCache[i] - local shader, fragManifest - if cached then - shader, fragManifest = cached.shader, cached.fragManifest - else - local fixedFragBody - fixedFragBody, fragManifest = Fixup.fragment(pass.fragment) - local fixedVert, vertManifest = Fixup.vertex(pass.vertex) - assert(#fragManifest == #vertManifest, ("pass%d: PUSH struct member count differs"):format(i)) - - local validateErrs = {} - local isEs = defaultEs() - for headIdx, head in ipairs(Fixup.PREC_HEADS) do - local fixedFrag = head .. fixedFragBody - local okShader, err = love.graphics.validateShader(isEs, fixedFrag, fixedVert) - if okShader then - shader = love.graphics.newShader(fixedFrag, fixedVert) - break - else - validateErrs[#validateErrs + 1] = ("variant %d: %s"):format(headIdx, tostring(err)) - end - end - if not shader then - require("src.core.Logger").error("ShaderFX: pass%d shader failed to validate -- %s", - i, table.concat(validateErrs, " | ")) - end - assert(shader, ("pass%d: no PREC_HEADS variant validated (%s)") - :format(i, table.concat(validateErrs, " | "))) - state.shaderCache[i] = { shader = shader, fragManifest = fragManifest } - end + local cached = assert(state.shaderCache[i], + ("pass%d: shader was not built at activate"):format(i)) + local shader, fragManifest = cached.shader, cached.fragManifest for _, s in ipairs(pass.samplers) do local img @@ -829,12 +802,87 @@ ShaderFX.OPTION_KEY = { main = "shaderfx", secondary = "shaderfxSecondary" } local slots = { main = {}, secondary = {} } -- slots[s] = { state=, entry= } local autoActivateTried = false -- see applyOptions()/tryAutoActivateFromEnv() below +ShaderFX.ERROR_LOG_REL = "shaderfx-error.log" +local MAX_ERRORS = 8 +ShaderFX._lastErrors = {} +local lastCropError = nil + +local function rendererInfo() + if not (love.graphics and love.graphics.getRendererInfo) then return "?" end + local ok, name, version, vendor, device = pcall(love.graphics.getRendererInfo) + if not ok then return "?" end + return table.concat({ tostring(name), tostring(version), tostring(vendor), tostring(device) }, " | ") +end + +local function glsl3Supported() + if not (love.graphics and love.graphics.getSupported) then return "?" end + local ok, t = pcall(love.graphics.getSupported) + if not ok or type(t) ~= "table" then return "?" end + return tostring(t.glsl3) +end + +function ShaderFX.recordError(presetName, message) + local line = ("%s: %s"):format(tostring(presetName), tostring(message)) + local errs = ShaderFX._lastErrors + errs[#errs + 1] = line + while #errs > MAX_ERRORS do table.remove(errs, 1) end + require("src.core.Logger").error("ShaderFX: %s", line) + if love.filesystem and love.filesystem.write then + local head = ("es=%s glsl3=%s renderer=%s\n\n"):format( + tostring(defaultEs()), glsl3Supported(), rendererInfo()) + pcall(love.filesystem.write, ShaderFX.ERROR_LOG_REL, head .. table.concat(errs, "\n\n") .. "\n") + end + return line +end + +function ShaderFX.lastError() + local errs = ShaderFX._lastErrors + return errs[#errs] +end + +local function buildPassShader(i, pass) + local okF, fixedFragBody, fragManifest = pcall(Fixup.fragment, pass.fragment) + if not okF then return nil, ("pass%d: fragment fixup: %s"):format(i, tostring(fixedFragBody)) end + local okV, fixedVert, vertManifest = pcall(Fixup.vertex, pass.vertex) + if not okV then return nil, ("pass%d: vertex fixup: %s"):format(i, tostring(fixedVert)) end + if #fragManifest ~= #vertManifest then + return nil, ("pass%d: PUSH struct member count differs"):format(i) + end + if not (love.graphics and love.graphics.newShader) then + return nil, ("pass%d: love.graphics.newShader unavailable"):format(i) + end + local errs, isEs = {}, defaultEs() + for headIdx, head in ipairs(Fixup.PREC_HEADS) do + local fixedFrag = head .. fixedFragBody + local okValidate, verr = true, nil + if love.graphics.validateShader then + okValidate, verr = love.graphics.validateShader(isEs, fixedFrag, fixedVert) + end + if okValidate then + local okNew, shaderOrErr = pcall(love.graphics.newShader, fixedFrag, fixedVert) + if okNew and shaderOrErr then return shaderOrErr, fragManifest end + errs[#errs + 1] = ("variant %d newShader: %s"):format(headIdx, tostring(shaderOrErr)) + else + errs[#errs + 1] = ("variant %d validate: %s"):format(headIdx, tostring(verr)) + end + end + return nil, ("pass%d: %s"):format(i, table.concat(errs, " | ")) +end + -- Loads `entry`'s cached artifact into `slot` and makes it that slot's active -- preset. `paramOverrides` layers a player's pragma edits over ALL_DEFAULTS. function ShaderFX.activate(slot, entry, paramOverrides) assert(slots[slot], "ShaderFX.activate: unknown slot " .. tostring(slot)) local state, err = ShaderFX.load(entry) if not state then return false, err end + for i, pass in ipairs(state.preset.passes) do + local shader, manifestOrErr = buildPassShader(i - 1, pass) + if not shader then + ShaderFX.recordError(entry.name, manifestOrErr) + return false, manifestOrErr + end + state.shaderCache[i - 1] = { shader = shader, fragManifest = manifestOrErr } + end if paramOverrides then for id, value in pairs(paramOverrides) do state.ALL_DEFAULTS[id] = value end end @@ -851,6 +899,7 @@ end -- `slot` nil deactivates both. function ShaderFX.deactivate(slot) + lastCropError = nil if slot then slots[slot].state, slots[slot].entry = nil, nil else @@ -1024,35 +1073,41 @@ function ShaderFX.render(canvas, rect, source, dpiX, dpiY) secondary = slots.secondary.state and slots.secondary.state.yawTwist, } - local ok, chainOut, chainPreset = pcall(function() - local frameSource = cropToGbSource(canvas, rect, source.w, source.h, scale) + local chainOut, chainPreset + local okCrop, frameSource = pcall(cropToGbSource, canvas, rect, source.w, source.h, scale) + if okCrop then + lastCropError = nil -- exposed for tests only (gold_shaderfx_menu_black_crop_test.lua) ShaderFX._lastCrop = frameSource local dims = { w = frameSource:getWidth(), h = frameSource:getHeight() } - local img, preset = frameSource, nil - if slots.main.state then - img = ShaderFX.runChain(slots.main.state, img, slots.main.state.luts, viewport, dims) - dims = { w = img:getWidth(), h = img:getHeight() } - preset = slots.main.state.preset + local img = frameSource + for _, slotName in ipairs(ShaderFX.SLOTS) do + local s = slots[slotName] + if s.state then + local okChain, out = pcall(ShaderFX.runChain, s.state, img, s.state.luts, viewport, dims) + if okChain and out then + img, chainOut, chainPreset = out, out, s.state.preset + dims = { w = out:getWidth(), h = out:getHeight() } + else + ShaderFX.recordError(s.entry and s.entry.name, + ("%s chain failed, preset deactivated: %s"):format(slotName, + tostring(okChain and "no output" or out))) + s.state, s.entry = nil, nil + end + end end - if slots.secondary.state then - img = ShaderFX.runChain(slots.secondary.state, img, slots.secondary.state.luts, viewport, dims) - dims = { w = img:getWidth(), h = img:getHeight() } - preset = slots.secondary.state.preset + else + local msg = tostring(frameSource) + if msg ~= lastCropError then + lastCropError = msg + ShaderFX.recordError("crop", msg) end - return img, preset - end) - - if not ok or not chainOut then - require("src.core.Logger").error( - "ShaderFX.render chain failed ok=%s chainOut=%s err=%s", - tostring(ok), tostring(chainOut), tostring(not ok and chainOut or nil)) end love.graphics.setColor(1, 1, 1, 1) love.graphics.setBlendMode("replace") love.graphics.draw(canvas, 0, 0) - if ok and chainOut then + if chainOut then -- A chain's final pass need not land on the viewport size (most presets end -- at scale_type="source"), so stretch, using that pass's own filter. local cw, ch = chainOut:getWidth(), chainOut:getHeight() @@ -1062,9 +1117,6 @@ function ShaderFX.render(canvas, rect, source, dpiX, dpiY) love.graphics.setColor(1, 1, 1, 1) love.graphics.setBlendMode("alpha") love.graphics.draw(chainOut, uRect.x, uRect.y, 0, uRect.w / cw, uRect.h / ch) - elseif not ok then - require("src.core.Logger").error("ShaderFX.render: chain failed, showing unprocessed frame: %s", - tostring(chainOut)) end love.graphics.setBlendMode("alpha") -- Test seam; set only after the real chain ran. diff --git a/src/render/ShaderFixup.lua b/src/render/ShaderFixup.lua index ff0293db..c3c05510 100644 --- a/src/render/ShaderFixup.lua +++ b/src/render/ShaderFixup.lua @@ -213,6 +213,13 @@ function Fixup.countUniformSlots(src) return total end +local function rewriteGpuShader4(src) + src = src:gsub("#extension%s+GL_EXT_gpu_shader4[^\n]*\n?", "") + src = src:gsub("%f[%w_]uint%f[^%w_]", "int") + return src +end +Fixup.rewriteGpuShader4 = rewriteGpuShader4 + -- LIBRA_UBO_* (the uniform-buffer block) has the same -- LOVE-can't-address-a-struct-member problem as LIBRA_PUSH_*, so it is -- flattened the same way, with MVP alone special-cased. Deleting the block @@ -229,6 +236,7 @@ local UBO_SPECIAL_MEMBERS = { MVP = "transform_projection" } function Fixup.fragment(src) src = stripVersion(src) + src = rewriteGpuShader4(src) src = rewriteArrayLiterals(src) src = rewriteIntegerModulo(src) local pushManifest, uboManifest @@ -238,14 +246,14 @@ function Fixup.fragment(src) for _, entry in ipairs(uboManifest) do manifest[#manifest + 1] = entry end src = guardPrecision(src) src = src:gsub("void%s+main%s*%(%s*%)", - "vec4 effect(EFFECT_PREC vec4 love_UnusedColor, Image love_UnusedTex, " + "EFFECT_PREC vec4 effect(EFFECT_PREC vec4 love_UnusedColor, Image love_UnusedTex, " .. "EFFECT_PREC vec2 love_UnusedTc, EFFECT_PREC vec2 love_UnusedSc)") -- gl_FragData isn't available in LOVE's effect() convention. Rewrite EVERY -- occurrence, whatever operator follows: real presets (ds-hybrid-sabr) write -- it once with `=` and later accumulate with `+=`. src = src:gsub("gl_FragData%[0%]", "gbFragColor") src = src:gsub( - "(vec4 effect%(EFFECT_PREC vec4 love_UnusedColor, Image love_UnusedTex, " + "(EFFECT_PREC vec4 effect%(EFFECT_PREC vec4 love_UnusedColor, Image love_UnusedTex, " .. "EFFECT_PREC vec2 love_UnusedTc, EFFECT_PREC vec2 love_UnusedSc%)%s*\n{)", "%1\n vec4 gbFragColor;") -- a bare early-exit `return;` needs a value now -- gbFragColor already @@ -259,6 +267,7 @@ end function Fixup.vertex(src) src = stripVersion(src) + src = rewriteGpuShader4(src) src = rewriteArrayLiterals(src) src = rewriteIntegerModulo(src) local pushManifest, uboManifest diff --git a/src/render/TextBox.lua b/src/render/TextBox.lua index a4f161d2..52c2e0d7 100644 --- a/src/render/TextBox.lua +++ b/src/render/TextBox.lua @@ -62,6 +62,14 @@ local function stripPauses(text) return table.concat(out), marks end +-- ../pokecrystal/home/text.asm:548 PromptText, :566 DoneText +function TextBox.ending(text) + if type(text) ~= "string" then return nil end + if text:find("{PROMPT}%s*$") then return "prompt" end + if text:find("{DONE}%s*$") then return "done" end + return nil +end + -- glyph offsets into the whole text -> [page][line][char] local function mapPauses(pages, marks) local at, acc, mi = {}, 0, 1 @@ -123,6 +131,14 @@ function TextBox.new(game, text, onDone, opts) self.preSound = opts and opts.preSound -- pokegold engine/overworld/scripting.asm:485 WaitSFX self.sfxWait = opts and opts.sfxWait + -- ../pokecrystal/home/joypad.asm:302 WaitButton + self.waitButton = opts and opts.waitButton + local ending = TextBox.ending(text) + if ending == "prompt" then + self.waitButton = false + elseif ending == "done" and self.waitButton == nil then + self.waitButton = true + end -- opts.instant: put the LAST page up already typed, with no typewriter and -- no page waits. A `yesorno` follows a `writetext` that has already been -- read, so re-typing the line under the YES/NO box would be wrong -- the @@ -232,6 +248,10 @@ TextBox.TOKENS = { if arg == "wBoxMonNicks" then return game.boxMonNicks end return nil end, + -- ../pokecrystal/home/text.asm:566 DoneText + DONE = function() return nil end, + -- ../pokecrystal/home/text.asm:548 PromptText + PROMPT = function() return nil end, } function TextBox.registerInto(registry, _, owner) @@ -351,6 +371,8 @@ end function TextBox:arrowVisible() if self.sfxWait then return false end if self.waiting then return true end + -- ../pokecrystal/home/text.asm:566 DoneText + if self.waitButton then return false end return not not (self.done and not self.choice and (not self.auto or (self.auto.promptFirst and not self.autoPrompted)) @@ -498,7 +520,9 @@ function TextBox:update(dt) if self:sfxHeld() then return end if input:wasPressed("a") or input:wasPressed("b") then -- home/joypad.asm:292 - require("src.core.Sound").playPress(self.game.data) + if not self.waitButton then + require("src.core.Sound").playPress(self.game.data) + end self.game.stack:pop() if self.onDone then self.onDone() end end diff --git a/src/save_convert/GenSave.lua b/src/save_convert/GenSave.lua index f4f44c6c..4e7ae242 100644 --- a/src/save_convert/GenSave.lua +++ b/src/save_convert/GenSave.lua @@ -386,7 +386,10 @@ local EXTRA_FLAG_BITS = { } -- port-local name -> the wEventFlags name it means (#396) -local FLAG_ALIAS = { EVENT_RECEIVED_BIKE_VOUCHER = "EVENT_GOT_BIKE_VOUCHER" } +local FLAG_ALIAS = { + EVENT_RECEIVED_BIKE_VOUCHER = "EVENT_GOT_BIKE_VOUCHER", + EVENT_GOT_HM_FLASH = "EVENT_GOT_HM05", +} -- scripts/OaksLab.asm:797-825 local PLAYER_TO_RIVAL = { diff --git a/src/script/Commands.lua b/src/script/Commands.lua index 2b1cfde2..8ecf2b81 100644 --- a/src/script/Commands.lua +++ b/src/script/Commands.lua @@ -816,6 +816,11 @@ function Commands.take_money(ctx, amount) ctx.save.money = math.max(0, (ctx.save.money or 0) - (amount or 0)) end +-- take_coins : SubBCDPredef (engine/events/prize_menu.asm:238-243) +function Commands.take_coins(ctx, amount) + ctx.save.coins = math.max(0, (ctx.save.coins or 0) - (amount or 0)) +end + -- Point LAST_MAP exits at an outdoor door (pokered wLastMap). Keeps the -- live overworld memory in sync so a scripted home warp from the HoF PC -- does not leave Red's house mats aimed at Indigo Plateau (#103). diff --git a/src/script/gen2/Vm.lua b/src/script/gen2/Vm.lua index 0e81d855..f585e981 100644 --- a/src/script/gen2/Vm.lua +++ b/src/script/gen2/Vm.lua @@ -1699,16 +1699,8 @@ local function runCmd(self, cmd, op) self:noteUnknownOp(op) -- ---- commands with no engine behind them yet --------------------------- elseif op == "deactivatefacing" then - -- Script_deactivatefacing: wScriptDelay = the byte (left ALONE when the - -- byte is 0, the same `and a / jr z` idiom Script_pause uses), then - -- wScriptMode = SCRIPT_WAIT and StopScript. WaitScript ticks that delay - -- down one per frame and calls UnfreezeAllObjects before reading again, - -- so what the command DOES is hold the script for N frames with the map's - -- objects released. This port never freezes them in the first place - -- (World:step keeps updatePeople running while the VM is busy), so the - -- wait is the whole of it and it goes through the same waitFrames `pause` - -- uses. - self:waitFrames(cmd.frames or arg1(cmd) or 0) + -- ../pokecrystal/engine/overworld/scripting.asm:2237, :30 WaitScript + self:pauseFrames(cmd.frames or arg1(cmd) or 0) elseif op == "writeunusedbyte" then -- Script_writeunusedbyte stores its operand in wUnusedScriptByte, and -- nothing in the ROM ever reads it back: the label is pokegold's own name @@ -2432,6 +2424,11 @@ function Vm:textStays() return self.nextOp == "yesorno" end +-- ../pokecrystal/engine/overworld/scripting.asm:374 Script_promptbutton +function Vm:textArrows() + return self.nextOp == "promptbutton" +end + -- `stay` is also settable per command, for the hand-ported scripts that hold -- the box open over something that is not `yesorno`: HiddenItems' -- FindItemInBallScript prints the found line, plays SFX_ITEM and then holds on @@ -2458,6 +2455,7 @@ function Vm:showRaw(body, stay, hold, sfxWait) hold = hold, -- pokegold engine/overworld/scripting.asm:485 WaitSFX sfxWait = sfxWait and true or nil, + arrows = self:textArrows(), }) end end @@ -2477,7 +2475,8 @@ function Vm:showText(textKey) body = body:gsub("{STRBUF}", self.stringBuffer) end if self.showTextFn then - coroutine.yield({ kind = "text", text = body, stay = self:textStays() }) + coroutine.yield({ kind = "text", text = body, stay = self:textStays(), + arrows = self:textArrows() }) end end @@ -2496,13 +2495,7 @@ end -- itself (engine/overworld/scripting.asm:2110-2124, and ShowEmoteScript's -- `pause 0` reading the wScriptDelay Script_showemote just wrote). Its inner -- loop is `ld c, 2 / call DelayFrames` ONCE PER UNIT, so the hardware holds --- two frames per operand byte: `pause 60` is 120 frames. --- --- The factor lives here and not in waitFrames because the other two waiters do --- not share the routine. `deactivatefacing` hands the byte to WaitScript, --- which does one `dec [wScriptDelay]` per frame (:30-42), and `earthquake` --- spends it as a step_sleep, which StepFunction_Sleep also decrements once per --- frame -- both are already 1:1 and doubling them would be a new bug. +-- HandleMap iteration -- MaxOverworldDelay 2 (engine/overworld/events.asm:177). function Vm:pauseFrames(n) self:waitFrames(Vm.pauseLength(n)) end @@ -2666,7 +2659,7 @@ function Vm:resume(resumeValue) if req and req.kind == "text" and self.showTextFn then self.showTextFn(req.text, function() self:resume() - end, req.stay, req.hold, req.sfxWait) + end, req.stay, req.hold, req.sfxWait, req.arrows) elseif req and req.kind == "wait" then self.waitLeft = req.frames or 0 elseif req and req.kind == "waitbutton" then diff --git a/src/sync/SyncEngine.lua b/src/sync/SyncEngine.lua index c8b25db5..6cce8d9e 100644 --- a/src/sync/SyncEngine.lua +++ b/src/sync/SyncEngine.lua @@ -31,12 +31,90 @@ local function gameVersions() return require("src.core.GameVersion").ORDER end -local function slotForPlaythrough(options, version, playthroughId) - local byVersion = options.playthroughIds and options.playthroughIds[version] - for slotId, id in pairs(byVersion or {}) do - if id == playthroughId then return slotId end +local CART_PREFIX = "cart_" + +local function cartOfScope(key) + if type(key) ~= "string" then return nil end + if key:sub(1, #CART_PREFIX) ~= CART_PREFIX then return nil end + local id = key:sub(#CART_PREFIX + 1) + if id == "" then return nil end + return id +end + +local function safeCartId(id) + if type(id) ~= "string" or id == "" or #id > 64 then return nil end + if not id:match("^[%w_%-]+$") then return nil end + return id +end + +local function blobCart(save) + local meta = type(save) == "table" and save.meta or nil + return safeCartId(type(meta) == "table" and meta.cartId or nil) +end + +local function cartInstalled(options, cartId) + local reg = type(options) == "table" and options.carts or nil + if type(reg) == "table" and type(reg[cartId]) == "table" then return true end + local ok, CartStore = pcall(require, "src.carts.CartStore") + if not ok or type(CartStore) ~= "table" then return false end + local fs = love and love.filesystem + if type(fs) ~= "table" or type(fs.getInfo) ~= "function" then return false end + local okInfo, info = pcall(fs.getInfo, CartStore.fileFor(cartId)) + return okInfo and info ~= nil +end + +local function syncScopes() + local SaveData = saveApi() + local out = {} + for _, version in ipairs(gameVersions()) do + out[#out + 1] = { key = version, version = version } end - return nil + local ok, ids = pcall(SaveData.cartsWithSlots) + if not ok or type(ids) ~= "table" then return out end + local okOpts, options = pcall(SaveData.loadOptions) + local reg = (okOpts and type(options) == "table" + and type(options.carts) == "table") and options.carts or {} + for _, id in ipairs(ids) do + local row = type(reg[id]) == "table" and reg[id] or nil + out[#out + 1] = { key = CART_PREFIX .. id, cart = id, + version = row and row.base or nil } + end + return out +end + +local function wireVersion(save, scope) + local GameVersion = require("src.core.GameVersion") + if not scope.cart and type(scope.version) == "string" + and GameVersion.VERSIONS[scope.version] then + return scope.version + end + local v = type(save) == "table" and save.version or nil + if type(v) == "string" and GameVersion.VERSIONS[v] then return v end + if type(scope.version) == "string" and GameVersion.VERSIONS[scope.version] then + return scope.version + end + return GameVersion.get() +end + +local function slotForPlaythrough(options, version, playthroughId) + local root = (type(options) == "table" and type(options.playthroughIds) == "table") + and options.playthroughIds or {} + local byVersion = type(root[version]) == "table" and root[version] or nil + for slotId, id in pairs(byVersion or {}) do + if id == playthroughId then return version, slotId end + end + local bestKey, bestSlot + for key, byScope in pairs(root) do + if key ~= version and cartOfScope(key) and type(byScope) == "table" then + for slotId, id in pairs(byScope) do + if id == playthroughId and slotId ~= "legacy" + and (bestKey == nil or key < bestKey) then + bestKey, bestSlot = key, slotId + end + end + end + end + return bestKey, bestSlot end function SyncEngine.defaultSaves() @@ -44,18 +122,31 @@ function SyncEngine.defaultSaves() list = function() local SaveData = saveApi() local out = {} - for _, version in ipairs(gameVersions()) do - for _, slot in ipairs(SaveData.listSlots(version)) do + for _, scope in ipairs(syncScopes()) do + local slots = scope.cart and SaveData.listCartSlots(scope.cart) + or SaveData.listSlots(scope.version) + for _, slot in ipairs(slots) do if slot.exists then - local source = SaveData.readSlotSource(version, slot.id) + local source + if scope.cart then + source = SaveData.readCartSlotSource(scope.cart, slot.id) + else + source = SaveData.readSlotSource(scope.version, slot.id) + end local save = source and SaveData.decode(source) if type(save) == "table" then local meta = type(save.meta) == "table" and save.meta or {} - local id = SaveData.slotPlaythroughId(version, slot.id, save) + local id + if scope.cart then + id = SaveData.cartSlotPlaythroughId(scope.cart, slot.id, save) + else + id = SaveData.slotPlaythroughId(scope.version, slot.id, save) + end if id then local name, summary = SaveData.slotSummary(save) out[#out + 1] = { - version = version, + version = wireVersion(save, scope), + cart = scope.cart, slot = slot.id, playthroughId = id, blob = source, @@ -89,27 +180,49 @@ function SyncEngine.defaultSaves() if type(save) ~= "table" then return nil, "the downloaded save is unreadable" end save.version = save.version or version local options = SaveData.loadOptions() - local slotId, created + local scopeKey, slotId, created if mode == "new" then save.meta = type(save.meta) == "table" and save.meta or {} save.meta.playthroughId = SaveData.newPlaythroughId() else - slotId = slotForPlaythrough(options, version, playthroughId) + scopeKey, slotId = slotForPlaythrough(options, version, playthroughId) end if not slotId then - slotId = SaveData.createSlot(version) + local cartId = blobCart(save) + if cartId then + if not cartInstalled(options, cartId) then + return false, ("install the \"%s\" cart to receive its save") + :format(cartId) + end + scopeKey = CART_PREFIX .. cartId + else + scopeKey = version + end + end + local cart = cartOfScope(scopeKey) + if not slotId then + if cart then + slotId = SaveData.createCartSlot(cart) + else + slotId = SaveData.createSlot(version) + end if not slotId then return nil, "could not make a save slot" end created = true end - local ok, err = SaveData.writeSlot(version, slotId, save) + local ok, err + if cart then + ok, err = SaveData.writeCartSlot(cart, slotId, save) + else + ok, err = SaveData.writeSlot(version, slotId, save) + end if not ok then return nil, err or "could not write the save" end options = SaveData.loadOptions() options.playthroughIds = options.playthroughIds or {} - options.playthroughIds[version] = options.playthroughIds[version] or {} - options.playthroughIds[version][slotId] = + options.playthroughIds[scopeKey] = options.playthroughIds[scopeKey] or {} + options.playthroughIds[scopeKey][slotId] = save.meta and save.meta.playthroughId or playthroughId SaveData.saveOptions(options) - return slotId, created == true + return slotId, created == true, cart end, } end @@ -253,6 +366,9 @@ function SyncEngine:_finish() self.error = nil self.state.lastSyncAt = self.now() self.status = self:defaultStatus() + if type(self.skipped) == "table" and #self.skipped > 0 then + self.status = self.skipped[1] + end self:_persist() end @@ -470,6 +586,7 @@ function SyncEngine:syncNow() self.autoAt = self.clock + SyncEngine.AUTO_INTERVAL self.queue = {} self.conflicts = {} + self.skipped = nil self.state.pendingConflicts = {} self.phase = "checking" self.status = "Checking for changes..." @@ -616,7 +733,15 @@ function SyncEngine:_queueDownload(key, version, playthroughId, mode, knownRev) e:_fail("the server sent no save data") return end - local slotId, detail = e.saves.write(version, playthroughId, data.blob, mode) + local slotId, detail, cartId = + e.saves.write(version, playthroughId, data.blob, mode) + if slotId == false then + e.skipped = e.skipped or {} + e.skipped[#e.skipped + 1] = + tostring(detail or "this save was skipped") + if not e:busy() then e:_finish() end + return + end if not slotId then e:_fail(detail or "could not write the downloaded save") return @@ -630,6 +755,7 @@ function SyncEngine:_queueDownload(key, version, playthroughId, mode, knownRev) e.lastDownloads = e.lastDownloads or {} e.lastDownloads[#e.lastDownloads + 1] = { version = version, + cart = cartId, slot = slotId, created = created, device = type(meta.device) == "string" and meta.device ~= "" diff --git a/src/ui/BagMenu.lua b/src/ui/BagMenu.lua index 047ed011..f2a7a4af 100644 --- a/src/ui/BagMenu.lua +++ b/src/ui/BagMenu.lua @@ -376,7 +376,13 @@ local function vanillaUseOn(game, battle, id, target, list, moveIndex, picker) -- (engine/menus/party_menu.asm:289-293) showMessages(game, payload, function() local StatBox = require("src.battle.BattleState").StatBox - game.stack:push(StatBox.new(game, target, function() + local statBox + -- ../pokered/engine/pokemon/evos_moves.asm:126-128 + local function dropStatBox() + if statBox and game.stack:top() == statBox then game.stack:pop() end + statBox = nil + end + statBox = StatBox.new(game, target, function() local Experience = require("src.battle.Experience") local def = game.data.pokemon[target.species] local moves = Experience.movesLearnedAt(def, extra.leveledTo) @@ -391,9 +397,12 @@ local function vanillaUseOn(game, battle, id, target, list, moveIndex, picker) -- the party menu stays up through TryEvolvingMon and only -- comes down at RemoveUsedItem (item_effects.asm:1392-1418) if evoTo then - Evolution.evolve(game, target, evoTo, closePicker, - evo and evo.method) + Evolution.evolve(game, target, evoTo, function() + dropStatBox() + closePicker() + end, evo and evo.method) else + dropStatBox() closePicker() end return @@ -413,7 +422,8 @@ local function vanillaUseOn(game, battle, id, target, list, moveIndex, picker) end end nextStep() - end)) + end, true) + game.stack:push(statBox) end, TextBox.soundOpts(game, "Get_Item1")) return end diff --git a/src/ui/EvolutionState.lua b/src/ui/EvolutionState.lua index 1ca02256..d5971229 100644 --- a/src/ui/EvolutionState.lua +++ b/src/ui/EvolutionState.lua @@ -18,6 +18,12 @@ local romText = require("src.core.RomText") local EvolutionState = {} EvolutionState.__index = EvolutionState +local PIC_LOAD_FRAMES = 84 +-- evolution.asm EvolveMon delays 80 frames before .animLoop, polling nothing (#968, #1031) +local CANCEL_GRACE_FRAMES = 80 +local ANIM_LOOP_FRAMES = 288 +local FLASH_FRAMES = CANCEL_GRACE_FRAMES + ANIM_LOOP_FRAMES + -- SGB: SetPal_PokemonWholeScreen for the mon on display function EvolutionState:sgbPalettes(game) local P = require("src.render.PaletteFX") @@ -33,7 +39,8 @@ function EvolutionState:sgbPalettes(game) -- every COLORS mode honest for free: OG RED short-circuits every name to the -- one global boot-ROM palette (a Game Boy Color ignores the SGB packets, so -- it never blacks out) and the mono modes replace it in effectiveColors. - if not self.done then + -- ../pokered/engine/movie/evolution.asm:23-26, :49-50 + if not self.done and self.t >= CANCEL_GRACE_FRAMES then local black = P.pal(game.data, "BLACK") if black then return { P.whole(black) } end end @@ -46,15 +53,6 @@ function EvolutionState:sgbPalettes(game) return P.wholeNamed(game.data, "MEWMON") end --- evolution.asm EvolveMon delays 80 frames before .animLoop, polling nothing (#968, #1031) -local CANCEL_GRACE_FRAMES = 80 --- .animLoop starts at `lb bc, $1, $10` and runs 8 iterations: iteration k holds --- the old pic for c = 18-2k frames inside Evolution_CheckForCancel (72 in all), --- then Evolution_BackAndForthAnim swaps b = k times, each swap a pair of --- Evolution_ChangeMonPic that end in Delay3 (6 frames a swap, 216 in all). -local ANIM_LOOP_FRAMES = 288 -local FLASH_FRAMES = CANCEL_GRACE_FRAMES + ANIM_LOOP_FRAMES - -- which pic is on screen t frames into .animLoop local function evoShowsNew(t) for b = 1, 8 do @@ -95,18 +93,74 @@ function EvolutionState.new(game, mon, newSpecies, onDone, via) self.t = 0 self.done = false self.canceled = false - -- engine/movie/evolution.asm:41-46 + -- engine/movie/evolution.asm:12-19 Music.stop() - self.crySrc = require("src.core.Sound").playCry(game.data, mon.species) + require("src.core.Sound").play(game.data, "Tink") + self.loading = PIC_LOAD_FRAMES + self.crySrc = nil self.cryT = 0 - self.cryWait = self.crySrc ~= nil - if not self.cryWait then - Music.play(game.data, Music.special(game.data, "evolution")) - end + self.cryWait = false return self end +-- engine/movie/evolution.asm:39-46 +function EvolutionState:beginCry() + self.crySrc = require("src.core.Sound").playCry(self.game.data, self.mon.species) + self.cryT = 0 + self.cryWait = self.crySrc ~= nil + if not self.cryWait then + Music.play(self.game.data, Music.special(self.game.data, "evolution")) + end +end + +-- engine/movie/evolution.asm:68-74 +function EvolutionState:beginDoneCry(species) + Music.stop() + self.endCrySrc = require("src.core.Sound").playCry(self.game.data, species) + self.endCryT = 0 + self.endCryWait = self.endCrySrc ~= nil + if not self.endCryWait then self:pushDoneText() end +end + +-- engine/pokemon/evos_moves.asm:136-153, :293 +function EvolutionState:pushDoneText() + local game = self.game + local TextBox = require("src.render.TextBox") + if self.canceled then + game.stack:push(TextBox.new(game, + romText(game.data, "_StoppedEvolvingText", + "Huh? %s\nstopped evolving!", self.oldName), + function() + Music.restoreMap(game.data) + game.stack:pop() + if self.onDone then self.onDone() end + end)) + return + end + local Evolution = require("src.pokemon.Evolution") + local newName = game.data.pokemon[self.newSpecies].name + -- engine/pokemon/evos_moves.asm:136-153 + local msg = romText(game.data, "_EvolvedText", "%s evolved", self.oldName) + .. romText(game.data, "_IntoText", "\ninto %s!", newName) + game.stack:push(TextBox.new(game, msg, + function() + Music.restoreMap(game.data) + game.stack:pop() + -- engine/pokemon/evos_moves.asm:212 (#12) + Evolution.learnEvolutionMoves(game, self.mon, self.onDone) + end, + TextBox.soundOpts(game, "Get_Item2"))) +end + function EvolutionState:update(dt) + -- engine/movie/evolution.asm:20-40 + if self.loading then + self.loading = self.loading - 1 + if self.loading > 0 then return end + self.loading = nil + self:beginCry() + return + end if self.cryWait then self.cryT = self.cryT + 1 local src = self.crySrc @@ -117,6 +171,17 @@ function EvolutionState:update(dt) end return end + -- home/pokemon.asm:145-149 + if self.endCryWait then + self.endCryT = self.endCryT + 1 + local src = self.endCrySrc + local playing = src and src.isPlaying and src:isPlaying() + if self.endCryT >= 3 and (not playing or self.endCryT > 180) then + self.endCryWait, self.endCrySrc = false, nil + self:pushDoneText() + end + return + end self.t = self.t + 1 if self.done then return end local game = self.game @@ -126,40 +191,15 @@ function EvolutionState:update(dt) and game.input:wasPressed("b") then self.done = true self.canceled = true - local TextBox = require("src.render.TextBox") - game.stack:push(TextBox.new(game, - romText(game.data, "_StoppedEvolvingText", - "Huh? %s\nstopped evolving!", self.oldName), - function() - Music.restoreMap(game.data) - game.stack:pop() -- the evolution screen itself - if self.onDone then self.onDone() end - end)) + -- evolution.asm:89-94 + self:beginDoneCry(self.mon.species) return end if self.t >= FLASH_FRAMES then self.done = true local Evolution = require("src.pokemon.Evolution") Evolution.apply(game, self.mon, self.newSpecies, self.via) - require("src.core.Sound").playCry(game.data, self.newSpecies) - local TextBox = require("src.render.TextBox") - local newName = game.data.pokemon[self.newSpecies].name - -- EvolvedText then IntoText in the same box (PrintText_NoCreatingTextBox), - -- then SFX_GET_ITEM_2 (engine/pokemon/evos_moves.asm:136-153) - local msg = romText(game.data, "_EvolvedText", "%s evolved", self.oldName) - .. romText(game.data, "_IntoText", "\ninto %s!", newName) - game.stack:push(TextBox.new(game, msg, - function() - Music.restoreMap(game.data) - game.stack:pop() -- the evolution screen itself - -- Gen1 re-runs the level-up learn check on the evolved species - -- after the "evolved into" text (evos_moves.asm EvolveMon -> - -- learn_move.asm LearnMoveFromLevelUp, #12). Pop the evo screen - -- first so the "learned MOVE!" text / forget prompt push onto the - -- overworld / battle-return, not this state. - Evolution.learnEvolutionMoves(game, self.mon, self.onDone) - end, - TextBox.soundOpts(game, "Get_Item2"))) + self:beginDoneCry(self.newSpecies) end end @@ -167,6 +207,8 @@ function EvolutionState:draw() love.graphics.setColor(1, 1, 1, 1) -- rows 0-11 only (hlcoord 0,0 / lb bc, 12, 20, evos_moves.asm:126-128) love.graphics.rectangle("fill", 0, 0, 160, 96) + -- engine/movie/evolution.asm:20-40 + if self.loading then return end -- accelerating flash between the two forms local sprite, spriteTrueColor diff --git a/src/ui/OptionsMenu.lua b/src/ui/OptionsMenu.lua index b341c162..99f6aad2 100644 --- a/src/ui/OptionsMenu.lua +++ b/src/ui/OptionsMenu.lua @@ -624,6 +624,13 @@ local function buildRows(game) end rows = filtered end + if require("src.core.Platform").isNX() then + local filtered = {} + for _, row in ipairs(rows) do + if row.id ~= "videoMode" then filtered[#filtered + 1] = row end + end + rows = filtered + end -- TOUCH PAD and VIBRATION only where the overlay can appear (mobile, or -- desktop with POKEPORT_TOUCH=1). POKEPORT_TOUCH=0 forces it off -- everywhere. VIBRATION rides the same gate: nothing else in the port diff --git a/src/ui/Screens.lua b/src/ui/Screens.lua index 2fb382b0..9270d2de 100644 --- a/src/ui/Screens.lua +++ b/src/ui/Screens.lua @@ -79,7 +79,10 @@ local GEN2 = { "MailCompose", "MailMenu", "MailRead", "MailboxMenu", -- Gen2MapRadio is the in-house wall radio (`special MapRadio`), not a card. "MapRadio", - "MainMenu", "MartMenu", "MoveDeleter", + "MainMenu", "MartMenu", + -- ../pokecrystal/home/map.asm:1910-1925 + "MenuFade", + "MoveDeleter", -- ../pokecrystal/engine/events/move_tutor.asm:1 the Goldenrod tutor's own -- move/party pages, Crystal only. "MoveTutor", diff --git a/src/ui/ShaderFXScreen.lua b/src/ui/ShaderFXScreen.lua index 8187089e..bb5cd446 100644 --- a/src/ui/ShaderFXScreen.lua +++ b/src/ui/ShaderFXScreen.lua @@ -146,8 +146,19 @@ function ShaderFXScreen.new(game, slot) end end local overrides = opts and opts.shaderfxParams and opts.shaderfxParams[item.entry.name] - local ok = ShaderFX.activate(slot, item.entry, overrides) + local ok, err = ShaderFX.activate(slot, item.entry, overrides) if opts then opts[optKey] = ok and item.entry.name or nil end + if not ok then + require("src.core.Logger").error("ShaderFXScreen: activate failed for %s: %s", + item.entry.name, tostring(err)) + item.right = Strings("FAILED") + if game.writeOptions then + game:writeOptions() + elseif game.persistOptions then + game:persistOptions() + end + return + end end if game.writeOptions then game:writeOptions() diff --git a/src/ui/SkinStudio.lua b/src/ui/SkinStudio.lua index ff1d4b10..ee0e8147 100644 --- a/src/ui/SkinStudio.lua +++ b/src/ui/SkinStudio.lua @@ -469,6 +469,7 @@ end function Studio.load(opts) opts = opts or {} + Studio._activeSkinId = nil Studio.onClose = opts.onClose Studio.onPlay = opts.onPlay Studio.version = opts.version @@ -605,7 +606,21 @@ function Studio.skinSummary(entry) return table.concat(bits, " \194\183 ") end +function Studio.forgetActiveSkin() + Studio._activeSkinId = nil +end + +function Studio.activeSkinId() + if Studio._activeSkinId == nil then + local saved = SaveData.loadOptions() + local tc = type(saved.touchControls) == "table" and saved.touchControls or {} + Studio._activeSkinId = (tc.enabled ~= false and tc.skin) or false + end + return Studio._activeSkinId or nil +end + function Studio.refreshAvailable() + Studio.forgetActiveSkin() Studio.available = TouchSkin.list() Studio.availableMeta = {} Studio.libraryThumbs = {} @@ -671,6 +686,7 @@ function Studio.selectEntry(id) tc.enabled, tc.skin = true, id opts.touchControls = tc SaveData.saveOptions(opts) + Studio.forgetActiveSkin() TouchControls:applyOptions(opts) setStatus("Using " .. id) return true @@ -696,6 +712,7 @@ function Studio.deleteEntry(id) tc.enabled, tc.skin = true, nil opts.touchControls = tc SaveData.saveOptions(opts) + Studio.forgetActiveSkin() TouchControls:applyOptions(opts) end Studio.refreshAvailable() @@ -754,6 +771,7 @@ function Studio.loadEntry(id) end function Studio.unload() + Studio._activeSkinId = nil Studio.pendingPlay = false TouchSkin.setSurface(nil) TouchSkin.setActive(nil) @@ -786,6 +804,7 @@ function Studio.disableTouchControls() tc.enabled, tc.skin = false, nil opts.touchControls = tc SaveData.saveOptions(opts) + Studio.forgetActiveSkin() TouchControls:applyOptions(opts) TouchSkin.setActive(nil) Studio.closeModal() @@ -1344,6 +1363,7 @@ function Studio.play() block.skin = skin.id opts.touchControls = block SaveData.saveOptions(opts) + Studio.forgetActiveSkin() -- Handing off here would unload the studio inside its own draw pass and -- leave the rest of this frame drawing against a dead skin; Studio.update -- runs it on the next tick instead. @@ -2504,9 +2524,7 @@ local function drawLibrary() local textH = Kit.textHeight("mono") + Kit.textHeight("small") local artH = math.min(cardW * 0.52, math.max(52 * Kit.scale, listH * 0.34)) local cardH = artH + textH + btnH * 2 + 34 * Kit.scale - local saved = SaveData.loadOptions() - local tc = type(saved.touchControls) == "table" and saved.touchControls or {} - local activeId = tc.enabled == false and nil or tc.skin + local activeId = Studio.activeSkinId() local rows = math.ceil(#entries / cols) local contentH = rows * cardH + math.max(0, rows - 1) * cardGap diff --git a/src/ui/gen2/BattleHud.lua b/src/ui/gen2/BattleHud.lua index 2faab48e..3c1de6ce 100644 --- a/src/ui/gen2/BattleHud.lua +++ b/src/ui/gen2/BattleHud.lua @@ -145,8 +145,8 @@ end -- HUD's bar minus the "HP:" prefix -- same tiles, same HPBarPals colour, same -- one-pixel-at-a-time fill. Sharing this method is what keeps the two screens -- from ever disagreeing about how full a bar looks. -function BattleHud:drawBar(hp, maxHp, tx, ty, zero) - local pixels = HpBar.pixels(hp, maxHp) +function BattleHud:drawBar(hp, maxHp, tx, ty, zero, pixels) + pixels = pixels or HpBar.pixels(hp, maxHp) local colors = self:barColors(HpBar.palette(pixels), zero) for cell = 0, HpBar.LENGTH_TILES - 1 do local remaining = pixels - cell * 8 @@ -162,8 +162,8 @@ end -- frame's vertical stub there. -- `zero` overrides colour 0: the stats screen puts the page tint there -- (engine/gfx/color.asm:386-390). #1693 -function BattleHud:drawHpBar(hp, maxHp, tx, ty, zero) - local pixels = HpBar.pixels(hp, maxHp) +function BattleHud:drawHpBar(hp, maxHp, tx, ty, zero, pixels) + pixels = pixels or HpBar.pixels(hp, maxHp) local colors = self:barColors(HpBar.palette(pixels), zero) -- The "HP:" badge sits inside the bar's own attrmap region, so it wears the -- HP palette too: its background is HPBarPals' light colour (the cream the @@ -172,7 +172,7 @@ function BattleHud:drawHpBar(hp, maxHp, tx, ty, zero) self:drawTile("hpBar", FIRST_BATTLE_EXTRA, TILE_HP_LABEL, tx, ty, colors) self:drawTile("hpBar", FIRST_BATTLE_EXTRA, TILE_HP_LABEL + 1, tx + 1, ty, colors) - self:drawBar(hp, maxHp, tx + 2, ty, zero) + self:drawBar(hp, maxHp, tx + 2, ty, zero, pixels) self:drawTile("hpBar", FIRST_BATTLE_EXTRA, TILE_BAR_END, tx + 2 + HpBar.LENGTH_TILES, ty, colors) return tx + 3 + HpBar.LENGTH_TILES diff --git a/src/ui/gen2/BattleState.lua b/src/ui/gen2/BattleState.lua index 8ac07a24..838e63fa 100644 --- a/src/ui/gen2/BattleState.lua +++ b/src/ui/gen2/BattleState.lua @@ -47,6 +47,7 @@ local SummaryMenu = require("src.ui.gen2.SummaryMenu") -- Only for TextBox.substitute: the {PLAYER} / {RIVAL} markers a map text -- carries into the battle box (PrintWinLossText, home/trainers.asm:230). local TextBox = require("src.render.TextBox") +local Typer = require("src.ui.gen2.Typer") local Unown = require("src.core.gen2.Unown") local BattleState = {} @@ -89,12 +90,35 @@ local TEXT_WIDTH = 18 local TEXT_ROWS = 2 local TEXT_ROW_STEP = 2 +-- ../pokecrystal/engine/battle/core.asm:9119-9137 +-- ../pokecrystal/data/text/battle.asm:10-35 +function BattleState.battleStartText(name, battleType) + local kind = Battle.battleTypeId(battleType) + if kind == Battle.BATTLETYPE_FISH then + return "The hooked " .. name .. " attacked!" + elseif kind == Battle.BATTLETYPE_TREE then + return name .. " fell out of the tree!" + end + return "Wild " .. name .. " appeared!" +end + +-- ../pokecrystal/engine/battle/core.asm:6422, :6251-6256 +function BattleState.sleepingTreeMon(mon, battleType) + if Battle.battleTypeId(battleType) ~= Battle.BATTLETYPE_TREE then + return false + end + return mon ~= nil and mon.status == "sleep" +end + -- SlideBattlePicOut (engine/battle/core.asm:2882) is called with a = 8: eight -- one-tile steps with `ld c, 2 / call DelayFrames` between them, so the enemy -- trainer's pic clears the box in 16 frames. local TRAINER_SLIDE_STEPS = 8 local TRAINER_SLIDE_FRAMES_PER_STEP = 2 local TRAINER_SLIDE_FRAMES = TRAINER_SLIDE_STEPS * TRAINER_SLIDE_FRAMES_PER_STEP +-- ../pokecrystal/engine/battle/core.asm:82-84 +local BACKPIC_SLIDE_STEPS = 9 +local BACKPIC_SLIDE_FRAMES = BACKPIC_SLIDE_STEPS * TRAINER_SLIDE_FRAMES_PER_STEP -- BattleWinSlideInEnemyTrainerFrontpic (engine/battle/core.asm:6279-6318) and -- WinTrainerBattle's DelayFrames 40 (:2311) @@ -443,8 +467,12 @@ function BattleState.new(game, opts) -- BattleStartMessage's `.wild` arm ends on WildPokemonAppearedText -- (core.asm:8730); `intro` is what defers the enemy HUD to the step after -- it, which is where StartBattle's `call z, UpdateEnemyHUD` sits. - self:push({ kind = "message", intro = true, cry = enemy, - text = "Wild " .. self:name(enemy) .. " appeared!" }) + -- ../pokecrystal/engine/battle/core.asm:9104-9106 + local asleep = BattleState.sleepingTreeMon(enemy, self.battle.battleType) + self:push({ kind = "message", intro = true, + cry = (not asleep) and enemy or nil, + text = BattleState.battleStartText(self:name(enemy), + self.battle.battleType) }) else local trainerName = (self.battle.trainer and self.battle.trainer.name) or "Foe" @@ -466,6 +494,8 @@ function BattleState.new(game, opts) end local player = self.battle and self.battle.player if player then + -- ../pokecrystal/engine/battle/core.asm:82-84 + self:push({ kind = "backpic-slide" }) self:push({ kind = "sendout", text = "Go! " .. self:name(player) .. "!" }) end @@ -581,7 +611,13 @@ function BattleState:dudeInput(stream, key, skipIdle) game and game.input, skipIdle) end +-- ../pokecrystal/engine/battle/move_effects/transform.asm:118-136 function BattleState:push(event) + if event.kind == "transform" and event.side and event.from and self.shownMon + and self.shownMon[event.side] == event.mon then + self.shownMon[event.side] = + setmetatable({ species = event.from }, { __index = event.mon }) + end self.queue[#self.queue + 1] = event end @@ -831,10 +867,10 @@ function BattleState:drawPic(mon, back) end if not image then return end local side = back and "player" or "enemy" - local anim = self:animPicState(side) -- The box is empty either because the animation running right now has -- cleared it, or because the last one ENDED with it cleared (picHidden). - if (anim and anim.hidden) or self.picHidden[side] then return end + if self:picBoxCleared(side) then return end + local anim = self:animPicState(side) -- Mid FLY / DIG the box is empty: DisappearUser ClearBoxes it -- (engine/battle/misc.asm:1-13), AppearUserRaiseSub puts it back on the -- stored attack (engine/battle/effect_commands.asm:2113-2117). @@ -884,6 +920,8 @@ function BattleState:drawPic(mon, back) -- One tile per two frames to the right, SlideBattlePicOut's own step. if enemyTrainer and self.trainerSlide then px = px + math.floor(self.trainerSlide / TRAINER_SLIDE_FRAMES_PER_STEP) * 8 + elseif trainerBack and self.backpicSlide then + px = px - math.floor(self.backpicSlide / TRAINER_SLIDE_FRAMES_PER_STEP) * 8 elseif enemyTrainer and self.winSlide then px = px + winSlideTiles(self.winSlide) * 8 end @@ -1053,31 +1091,43 @@ function BattleState:faintSlideFrames(side) return tiles * FAINT_SLIDE_FRAMES_PER_ROW end --- One tick of the HP bar chase (_AnimateHPBar): under 48 max HP the bar steps --- one hit point a frame (ShortAnim_UpdateVariables); from 48 up it moves one --- PIXEL a frame, which is maxHp/48 hit points at a time --- (LongAnim_UpdateVariables). Returns true while a step was taken, so the --- caller holds the queue the way the cart's loop holds the game. +-- ../pokecrystal/engine/battle/anim_hp_bar.asm:286 +local HP_BAR_FRAMES_PER_STEP = 2 + +-- ../pokecrystal/engine/battle/anim_hp_bar.asm:1 function BattleState:stepHpAnim() local anim = self.hpAnim if not anim or not self.shownHp then return false end - -- wCurHPAnimMaxHP is loaded from the battle struct of the mon whose bar is - -- ON SCREEN (wEnemyMonMaxHP -> wHPBuffer1 before `predef AnimateHPBar`, - -- engine/battle/effect_commands.asm:3399-3414), and _AnimateHPBar picks its - -- short/long loop and its pixels off that alone (anim_hp_bar.asm:42-50, - -- :56-82). After a faint that mon is still the OUTGOING one: the engine has - -- already rebound battle.enemy, but its `send` event has not been dequeued - -- yet, so sizing the tick off battle[side] drains the dead mon's bar at the - -- replacement's rate. - local mon = self:activeMon(anim.side) - local maxHp = (mon and (mon.maxHp or (mon.stats and mon.stats.hp))) or 0 - local target = anim.to or 0 - local shown = HpBar.stepToward(self.shownHp[anim.side] or 0, target, maxHp) - self.shownHp[anim.side] = shown - if shown == target then self.hpAnim = nil end + local side = anim.side + if not anim.bar then + -- engine/battle/effect_commands.asm:3399-3414), and _AnimateHPBar picks its + -- short/long loop and its pixels off that alone (anim_hp_bar.asm:42-50, + local mon = self:activeMon(side) + local maxHp = (mon and (mon.maxHp or (mon.stats and mon.stats.hp))) or 0 + anim.bar = HpBar.newAnim(self.shownHp[side] or 0, anim.to or 0, maxHp) + end + if (anim.hold or 0) > 0 then + anim.hold = anim.hold - 1 + return true + end + if HpBar.animStep(anim.bar) then + self.shownHp[side] = anim.to or 0 + self.hpAnim = nil + return false + end + self.shownHp[side] = anim.bar.hp + anim.hold = HP_BAR_FRAMES_PER_STEP - 1 return true end +-- ../pokecrystal/engine/battle/anim_hp_bar.asm:196 +function BattleState:hudHpPixels(mon, side) + local anim = self.hpAnim + if anim and anim.side == side and anim.bar then return anim.bar.px end + return HpBar.pixels(self:hudHp(mon, side), + mon and (mon.maxHp or (mon.stats and mon.stats.hp))) +end + -- .PlayExpBarSound's own two halves (engine/battle/core.asm:7311-7318): the -- looping SFX_EXP_BAR, then `ld c, 10 / call DelayFrames` before the first -- pixel moves. TerminateExpBarSound (home/audio.asm:497) cuts it dead at the @@ -1085,7 +1135,9 @@ end -- only plays where a level was actually crossed. local SFX_EXP_BAR = "Sfx_ExpBar" local SFX_END_OF_EXP_BAR = "Sfx_HitEndOfExpBar" +local SFX_GREW_TO_LEVEL = "Sfx_DexFanfare5079" local EXP_SOUND_FRAMES = 10 +local EXP_WAIT_SFX_CAP = 180 -- (../pokecrystal/engine/sprite_anims/core.asm:547-608). The `call WaitSFX` -- behind it (../pokecrystal/engine/battle/core.asm:7538) is what holds the @@ -1124,6 +1176,13 @@ function BattleState:stepExpAnim() target = self:expPixels(mon, self.shownLevel, mon and mon.experience) end if not anim.started then + -- ../pokecrystal/engine/battle/core.asm:7569 + if Sound.sfxBusy and Sound.sfxBusy() + and (anim.busy or 0) < EXP_WAIT_SFX_CAP then + anim.busy = (anim.busy or 0) + 1 + return true + end + anim.busy = nil anim.started = true anim.frames = 3 anim.wait = 0 @@ -1133,6 +1192,11 @@ function BattleState:stepExpAnim() end if anim.delay > 0 then anim.delay = anim.delay - 1 + -- ../pokecrystal/engine/battle/core.asm:7584 + if anim.delay == 0 and anim.restart then + anim.restart = nil + self.shownExp = 0 + end return true end local shown = self.shownExp or 0 @@ -1151,17 +1215,31 @@ function BattleState:stepExpAnim() if (self.shownLevel or 1) < toLevel then self.shownLevel = (self.shownLevel or 1) + 1 self:playSfx(SFX_END_OF_EXP_BAR) - self.expBurst = { frame = 1 } + self.expBurst = { frame = 0, + line = self:expLevelLine(anim, self.shownLevel, toLevel) } return true end self.expAnim = nil return true end --- (../pokecrystal/engine/battle/core.asm:7536-7538). The bar stays at $40 +-- ../pokecrystal/data/text/battle.asm:343 +function BattleState:expLevelLine(anim, level, toLevel) + if anim.line and level == toLevel then return anim.line end + return self:name(anim.mon) .. " grew to level " .. level .. "!" +end + +-- ../pokecrystal/engine/battle/core.asm:7536-7541 function BattleState:stepExpBurst(anim) local burst = self.expBurst - if not burst then return false end + if not burst then + if anim.lineWait then + anim.lineWait = nil + anim.started = false + anim.restart = true + end + return false + end burst.frame = burst.frame + 1 if burst.frame <= EXP_BURST_FRAMES then return true end if not burst.left then @@ -1171,15 +1249,22 @@ function BattleState:stepExpBurst(anim) burst.left = burst.left - 1 if burst.left > 0 and Sound.isPlaying(SFX_END_OF_EXP_BAR) then return true end self.expBurst = nil - self.shownExp = 0 - anim.started = false + -- ../pokecrystal/engine/battle/core.asm:7540 + self.message = burst.line + self.messageTimer = 0 + local sfx = anim.lineSfx or SFX_GREW_TO_LEVEL + self:playSfx(sfx) + self.waitSfx = sfx + anim.lineWait = true return true end -- (../pokecrystal/engine/sprite_anims/core.asm:558-608). function BattleState:drawExpBurst() local burst = self.expBurst - if not burst or burst.frame > EXP_BURST_FRAMES then return end + if not burst or burst.frame < 1 or burst.frame > EXP_BURST_FRAMES then + return + end local radius = (burst.frame - 1) * 2 local set = self.palettes and self.palettes.battleObjects local colors = set and set.PAL_BATTLE_OB_BLUE or nil @@ -1205,10 +1290,11 @@ end function BattleState:drawHpBar(mon, side, tx, ty) local maxHp = mon.maxHp or (mon.stats and mon.stats.hp) local hp = self:hudHp(mon, side) + local pixels = self:hudHpPixels(mon, side) if self.hud:available() then - return self.hud:drawHpBar(hp, maxHp, tx, ty) + return self.hud:drawHpBar(hp, maxHp, tx, ty, nil, pixels) end - return HpBar.drawWithLabel(self.palettes, hp, maxHp, tx, ty, Font) + return HpBar.drawWithLabel(self.palettes, hp, maxHp, tx, ty, Font, pixels) end -- CheckCaughtMon against wPokedexCaught (home/pokedex_flags.asm:48-51). @@ -1265,8 +1351,7 @@ function BattleState:lowHealthAlarmActive() end local player = self.battle and self.battle.player return (player and (player.hp or 0) > 0 - and HpBar.paletteFor(self:hudHp(player, "player"), - player.maxHp or (player.stats and player.stats.hp)) == "red") and true + and HpBar.palette(self:hudHpPixels(player, "player")) == "red") and true or false end @@ -1473,10 +1558,24 @@ function BattleState:stepAnim(input) if self:startPendingAfterAnim() then return end return self:endSendOutAnim() end + self:revealSentOut() +end + +-- ../pokecrystal/engine/battle_anims/bg_effects.asm:709-720 +function BattleState:revealSentOut() + local after = self.afterSendOut + if not (after and self.anim and self.picHidden[after.side]) then return end + if self.anim.bg.picSize[after.side] ~= nil then + self.picHidden[after.side] = false + end +end + +function BattleState:picBoxCleared(side) + local anim = self:animPicState(side) + return ((anim and anim.hidden) or self.picHidden[side]) and true or false end -- REMOVE_MON / RETURN_MON also serve SUBSTITUTE, SKY_ATTACK, BEAT_UP and --- BATON_PASS, so picHidden is only latched by a catch and a faint. -- Whatever Call_PlayBattleAnim was standing in front of: a send-out's cry and -- HUD update run the moment its animation is done, cut short or not. @@ -1484,6 +1583,7 @@ function BattleState:endSendOutAnim(skipped) local after = self.afterSendOut if not after then return end self.afterSendOut = nil + self.picHidden[after.side] = false if after.shiny and not skipped then after.shiny = nil if self:animForId("ANIM_SEND_OUT_MON", after.side, 1) then @@ -1581,23 +1681,30 @@ function BattleState:advanceQueue() event.slid = true table.insert(self.queue, 1, event) self.faintSlide = { side = event.side, frames = 0 } - -- FaintEnemyPokemon opens on SFX_KINESIS, FaintYourPokemon on the fainting - -- mon's own cry (engine/battle/core.asm:2196-2201, :2210-2212). - if event.side == "enemy" then - self:playSfx("Sfx_Kinesis") + -- ../pokecrystal/engine/battle/core.asm:2256, :2269 + if Sound.sfxBusy and Sound.sfxBusy() then + self.faintSlide.wait = 0 else - self:playCry(self:activeMon("player")) + self:faintSound(event.side) end return end -- Battle:awardExperience emits one `level` event per mon that grew, which is -- exactly where the cart sets that slot's wEvolvableFlags bit. if event.kind == "level" and event.index then + local battle = self.battle + local mon = battle and battle.party and battle.party[event.index] + -- ../pokecrystal/engine/battle/core.asm:7294-7296 + if mon and battle and mon ~= battle.player and not event.hitPlayed then + event.hitPlayed = true + table.insert(self.queue, 1, event) + self:playSfx(SFX_END_OF_EXP_BAR) + self.waitSfx = SFX_END_OF_EXP_BAR + return + end self.evolvable[event.index] = true -- GiveExperiencePoints' `.skip_active_mon_update` guard -- (engine/battle/core.asm:6999-7003): the OUT mon's shown HP snaps. - local battle = self.battle - local mon = battle and battle.party and battle.party[event.index] -- pokegold engine/battle/core.asm:7057-7069: every mon that leveled -- gets the stats box, not just the mon currently on the field. self.pendingStatsMon = mon @@ -1658,9 +1765,25 @@ function BattleState:advanceQueue() -- at MAX_LEVEL (:7199-7201). if mon and mon == battle.player and (self.shownLevel or 1) < Mon.MAX_LEVEL then - self.expAnim = { mon = mon, frames = 3, wait = 0, pixels = 0 } + local anim = { mon = mon, frames = 3, wait = 0, pixels = 0 } + -- ../pokecrystal/engine/battle/core.asm:7540 + for _, ahead in ipairs(self.queue) do + if ahead.kind == "level" and ahead.index == event.index then + if (self.shownLevel or 1) < (ahead.level or mon.level or 1) then + ahead.lineShown = true + anim.line, anim.lineSfx = ahead.text, ahead.sfx + end + break + end + end + self.expAnim = anim end end + -- ../pokecrystal/engine/battle/move_effects/transform.asm:118-136 + if event.kind == "transform" and event.side and event.mon + and self.shownMon then + self.shownMon[event.side] = event.mon + end if event.kind == "send" and event.side and event.mon then -- The pic and the HUD name follow the queue, so the mon that just fainted -- is still on screen for its own line and the replacement arrives here. @@ -1696,6 +1819,11 @@ function BattleState:advanceQueue() self.trainerSlide = 0 return end + -- ../pokecrystal/engine/battle/core.asm:82-84 + if event.kind == "backpic-slide" then + self.backpicSlide = 0 + return + end -- BattleWinSlideInEnemyTrainerFrontpic and the DelayFrames 40 behind it -- (engine/battle/core.asm:2310-2312) if event.kind == "trainer-return" then @@ -1738,7 +1866,9 @@ function BattleState:advanceQueue() self.menuIndex = 1 self.moveIndex = 1 self.message = event.text - self.messageTimer = MESSAGE_FRAMES + -- ../pokecrystal/data/text/common_2.asm:137-140 + -- ../pokecrystal/engine/battle/core.asm:92-93 + self.messageTimer = 0 self:startSendOut("player", self.battle and self.battle.player) return end @@ -1803,8 +1933,9 @@ function BattleState:advanceQueue() self:playVictoryMusic() end end - if event.text then + if event.text and not event.lineShown then self.message = event.text + self.typedText = nil -- engine/battle/core.asm:8733 if self.startHuds then self.ballRows.player = self.startHuds.player @@ -1916,10 +2047,9 @@ function BattleState:startSendOut(side, mon) -- BattleCheckPlayerShininess / BattleCheckEnemyShininess replay the anim's -- `.Shiny` arm before the cry (core.asm:3826-3831, :3371-3377). local after = { side = side, mon = mon, shiny = mon and mon.shiny and true } - -- ShowSetEnemyMonAndSendOutAnimation and SendOutPlayerMon both draw the pic - -- into the box before they play the animation, which is the one thing that - -- undoes a cleared box. - self.picHidden[side] = false + -- ../pokecrystal/engine/battle/core.asm:4030-4033, :3557 + -- ../pokecrystal/engine/battle_anims/bg_effects.asm:647 + self.picHidden[side] = true self.faintSlide = nil -- The rows are shadow OAM (engine/battle/trainer_huds.asm:203-223), which -- this animation's own sprites overwrite. @@ -1941,6 +2071,7 @@ end -- SendOutPlayerMon (:3836-3838). function BattleState:finishSendOut(after) if not after then return end + self.picHidden[after.side] = false self:playCry(after.mon) if after.side == "enemy" then self:startFrontAnim(after.mon) @@ -1960,6 +2091,15 @@ function BattleState:playSfx(name) end end +-- ../pokecrystal/engine/battle/core.asm:2258-2260, :2270-2271 +function BattleState:faintSound(side) + if side == "enemy" then + self:playSfx("Sfx_Kinesis") + else + self:playCry(self:activeMon("player")) + end +end + -- PlayStereoCry with the battler's own species. audio.cries is keyed by -- SPECIES, the same table the animation runtime's `cry` callback plays through. function BattleState:playCry(mon) @@ -2262,12 +2402,31 @@ function BattleState:update(_dt) end return end + -- ../pokecrystal/engine/battle/core.asm:82-84 + if self.backpicSlide then + self.backpicSlide = self.backpicSlide + 1 + if self.backpicSlide >= BACKPIC_SLIDE_FRAMES then + self.backpicSlide = nil + self.showPlayerTrainer = false + self.picHidden.player = true + self:advanceQueue() + end + return + end -- MonFaintedAnimation is a plain loop with DelayFrames in it, like -- SlideBattlePicOut above: it owns the screen until the pic is off the -- field, and the box it emptied stays empty (only a send-out refills it). if self.faintSlide then local slide = self.faintSlide + if slide.wait then + if Sound.sfxBusy() and slide.wait < EXP_WAIT_SFX_CAP then + slide.wait = slide.wait + 1 + return + end + slide.wait = nil + self:faintSound(slide.side) + end slide.frames = slide.frames + 1 if slide.frames >= self:faintSlideFrames(slide.side) then self.picHidden[slide.side] = true @@ -2285,6 +2444,12 @@ function BattleState:update(_dt) return end + -- ../pokecrystal/home/text.asm:660 + if self:syncTyper() then + self.typer:tick() + return + end + -- An animation owns the screen for as long as it runs, exactly the way -- RunBattleAnimScript owns the main loop. -- pokegold data/moves/animations.asm .Click: a finished keepsprites run @@ -2315,10 +2480,6 @@ function BattleState:update(_dt) end self.waitSfx, self.waitSfxLeft = nil, nil end - -- AnimateExpBar sits right after PrintText Text_MonGainedExpPoint - -- (core.asm:6881-6888), with that line still on screen. Run the crawl - -- before any PromptButton wait so the bar does not sit frozen until A. - if self:stepExpAnim() then return end -- engine/battle/effect_commands.asm:6661 if (self.messageDelay or 0) > 0 then self.messageDelay = self.messageDelay - 1 @@ -2338,6 +2499,8 @@ function BattleState:update(_dt) end return end + -- ../pokecrystal/engine/battle/core.asm:7121-7128 + if self:stepExpAnim() then return end -- pokegold engine/battle/core.asm:7057-7069: the stats box shows once -- the "grew to level" line has finished, held for A/B. if self.pendingStatsMon then @@ -2740,18 +2903,21 @@ function BattleState:refuseSwitch(forced, text) self.refuseForced = forced and true or false self.phase = "refuse-switch" self.message = text or TEXT_NO_WILL_TO_FIGHT + self.typedText = nil self.messageTimer = MESSAGE_FRAMES end function BattleState:refuseMove(text) self.phase = "refuse-move" self.message = text + self.typedText = nil self.messageTimer = MESSAGE_FRAMES end function BattleState:refuseMenu(text) self.phase = "refuse-menu" self.message = text + self.typedText = nil self.messageTimer = MESSAGE_FRAMES end @@ -3393,7 +3559,8 @@ function BattleState:catchOptions(itemId) status = enemy.status, random = battle.random, weight = dexEntry and dexEntry.weight, level = enemy.level, playerLevel = player and player.level, - fishing = battle.battleType == "fish", species = enemy.species, + fishing = battle.battleType == Battle.BATTLETYPE_FISH, + species = enemy.species, gender = enemy.gender, playerSpecies = player and player.species, playerGender = player and player.gender, evolveItem = evolveItem, } @@ -3861,8 +4028,6 @@ function BattleState:drawHud() self:drawFrame(9, 11, 10, true) HpBar.drawExp(self.palettes, expFraction, 10 * 8, 11 * 8 + 4) end - -- The OBJ layer over the bar (../pokecrystal/engine/battle/core.asm:7537). - if self.expBurst then self:drawExpBurst() end Font.useBattleExtra(wasBattle) end @@ -3915,13 +4080,39 @@ end -- them (home/text.asm:143 and :397). A string that will not fit two 18-tile -- lines is cut rather than spilling onto the rows Paragraph clears. function BattleState:printMessage() - local lines = Chrome.wrap(self.message or "", TEXT_WIDTH) + self:syncTyper() + local lines = self:messageLines() for i = 1, math.min(#lines, TEXT_ROWS) do Chrome.printThrough(lines[i], TEXT_INNER_X, TEXT_INNER_Y + (i - 1) * TEXT_ROW_STEP, Chrome.DEFAULT_BOX_PALETTE) end end +-- ../pokecrystal/home/text.asm:660 StdBattleTextbox -> PrintText +-- ../pokecrystal/home/print_text.asm:1 PrintLetterDelay +function BattleState:syncTyper() + local text = self.message + if text ~= self.typedText then + self.typedText = text + if text == nil or text == "" then + self.typer = nil + else + local lines = Chrome.wrap(text, TEXT_WIDTH) + for i = #lines, TEXT_ROWS + 1, -1 do lines[i] = nil end + self.typer = Typer.new(self.game) + self.typer:start(lines) + end + end + return self.typer ~= nil and not self.typer:done() +end + +function BattleState:messageLines() + if self.typer and self.typedText == self.message then + return self.typer:lines() + end + return Chrome.wrap(self.message or "", TEXT_WIDTH) +end + -- MoveInfoBox (engine/battle/core.asm:5403-5478): "TYPE/" at (1,9), the type -- at (2,10), cur/max PP at (5,11), or "Disabled!" at (1,10). function BattleState:drawMoveInfoBox(move) @@ -4156,9 +4347,13 @@ function BattleState:drawSceneBody() self.animView:present(self.anim, panel, self.battle) self:drawLiftedRows() self.animView:drawObjects(self.anim, self.battle) + -- ../pokecrystal/engine/sprite_anims/core.asm:572 + if self.expBurst then self:drawExpBurst() end return end panel() + -- ../pokecrystal/engine/sprite_anims/core.asm:572 + if self.expBurst then self:drawExpBurst() end end function BattleState:draw() diff --git a/src/ui/gen2/BattleTransition.lua b/src/ui/gen2/BattleTransition.lua index 0d2ba9ed..cbb61401 100644 --- a/src/ui/gen2/BattleTransition.lua +++ b/src/ui/gen2/BattleTransition.lua @@ -98,9 +98,11 @@ function BattleTransition.flashVeil(pal) return (sum - 6) / 6 end +-- ../pokecrystal/engine/battle/battle_transition.asm:266-311 +BattleTransition.FLASH_SLOT_FRAMES = + #BattleTransition.FLASH_PALS * BattleTransition.FLASH_HOLD + 1 BattleTransition.FLASH_FRAMES = - #BattleTransition.FLASH_PALS * BattleTransition.FLASH_HOLD - * BattleTransition.FLASH_CYCLES + BattleTransition.FLASH_SLOT_FRAMES * BattleTransition.FLASH_CYCLES -------------------------------------------------------------------------- -- The Poke Ball overlay (trainer battles only) @@ -159,9 +161,24 @@ function BattleTransition.pokeballCells() return cells end --- ../pokecrystal/engine/battle/battle_transition.asm:266-290 -function BattleTransition.ballShade(byte) - return GbcPalette.bgpShades(byte)[4] + 1 +-- ../pokecrystal/engine/battle/battle_transition.asm:151 +local SQUARE_TILE = { + "33333333", "30000003", "31000013", "31100113", + "31122113", "31222213", "32222223", "33333333", +} + +function BattleTransition.squareTile() return SQUARE_TILE end + +function BattleTransition.squareShades(byte) + local shades = GbcPalette.bgpShades(byte) + local out = {} + for y = 1, 8 do + out[y] = {} + for x = 1, 8 do + out[y][x] = shades[tonumber(SQUARE_TILE[y]:sub(x, x)) + 1] + 1 + end + end + return out end -------------------------------------------------------------------------- @@ -205,8 +222,8 @@ BattleTransition.SPIN_STEPS = { { "LOWER_LEFT", "wedge1", 1, 11 }, } --- Each spin step holds for two frames (`call DelayFrame` twice). -BattleTransition.SPIN_HOLD = 2 +-- ../pokecrystal/engine/battle/battle_transition.asm:381-402 +BattleTransition.SPIN_HOLD = 3 -- Walk one wedge, marking cells in `black` (a [y * COLS + x] set). The -- quadrant only decides two signs: RIGHT_QUADRANT_F flips the fill direction @@ -247,8 +264,6 @@ end -------------------------------------------------------------------------- -- `.boxes`: width, height, and the top-left corner, growing out of the middle --- until the last one is the whole screen. One box per WaitBGMap, i.e. one a --- frame. BattleTransition.ZOOM_BOXES = { { 4, 2, 8, 8 }, { 6, 4, 7, 7 }, @@ -260,6 +275,14 @@ BattleTransition.ZOOM_BOXES = { { 18, 16, 1, 1 }, { 20, 18, 0, 0 }, } +-- ../pokecrystal/home/tilemap.asm:3-10 +BattleTransition.ZOOM_HOLD = 4 + +-- ../pokecrystal/engine/battle/battle_transition.asm:29-32 +BattleTransition.OUTRO_LEAD = { spin = 2, speckle = 2, sine = 2, zoom = 1 } +-- ../pokecrystal/engine/battle/battle_transition.asm:395-402 +BattleTransition.END_HOLD = { spin = 4, speckle = 4, sine = 1, zoom = 1 } + -- `zoombox width, height, start y, start x` -- the macro's own argument order, -- which is why the third number is the ROW. function BattleTransition.zoomStep(black, box) @@ -426,16 +449,24 @@ function BattleTransition.new(game, opts) self.frame = 0 self.step = 0 self.sine = nil - if self.trainer then - self.phase = "pokeball" - elseif self.dark then - self:beginOutro() - else - self.phase = "flash" + self.phase = self.trainer and "pokeball" or "flash" + -- ../pokecrystal/engine/overworld/map_objects.asm:2191-2205 + self.respawnFilter = function(npc) return self:keepsOpponent(npc) end + if self.world then + self.world.bgOverlay = function(s) self:drawBgLayer(s) end end return self end +function BattleTransition:keepsOpponent(npc) + local world = self.world + local vm = world and world.vm + if not (vm and vm.lastTalked) then return false end + if not (self.trainer or (vm.running and vm:running())) then return false end + return npc and npc.def and (npc.def.index or 0) + 1 == vm.lastTalked + or false +end + function BattleTransition:trainerRamp() if not self.recolor then return nil end return self.dark and BattleTransition.TRAINER_PAL_DARK @@ -444,7 +475,7 @@ end -- ../pokecrystal/engine/battle/battle_transition.asm:272-275 function BattleTransition:flashFrames() - if self.dark then return 0 end + if self.dark then return BattleTransition.FLASH_CYCLES end return BattleTransition.FLASH_FRAMES end @@ -452,6 +483,8 @@ function BattleTransition:beginOutro() self.phase = "outro" self.frame = 0 self.step = 0 + self.ending = nil + self.captured = false if self.style == "sine" then self.sine = BattleTransition.sineFrames() end @@ -466,12 +499,8 @@ function BattleTransition:update(_dt) -- Two DelayFrames on the DMG path, one CGBOnly_CopyTilemapAtOnce on the -- other; either way the ball is on screen for a moment before the flash. if self.frame >= 2 then - if self.dark then - self:beginOutro() - else - self.phase = "flash" - self.frame = 0 - end + self.phase = "flash" + self.frame = 0 end return end @@ -492,39 +521,61 @@ end function BattleTransition:outroFrame() local style = self.style + self.captured = false + local world = self.world + if self.frame >= 1 and world and not world.spriteFilter then + world.spriteFilter = self.respawnFilter + end + if self.ending then + self.ending = self.ending - 1 + if self.ending <= 0 then self:blackOut() end + return + end + local at = self.frame - (BattleTransition.OUTRO_LEAD[style] or 2) + if at < 0 then return end if style == "spin" then - if self.frame % BattleTransition.SPIN_HOLD ~= 1 then return end + if at % BattleTransition.SPIN_HOLD ~= 0 then return end self.step = self.step + 1 local step = BattleTransition.SPIN_STEPS[self.step] if not step then - self:blackOut() + self:endOutro() return end BattleTransition.spinStep(self.black, step) elseif style == "zoom" then + if at % BattleTransition.ZOOM_HOLD ~= 0 then return end self.step = self.step + 1 local box = BattleTransition.ZOOM_BOXES[self.step] if not box then - self:blackOut() + self:endOutro() return end BattleTransition.zoomStep(self.black, box) elseif style == "speckle" then self.step = self.step + 1 if self.step > BattleTransition.SPECKLE_PASSES then - self:blackOut() + self:endOutro() return end BattleTransition.speckleStep(self.black, self.random) else -- sine self.step = self.step + 1 if not (self.sine and self.sine[self.step]) then - self:blackOut() + self:endOutro() return end end end +function BattleTransition:endOutro() + local hold = BattleTransition.END_HOLD[self.style] or 0 + if hold > 0 then + self.ending = hold + else + self:blackOut() + end +end + -- DoBattleTransition's own `.done`: every BG palette is filled with zero and -- wBGP set to %11111111, i.e. the screen is solid black, and it stays that way -- while the battle screen loads its tiles and decompresses its pics. Two of @@ -544,6 +595,14 @@ end function BattleTransition:finish() if self.finished then return end self.finished = true + local world = self.world + if world then + if world.bgOverlay then world.bgOverlay = nil end + if world.spriteFilter == self.respawnFilter then + world.spriteFilter = nil + end + world.peopleHidden = nil + end local stack = self.game and self.game.stack if stack then stack:pop() end if self.onDone then self.onDone() end @@ -552,7 +611,8 @@ end -- The LY overrides this frame, or nil outside the sine outro. function BattleTransition:lyOverrides() if self.phase ~= "outro" or self.style ~= "sine" then return nil end - return self.sine and self.sine[self.step] or nil + if not (self.sine and self.step > 0) then return nil end + return self.sine[math.min(self.step, #self.sine)] end -- `black` covers the 20x18 tilemap; the window is bigger than that, so a cell @@ -573,9 +633,10 @@ end -- The .pals row this frame is holding, or nil outside the flash phase. function BattleTransition:flashPal() - if self.phase ~= "flash" then return nil end - local index = math.floor(self.frame / BattleTransition.FLASH_HOLD) - % #BattleTransition.FLASH_PALS + 1 + if self.phase ~= "flash" or self.dark then return nil end + local slot = self.frame % BattleTransition.FLASH_SLOT_FRAMES + local index = math.min(math.floor(slot / BattleTransition.FLASH_HOLD), + #BattleTransition.FLASH_PALS - 1) + 1 return BattleTransition.FLASH_PALS[index] end @@ -626,7 +687,7 @@ function BattleTransition:drawMap(w, h, byte) local ramp = self:trainerRamp() if not ramp and (not byte or byte == GbcPalette.BGP_IDENTITY) then -- `dc 3, 2, 1, 0` twice in the table: the picture is simply itself. - self.world:draw() + self:drawWorld() return true end -- TILT projects the finished frame through a linear-filtered canvas, so its @@ -638,9 +699,16 @@ function BattleTransition:drawMap(w, h, byte) love.graphics.setColor(1, 1, 1, 1) love.graphics.draw(canvas, 0, 0) GbcPalette.clear() + self.overlayDrawn = self.captureLayered return true end +function BattleTransition:drawWorld() + self.overlayDrawn = false + self.world:draw() + return self.overlayDrawn +end + BattleTransition.TINT_ALPHA = 0.55 function BattleTransition:drawTint(w, h) @@ -673,13 +741,14 @@ function BattleTransition:drawWidescreen(w, h) local pal = self:flashPal() local byte = pal and BattleTransition.flashByte(pal) or nil + self.overlayDrawn = false if world and world.map then if ly then self:drawWavy(w, h, ly) elseif self:drawMap(w, h, byte) then pal = nil else - world:draw() + self:drawWorld() self:drawTint(w, h) end else @@ -687,12 +756,6 @@ function BattleTransition:drawWidescreen(w, h) G.rectangle("fill", 0, 0, w, h) end - -- ../pokecrystal/engine/battle/battle_transition.asm:584 - if self.trainer then - self:drawCells(w, h, BattleTransition.pokeballCells(), - (not pal) and byte or nil, ly) - end - if pal then local veil = BattleTransition.flashVeil(pal) if veil ~= 0 then @@ -702,7 +765,7 @@ function BattleTransition:drawWidescreen(w, h) end end - self:drawBlack(w, h) + if not self.overlayDrawn then self:drawTopLayer(w, h) end G.setColor(1, 1, 1, 1) end @@ -722,14 +785,92 @@ function BattleTransition:grid(w, h) return size, ox, oy end -function BattleTransition:drawBlack(w, h) - local G = love.graphics +function BattleTransition:drawTopLayer(w, h) local size, ox, oy = self:grid(w, h) + self:paintLayer(size, ox, oy, w, h) +end + +-- ../pokecrystal/engine/battle/battle_transition.asm:609-646 +function BattleTransition:drawBgLayer(s) + if self.finished then return end + local world = self.world + if not (world and world.gbScreenOrigin) then return end + local G = love.graphics + local canvas = G.getCanvas() + local bw, bh + if canvas then + bw, bh = canvas:getDimensions() + else + bw, bh = Playfield.dimensions() + end + local sox, soy = world:gbScreenOrigin() + self:paintLayer(8 * s, sox * s, soy * s, bw, bh) + self.overlayDrawn = true +end + +function BattleTransition:ballImage() + local ramp = self:trainerRamp() + local cache = self.ballCache + if cache and cache.ramp == ramp and cache.mode == GbcPalette.mode + and cache.custom == GbcPalette.customRamp then + return cache.image + end + local previous = GbcPalette.setBgp(nil) + local ok, image = pcall(function() + local data = love.image.newImageData(8, 8) + for y = 1, 8 do + for x = 1, 8 do + local c = GbcPalette.color(ramp, tonumber(SQUARE_TILE[y]:sub(x, x)) + 1) + data:setPixel(x - 1, y - 1, c[1] / 255, c[2] / 255, c[3] / 255, 1) + end + end + local made = love.graphics.newImage(data) + if made.setFilter then made:setFilter("nearest", "nearest") end + return made + end) + GbcPalette.setBgp(previous) + if not ok then image = nil end + self.ballCache = { ramp = ramp, mode = GbcPalette.mode, + custom = GbcPalette.customRamp, image = image } + return image +end + +function BattleTransition:blackColor() + local ramp = self:trainerRamp() + if ramp then + local previous = GbcPalette.setBgp(nil) + local c = GbcPalette.color(ramp, 4) + GbcPalette.setBgp(previous) + if c then return c[1] / 255, c[2] / 255, c[3] / 255 end + end + return 0, 0, 0 +end + +function BattleTransition:paintLayer(size, ox, oy, w, h) + local G = love.graphics + if self.trainer then + local image = self:ballImage() + local scale = size / 8 + G.setColor(1, 1, 1, 1) + for _, cell in ipairs(BattleTransition.pokeballCells()) do + local x = math.floor(ox + cell[1] * size) + local y = math.floor(oy + cell[2] * size) + if image then + G.draw(image, x, y, 0, scale, scale) + else + local r, g, b = self:blackColor() + G.setColor(r, g, b, 1) + G.rectangle("fill", x, y, size, size) + G.setColor(1, 1, 1, 1) + end + end + end + if not next(self.black) then return end local first = -math.ceil(ox / size) local last = math.ceil((w - ox) / size) local top = -math.ceil(oy / size) local bottom = math.ceil((h - oy) / size) - G.setColor(0, 0, 0, 1) + G.setColor(self:blackColor()) for row = top, bottom - 1 do for col = first, last - 1 do if self:blackAt(col, row) then @@ -737,57 +878,25 @@ function BattleTransition:drawBlack(w, h) end end end -end - --- BATTLETRANSITION_SQUARE, the Poke Ball's own tile: a filled block in the --- text palette rather than the black the wipe uses, so the ball reads against --- the map behind it. -function BattleTransition:drawCells(w, h, cells, byte, ly) - local G = love.graphics - local size, ox, oy = self:grid(w, h) - -- Shade 3 of the text palette, through the COLOR mode like every other - -- direct colour read. - local ramp = self:trainerRamp() - local shade = BattleTransition.ballShade(byte) - local color = GbcPalette.color(ramp, shade) - if color then - G.setColor(color[1] / 255, color[2] / 255, color[3] / 255, 1) - else - G.setColor(0, 0, 0, 1) - end - local scale = math.max(1, size / 8) - for _, cell in ipairs(cells) do - local x, y = ox + cell[1] * size, oy + cell[2] * size - if not ly then - G.rectangle("fill", x, y, size, size) - else - local base = math.floor(y / scale) - for line = 0, 7 do - local row = math.max(0, math.min(143, base + line)) - local shift = (ly[row] or 0) * scale - local top = y + line * scale - G.rectangle("fill", x - shift, top, size, scale) - if shift > 0 then - G.rectangle("fill", x - shift + w, top, size, scale) - elseif shift < 0 then - G.rectangle("fill", x - shift - w, top, size, scale) - end - end - end - end + G.setColor(1, 1, 1, 1) end -- The sine outro shifts whole scanlines, which needs the frame as a texture: -- the world is captured once and re-blitted a row at a time from then on. function BattleTransition:drawWavy(w, h, ly) local G = love.graphics + local world = self.world + -- ../pokecrystal/engine/battle/battle_transition.asm:320-321 + local split = not (Tilt.active and Tilt.active()) + if split then world.peopleHidden = true end local canvas = self:capture(w, h) + world.peopleHidden = nil if not canvas then - self.world:draw() + self:drawWorld() return end local scale = 1 - if self.world.fitScale then scale = self.world:fitScale() end + if world.fitScale then scale = world:fitScale() end G.setColor(0, 0, 0, 1) G.rectangle("fill", 0, 0, w, h) G.setColor(1, 1, 1, 1) @@ -819,6 +928,11 @@ function BattleTransition:drawWavy(w, h, ly) else self:drawTint(w, h) end + self.overlayDrawn = self.captureLayered + if split and world.drawPeople and world.zoomScale then + G.setColor(1, 1, 1, 1) + world:drawPeople(world:zoomScale()) + end end function BattleTransition:capture(w, h) @@ -841,7 +955,7 @@ function BattleTransition:capture(w, h) G.origin() G.setCanvas(self.canvas) G.clear(0, 0, 0, 1) - self.world:draw() + self.captureLayered = self:drawWorld() G.setCanvas(previous) G.pop() self.captured = true diff --git a/src/ui/gen2/BlankScreen.lua b/src/ui/gen2/BlankScreen.lua index 43285b35..7cf30df3 100644 --- a/src/ui/gen2/BlankScreen.lua +++ b/src/ui/gen2/BlankScreen.lua @@ -40,10 +40,8 @@ function BlankScreen:drawWidescreen(winW, winH) end function BlankScreen:update() - if self.left > 0 then - self.left = self.left - 1 - return - end + self.left = self.left - 1 + if self.left > 0 then return end local done = self.onDone self.onDone = nil if done then done() end diff --git a/src/ui/gen2/CenterPcMenu.lua b/src/ui/gen2/CenterPcMenu.lua index ca665f63..3b8384ff 100644 --- a/src/ui/gen2/CenterPcMenu.lua +++ b/src/ui/gen2/CenterPcMenu.lua @@ -20,10 +20,12 @@ local Screens = require("src.ui.Screens") local Sound = require("src.core.Sound") local Specials = require("src.script.gen2.Specials") local Strings = require("src.core.Strings") +local Typer = require("src.ui.gen2.Typer") local CenterPcMenu = {} CenterPcMenu.__index = CenterPcMenu -CenterPcMenu.isOpaque = true +-- ../pokecrystal/engine/events/pokecenter_pc.asm:15 +CenterPcMenu.isOpaque = false -- ENGINE_POKEDEX (constants/engine_flags.asm, index 11): the flag -- CheckReceivedDex reads and Mr.Pokemon's `setflag ENGINE_POKEDEX` writes. @@ -32,7 +34,6 @@ CenterPcMenu.isOpaque = true local ENGINE_POKEDEX = 11 function CenterPcMenu:wantsFillScale() return true end -function CenterPcMenu:drawsWidescreen() return true end -- `\f` = para (home/text.asm:403 Paragraph), `\v` = cont (home/text.asm:442 -- _ContTextNoPause); two rows to a page (constants/text_constants.asm:32). @@ -81,6 +82,7 @@ function CenterPcMenu.new(game, opts) self.message = nil self.confirm = nil self.closed = false + self.booted = false self:buildEntries() local party = self.save and self.save.party if not (party and #party > 0) then @@ -93,7 +95,9 @@ function CenterPcMenu.new(game, opts) else -- PC_PlayBootSound + _PokecenterPCTurnOnText. self:playSfx("Sfx_BootPc") - self:say({ { "{PLAYER} turned on", "the PC." } }) + -- ../pokecrystal/engine/events/pokecenter_pc.asm:22 + self:say({ { "{PLAYER} turned on", "the PC." } }, + function() self.booted = true end) end return self end @@ -136,7 +140,9 @@ end -- played the moment it comes up (FindOakRating hands PlaySFX its fanfare -- right before the rating text prints). function CenterPcMenu:say(pages, onDone) - self.message = { pages = pages, page = 1, onDone = onDone } + Typer.say(self, pages, onDone, { expand = function(line) + return (line:gsub("{PLAYER}", self:playerName())) + end }) local first = pages[1] if first and first.sfx then self:playSfx(first.sfx) end end @@ -236,10 +242,12 @@ function CenterPcMenu:update(_dt) if not input then return end if self.message then + Typer.step(self) + if Typer.typing(self) then return end if input:wasPressed("a") or input:wasPressed("b") then local m = self.message if m.page < #m.pages then - m.page = m.page + 1 + Typer.turn(self, m) local page = m.pages[m.page] if page and page.sfx then self:playSfx(page.sfx) end return @@ -294,32 +302,28 @@ function CenterPcMenu:drawBottomLines(lines) end function CenterPcMenu:drawPanel() - Chrome.clear() + if self.booted then + self:drawBottomLines({ "Access whose PC?" }) + Chrome.box(0, 0, 16, math.max(12, #self.entries * 2 + 2)) + for i, entry in ipairs(self.entries) do + local ty = i * 2 + if i == self.index then Chrome.cursor(1, ty) end + Chrome.print(entry.label, 2, ty) + end + end if self.message then - self:drawBottomLines(self.message.pages[self.message.page]) - love.graphics.setColor(1, 1, 1, 1) - return - end - - -- _PokecenterPCWhoseText stays up under the menu - -- (PC_DisplayTextWaitMenu leaves it there); the menu window is drawn on - -- top of it, the way the cart's windows stack. - self:drawBottomLines({ "Access whose PC?" }) - -- .TopMenu is menu_coords 0, 0, 15, 12. - Chrome.box(0, 0, 16, math.max(12, #self.entries * 2 + 2)) - for i, entry in ipairs(self.entries) do - local ty = i * 2 - if i == self.index then Chrome.cursor(1, ty) end - Chrome.print(entry.label, 2, ty) - end - - if self.confirm then + -- ../pokecrystal/engine/events/pokecenter_pc.asm:652 + self:drawBottomLines( + Typer.text(self, self.message.pages[self.message.page])) + elseif self.confirm then self:drawBottomLines(self.confirm.prompt) Chrome.box(14, 7, 6, 5) Chrome.print("YES", 16, 8) Chrome.print("NO", 16, 10) Chrome.cursor(15, self.confirm.choice == 1 and 8 or 10) + elseif not self.booted then + self:drawBottomLines(nil) end love.graphics.setColor(1, 1, 1, 1) @@ -329,17 +333,6 @@ function CenterPcMenu:draw() self:drawPanel() end -function CenterPcMenu:drawWidescreen(winW, winH) - local G = love.graphics - Chrome.letterbox(winW, winH, 1, 1, 1) - local scale = Chrome.fitScale(winW, winH) - G.push() - G.translate(Chrome.fitOrigin(winW, winH, scale)) - G.scale(scale, scale) - self:drawPanel() - G.pop() -end - CenterPcMenu.ENGINE_POKEDEX = ENGINE_POKEDEX return CenterPcMenu diff --git a/src/ui/gen2/DayCareMenu.lua b/src/ui/gen2/DayCareMenu.lua index aba79844..6aec7dd2 100644 --- a/src/ui/gen2/DayCareMenu.lua +++ b/src/ui/gen2/DayCareMenu.lua @@ -50,6 +50,7 @@ local Chrome = require("src.ui.gen2.Chrome") local CommonText = require("src.core.gen2.CommonText") local Screens = require("src.ui.Screens") local Sound = require("src.core.Sound") +local Typer = require("src.ui.gen2.Typer") local DayCareMenu = {} DayCareMenu.__index = DayCareMenu @@ -306,12 +307,14 @@ end -- (`prompt` and `done` both park until A or B here, because the script that -- calls the special does `waitbutton` straight after). function DayCareMenu:say(list, onDone) - self.message = { pages = list or {}, page = 1, onDone = onDone } + -- ../pokecrystal/engine/events/daycare.asm:262 PrintDayCareText + Typer.say(self, list, onDone) end function DayCareMenu:ask(list, onYes, onNo) self.confirm = { pages = list or {}, page = 1, choice = 1, onYes = onYes, onNo = onNo } + Typer.begin(self, self.confirm) end function DayCareMenu:close() @@ -479,10 +482,12 @@ end -- ---------------------------------------------------------------- update function DayCareMenu:updateMessage(input) + Typer.step(self) + if Typer.typing(self) then return end if not (input:wasPressed("a") or input:wasPressed("b")) then return end local message = self.message if message.page < #message.pages then - message.page = message.page + 1 + Typer.turn(self, message) return end self.message = nil @@ -491,10 +496,12 @@ end function DayCareMenu:updateConfirm(input) local confirm = self.confirm + Typer.step(self) + if Typer.typing(self) then return end -- The yes/no box only comes up on the string's LAST page. if confirm.page < #confirm.pages then if input:wasPressed("a") or input:wasPressed("b") then - confirm.page = confirm.page + 1 + Typer.turn(self, confirm) end return end @@ -549,16 +556,17 @@ function DayCareMenu:drawYesNo(choice) end function DayCareMenu:drawPanel() + local typed = self.typer == nil or self.typer:done() if self.message then - self:drawTextBox(self.message.pages[self.message.page]) - if self.message.page < #self.message.pages then + self:drawTextBox(Typer.text(self, self.message.pages[self.message.page])) + if typed and self.message.page < #self.message.pages then Chrome.print(DOWN_ARROW, ARROW_X, ARROW_Y) end elseif self.confirm then - self:drawTextBox(self.confirm.pages[self.confirm.page]) + self:drawTextBox(Typer.text(self, self.confirm.pages[self.confirm.page])) if self.confirm.page >= #self.confirm.pages then self:drawYesNo(self.confirm.choice) - else + elseif typed then Chrome.print(DOWN_ARROW, ARROW_X, ARROW_Y) end else diff --git a/src/ui/gen2/DecorationMenu.lua b/src/ui/gen2/DecorationMenu.lua index 6cd77472..f0acbe96 100644 --- a/src/ui/gen2/DecorationMenu.lua +++ b/src/ui/gen2/DecorationMenu.lua @@ -22,10 +22,12 @@ local Chrome = require("src.ui.gen2.Chrome") local Decorations = require("src.core.gen2.Decorations") local Strings = require("src.core.Strings") +local Typer = require("src.ui.gen2.Typer") local DecorationMenu = {} DecorationMenu.__index = DecorationMenu -DecorationMenu.isOpaque = true +-- ../pokecrystal/engine/overworld/decorations.asm:8 +DecorationMenu.isOpaque = false -- .category_pointers' eighth row, which is not a category and is always shown. local EXIT = Strings.source("EXIT") @@ -98,14 +100,21 @@ function DecorationMenu:say(pages) if not pages or #pages == 0 then return end self.pages = pages self.pageIndex = 1 + -- ../pokecrystal/home/menu.asm:328 MenuTextboxBackup + self.typer = Typer.new(self.game) + self.typer:start(pages[1]) end -- MenuTextboxBackup returns to whatever menu was underneath, so a message is -- not a mode of its own: it sits over the list it was printed from. function DecorationMenu:advanceMessage() self.pageIndex = self.pageIndex + 1 - if self.pageIndex <= #self.pages then return end + if self.pageIndex <= #self.pages then + if self.typer then self.typer:start(self.pages[self.pageIndex]) end + return + end self.pages = nil + self.typer = nil end -- One row of the item list chosen: DoDecorationAction2. An ornament row asks @@ -147,6 +156,8 @@ function DecorationMenu:update(_dt) if not (input and not self.done) then return end if self.pages then + Typer.step(self) + if Typer.typing(self) then return end if input:wasPressed("a") or input:wasPressed("b") then self:advanceMessage() end @@ -223,8 +234,6 @@ function DecorationMenu:drawList(x, y, w, h, labels, index, scroll) end function DecorationMenu:drawPanel() - Chrome.clear() - if self.mode == "category" then local labels = {} for i, category in ipairs(self.categories) do labels[i] = category.label end @@ -248,7 +257,14 @@ function DecorationMenu:drawPanel() if self.pages then Chrome.box(0, 12, 20, 6) local line = 14 - for part in ((self.pages[self.pageIndex] or "") .. "\n"):gmatch("(.-)\n") do + local shown = self.typer and self.typer:lines() + if not shown then + shown = {} + for part in ((self.pages[self.pageIndex] or "") .. "\n"):gmatch("(.-)\n") do + shown[#shown + 1] = part + end + end + for _, part in ipairs(shown) do Chrome.print(part, 1, line) line = line + 2 end @@ -261,17 +277,4 @@ function DecorationMenu:draw() self:drawPanel() end -function DecorationMenu:drawWidescreen(winW, winH) - local G = love.graphics - Chrome.letterbox(winW, winH, 1, 1, 1) - local scale = Chrome.fitScale(winW, winH) - G.push() - G.translate(Chrome.fitOrigin(winW, winH, scale)) - G.scale(scale, scale) - self:drawPanel() - G.pop() -end - -function DecorationMenu:drawsWidescreen() return true end - return DecorationMenu diff --git a/src/ui/gen2/EggHatchAnim.lua b/src/ui/gen2/EggHatchAnim.lua index af8cde8a..4b0b7464 100644 --- a/src/ui/gen2/EggHatchAnim.lua +++ b/src/ui/gen2/EggHatchAnim.lua @@ -34,6 +34,7 @@ local Assets = require("src.render.Assets") local Chrome = require("src.ui.gen2.Chrome") local GbcPalette = require("src.render.GbcPalette") +local MonAnimView = require("src.render.MonAnimView") local Music = require("src.core.Music") local Palettes = require("src.world.gen2.Palettes") local Sound = require("src.core.Sound") @@ -187,12 +188,21 @@ function EggHatchAnim:buildBeats() self:initFragments() self.showMon = true end) - -- WaitSFX then `ld a, [wJumptableIndex] / call PlayMonCry`. The port has no - -- channel state to poll, so the cry simply follows the loop. - beat(0, function() self:playCry() end) + -- ../pokecrystal/engine/pokemon/breeding.asm:754-761 + beat(0, function() self:startPicAnim() end) return beats end +-- ../pokecrystal/engine/gfx/pic_animation.asm:74 +function EggHatchAnim:startPicAnim() + local def = self.species and self.data.pokemon + and self.data.pokemon[self.species] + self.picAnim = MonAnimView.start(def, self.mon, "hatch", + function(path) return self:image(path) end, + function() self:playCry() end) + if not self.picAnim then self:playCry() end +end + -------------------------------------------------------------------------- -- Sound -------------------------------------------------------------------------- @@ -289,6 +299,15 @@ end function EggHatchAnim:update(_dt) if self.done then return end + -- ../pokecrystal/engine/gfx/pic_animation.asm:79-89 + if self.picAnim then + if self.picAnim:step() then + self.picAnim = nil + return self:finish() + end + return + end + -- EggHatch_DoAnimFrame is PlaySpriteAnimations + DelayFrame, so the sprites -- advance on every frame the sequence spends anywhere. A fragment that has -- run out its amplitude leaves the screen: AnimSeq_RevealNewMon's @@ -310,6 +329,7 @@ function EggHatchAnim:update(_dt) if not beat then return self:finish() end self.beatLeft = beat.frames self:runBeat(beat) + if self.picAnim then return end end self.beatLeft = self.beatLeft - 1 end @@ -359,6 +379,10 @@ function EggHatchAnim:drawPic() if not image then return end local G = love.graphics local w = image:getWidth() + -- ../pokecrystal/engine/gfx/pic_animation.asm:431-435 + local sheet, quad, size + if self.picAnim then sheet, quad, size = self.picAnim:frame() end + if sheet then w = size end local tx = self.showMon and MON_TILE_X or EGG_TILE_X local ty = self.showMon and MON_TILE_Y or EGG_TILE_Y -- PadFrontpic's own placement, not a centring rule: the two agree at 7 and @@ -368,7 +392,10 @@ function EggHatchAnim:drawPic() local py = ty * 8 + pad[2] * 8 G.setColor(1, 1, 1, 1) local colors = self:picColors() - local function body() G.draw(image, px, py) end + local function body() + if sheet then return G.draw(sheet, quad, px, py) end + G.draw(image, px, py) + end if colors and GbcPalette.available() then GbcPalette.with(colors, body) else diff --git a/src/ui/gen2/EvolutionAnim.lua b/src/ui/gen2/EvolutionAnim.lua index eca843c5..aac1c431 100644 --- a/src/ui/gen2/EvolutionAnim.lua +++ b/src/ui/gen2/EvolutionAnim.lua @@ -31,6 +31,7 @@ local Chrome = require("src.ui.gen2.Chrome") local Evolution = require("src.core.gen2.Evolution") local GbcPalette = require("src.render.GbcPalette") local Mon = require("src.battle.gen2.Mon") +local MonAnimView = require("src.render.MonAnimView") local Music = require("src.core.Music") local Palettes = require("src.world.gen2.Palettes") local Sound = require("src.core.Sound") @@ -127,9 +128,47 @@ end -------------------------------------------------------------------------- function EvolutionAnim:playCry(species) - if not species then return end + if not species then return nil end local cries = self.data.audio and self.data.audio.cries - if cries and cries[species] then Sound.playCry(self.data, species) end + if not (cries and cries[species]) then return nil end + return Sound.playCry(self.data, species) +end + +-- ../pokecrystal/engine/pokemon/stats_screen.asm:1183-1195 +function EvolutionAnim:statused() + local mon = self.mon + if not mon then return false end + if (mon.hp or 0) <= 0 then return true end + return mon.status == "sleep" or mon.status == "freeze" +end + +-- ../pokecrystal/engine/movie/evolution_animation.asm:88-89 +function EvolutionAnim:startEvolutionMusic() + local songs = self.data.audio and self.data.audio.songs + if songs and songs.Music_Evolution then + Music.play(self.data, "Music_Evolution", true, { reason = "evolution" }) + end +end + +-- ../pokecrystal/home/pokemon.asm:124-127 +function EvolutionAnim:beginCry(species, after) + self.crySrc = self:playCry(species) + self.cryT = 0 + self.cryAfter = after + self.cryWait = self.crySrc ~= nil + if not self.cryWait then return after() end +end + +function EvolutionAnim:updateCry() + self.cryT = self.cryT + 1 + local src = self.crySrc + local playing = src and src.isPlaying and src:isPlaying() + if self.cryT >= 3 and (not playing or self.cryT > 180) then + self.cryWait, self.crySrc = false, nil + local after = self.cryAfter + self.cryAfter = nil + if after then after() end + end end function EvolutionAnim:playSfx(name) @@ -163,16 +202,13 @@ function EvolutionAnim:setPhase(phase) end if phase == "cry" then - -- PlayMonCry of the OLD species, then MUSIC_EVOLUTION, then 80 frames. - -- PlayMusic MUSIC_NONE ran first, so nothing else is sounding. + -- ../pokecrystal/engine/movie/evolution_animation.asm:82-92 Music.stop() - self:playCry(self.oldSpecies) - local songs = self.data.audio and self.data.audio.songs - if songs and songs.Music_Evolution then - Music.play(self.data, "Music_Evolution", true, { reason = "evolution" }) - end self.timer = Evolution.MUSIC_FRAMES - return + if self:statused() then return self:startEvolutionMusic() end + return self:beginCry(self.oldSpecies, function() + self:startEvolutionMusic() + end) end if phase == "flash" then @@ -192,16 +228,34 @@ function EvolutionAnim:setPhase(phase) self.blackout = false self.showNew = not self.canceled if self.canceled then - -- .PlayEvolvedSFX returns immediately once wEvolutionCanceled is set: - -- no SFX_EVOLVED and no balls of light, straight to the cry. - self:playCry(self.oldSpecies) - return self:setPhase("stopped") + -- ../pokecrystal/engine/movie/evolution_animation.asm:151-158 + if self:statused() then return self:setPhase("stopped") end + return self:beginCry(self.oldSpecies, function() + self:setPhase("stopped") + end) end self:playSfx("Sfx_Evolved") self.timer = Evolution.BALL_SPAWN_FRAMES + Evolution.BALL_TAIL_FRAMES return end + -- ../pokecrystal/engine/movie/evolution_animation.asm:113-130 + if phase == "picAnim" then + self.balls = {} + self.picAnim = nil + if not self:statused() then + self.picAnim = MonAnimView.start(self:speciesDef(self.newSpecies), + self.mon, "evolve", + function(path) return self:image(path) end, + function() self:playCry(self.newSpecies) end) + end + if self.picAnim then return end + if self:statused() then return self:setPhase("congrats") end + return self:beginCry(self.newSpecies, function() + self:setPhase("congrats") + end) + end + if phase == "stopped" then -- CancelEvolution: StoppedEvolvingText over the pic, then ClearTilemap. self.lines = { "Huh? " .. self.nick, "stopped evolving!" } @@ -332,6 +386,9 @@ function EvolutionAnim:update(_dt) return end + -- ../pokecrystal/home/pokemon.asm:124-127 + if self.cryWait then return self:updateCry() end + if phase == "flash" then return self:updateFlash(input) end @@ -339,15 +396,18 @@ function EvolutionAnim:update(_dt) if phase == "reveal" then self:updateBalls() self.timer = self.timer - 1 - if self.timer <= 0 then - -- ClearSpriteAnims, then PlayMonCry of the species the screen is now - -- showing. - self:playCry(self.newSpecies) - self:setPhase("congrats") - end + -- ../pokecrystal/engine/movie/evolution_animation.asm:113-114 + if self.timer <= 0 then self:setPhase("picAnim") end return end + -- ../pokecrystal/engine/gfx/pic_animation.asm:79-89 + if phase == "picAnim" then + if self.picAnim and not self.picAnim:step() then return end + self.picAnim = nil + return self:setPhase("congrats") + end + self.timer = (self.timer or 0) - 1 local prompt = phase == "stopped" or phase == "congrats" or phase == "learn" if prompt and input @@ -472,9 +532,11 @@ end -- Draw -------------------------------------------------------------------------- -function EvolutionAnim:pic(species) - local def = species and self.data.pokemon and self.data.pokemon[species] - local path = def and def.spriteFront +function EvolutionAnim:speciesDef(species) + return species and self.data.pokemon and self.data.pokemon[species] or nil +end + +function EvolutionAnim:image(path) if not path then return nil end local cached = self.picCache[path] if cached == nil then @@ -487,6 +549,11 @@ function EvolutionAnim:pic(species) return cached or nil end +function EvolutionAnim:pic(species) + local def = self:speciesDef(species) + return self:image(def and def.spriteFront) +end + -- The four colours the box draws through right now: the mon's own while the -- SGB layout is c = FALSE, PREDEFPAL_BLACKOUT while it is TRUE. -- Shininess is a DV pattern, and the DVs survive the evolution, so both stages @@ -503,6 +570,10 @@ function EvolutionAnim:drawPic() if not image then return end local G = love.graphics local w, h = image:getDimensions() + -- ../pokecrystal/engine/gfx/pic_animation.asm:431-435 + local sheet, quad, size + if self.picAnim then sheet, quad, size = self.picAnim:frame() end + if sheet then w, h = size, size end -- PlaceGraphic pads the pic into the 7x7 box bottom-first, so a 40x40 -- Cyndaquil stands on the same ground line a 56x56 Onix does. local box = PIC_TILES * 8 @@ -510,7 +581,10 @@ function EvolutionAnim:drawPic() local py = PIC_TILE_Y * 8 + (box - h) G.setColor(1, 1, 1, 1) local colors = self:picColors(species) - local function body() G.draw(image, px, py) end + local function body() + if sheet then return G.draw(sheet, quad, px, py) end + G.draw(image, px, py) + end if colors and GbcPalette.available() then GbcPalette.with(colors, body) else diff --git a/src/ui/gen2/HallOfFame.lua b/src/ui/gen2/HallOfFame.lua index fa6ae2fe..dffcdb51 100644 --- a/src/ui/gen2/HallOfFame.lua +++ b/src/ui/gen2/HallOfFame.lua @@ -47,6 +47,7 @@ local Core = require("src.core.gen2.HallOfFame") local Font = require("src.render.Font") local GbcPalette = require("src.render.GbcPalette") local Gen2Save = require("src.core.gen2.Save") +local MonAnimView = require("src.render.MonAnimView") local Music = require("src.core.Music") local Palettes = require("src.world.gen2.Palettes") local Sound = require("src.core.Sound") @@ -75,6 +76,8 @@ local TRAINER_SCX_START = 0xc0 -- .DisplayNewHallOfFamer: `ld c, 180 / call DelayFrames` after the cry. local FAMER_FRAMES = 180 +-- ../pokecrystal/engine/events/halloffame.asm:126-130 +local ANIM_FAMER_FRAMES = 60 -- AnimateHallOfFame .done: RotateThreePalettesRight, then `ld c, 8`. local END_FRAMES = 8 @@ -178,7 +181,7 @@ end function HallOfFame.headerPlacements(mode, winCount, text) local out = {} local function line(label, fallback) - local extracted = CommonText.get(text, label) + local extracted = CommonText.plain(CommonText.get(text, label)) return extracted or fallback end if mode == "induct" then @@ -353,13 +356,38 @@ function HallOfFame:enterView() self.phase = "display" self.scx, self.scy = 0, 0 self:playCry(mon.species) + self:startPicAnim() end function HallOfFame:enterDisplay() self.phase = "display" self.scx, self.scy = 0, 0 - self.timer = FAMER_FRAMES self:playCry((self:currentMon() or {}).species) + self:startPicAnim() + self.timer = self.picAnim and ANIM_FAMER_FRAMES or FAMER_FRAMES +end + +-- ../pokecrystal/engine/events/halloffame.asm:128, :394-395 +-- ../pokegold/engine/events/halloffame.asm:124-125 +function HallOfFame:startPicAnim() + local mon = self:currentMon() + self.picAnim = MonAnimView.start( + mon and self:speciesDef(mon.species), mon, "hof", + function(path) return self:image(path) end) +end + +-- ../pokecrystal/engine/gfx/pic_animation.asm:79-89 +function HallOfFame:stepPicAnim() + local anim = self.picAnim + if not anim then return false end + if anim:step() then self.picAnim = nil end + return true +end + +function HallOfFame:picAnimFrame() + local anim = self.picAnim + if not anim then return nil end + return anim:frame() end -- HOF_AnimatePlayerPic, which is where AnimateHallOfFame's .done arm lands @@ -420,6 +448,7 @@ function HallOfFame:step() end if self.phase == "display" then + if self:stepPicAnim() then return false end if self.mode == "view" then return false end self.timer = self.timer - 1 if self.timer > 0 then return false end @@ -490,6 +519,8 @@ function HallOfFame:update(_dt) if self.done then return end if self.mode == "view" then self.frames = self.frames + 1 + -- ../pokecrystal/engine/events/halloffame.asm:313-317 + if self:stepPicAnim() then return end return self:viewInput(self.game and self.game.input) end self:step() @@ -544,10 +575,10 @@ end -- Draw an image at a tile coordinate through the current scroll, padded into -- the 7x7 block the way PlaceGraphic pads it. -function HallOfFame:drawScrolled(image, tileX, tileY, colors) +function HallOfFame:drawScrolled(image, tileX, tileY, colors, quad, size) if not image then return end local G = love.graphics - local wide = math.floor(image:getWidth() / 8) + local wide = math.floor((size or image:getWidth()) / 8) local pad = PIC_PAD[wide] or PIC_PAD[PIC_TILES] local baseX = tileX * 8 + pad[1] * 8 local baseY = tileY * 8 + pad[2] * 8 @@ -557,7 +588,7 @@ function HallOfFame:drawScrolled(image, tileX, tileY, colors) local function body() for _, x in ipairs({ x1, x2 }) do for _, y in ipairs({ y1, y2 }) do - G.draw(image, x, y) + if quad then G.draw(image, quad, x, y) else G.draw(image, x, y) end end end end @@ -611,8 +642,14 @@ function HallOfFame:drawMonPanel() Chrome.textbox(0, 0, 18, 3) Chrome.textbox(0, 12, 18, 4) local def = mon and self:speciesDef(mon.species) - self:drawScrolled(self:monPic(mon, false), - FRONTPIC_X, FRONTPIC_Y, self:monColors(mon)) + local sheet, quad, size = self:picAnimFrame() + if sheet then + self:drawScrolled(sheet, FRONTPIC_X, FRONTPIC_Y, self:monColors(mon), + quad, size) + else + self:drawScrolled(self:monPic(mon, false), + FRONTPIC_X, FRONTPIC_Y, self:monColors(mon)) + end self:drawPlacements(HallOfFame.headerPlacements(self.mode, self.entry and self.entry.winCount, self.textData)) self:drawPlacements(HallOfFame.monPlacements(mon, def)) @@ -673,6 +710,7 @@ end HallOfFame.WHITE = WHITE HallOfFame.PIC_PAD = PIC_PAD HallOfFame.FAMER_FRAMES = FAMER_FRAMES +HallOfFame.ANIM_FAMER_FRAMES = ANIM_FAMER_FRAMES HallOfFame.END_FRAMES = END_FRAMES HallOfFame.SCY_START = SCY_START HallOfFame.BACKPIC_SCX_START = BACKPIC_SCX_START diff --git a/src/ui/gen2/ItemPcMenu.lua b/src/ui/gen2/ItemPcMenu.lua index 5a294628..573ea72b 100644 --- a/src/ui/gen2/ItemPcMenu.lua +++ b/src/ui/gen2/ItemPcMenu.lua @@ -32,11 +32,16 @@ local PcItems = require("src.core.gen2.PcItems") local Runtime = require("src.mods.Runtime") local Screens = require("src.ui.Screens") local Sound = require("src.core.Sound") +local Typer = require("src.ui.gen2.Typer") local WaitPlaySFX = require("src.ui.gen2.WaitPlaySFX") local ItemPcMenu = {} ItemPcMenu.__index = ItemPcMenu -ItemPcMenu.isOpaque = true +-- ../pokecrystal/engine/events/pokecenter_pc.asm:231 +ItemPcMenu.isOpaque = false + +-- ../pokecrystal/engine/events/pokecenter_pc.asm:330 +local CLEARS_SCREEN = { withdraw = true, deposit = true, toss = true } -- MAX_PC_ITEMS stacks of at most MAX_ITEM_STACK (constants/item_constants.asm), -- the same pair src/core/gen2/MomShopping.lua enforces for Mom's deliveries. @@ -72,7 +77,11 @@ local LIST_X = 5 local TIMES = "\xc3\x97" function ItemPcMenu:wantsFillScale() return true end -function ItemPcMenu:drawsWidescreen() return true end + +function ItemPcMenu:setPhase(phase) + self.phase = phase + self.isOpaque = CLEARS_SCREEN[phase] or false +end -- opts: save, items (items.lua), house (PLAYERSPC_HOUSE: boot text, -- DECORATION row, TURN OFF), events (wEventFlags, for the decoration @@ -111,7 +120,7 @@ function ItemPcMenu.new(game, opts) entries[#entries + 1] = self.house and TURN_OFF or LOG_OFF self.entries = entries self.index = 1 - self.phase = "menu" + self:setPhase("menu") self.rows = {} self.listIndex = 1 self.scroll = 0 @@ -164,7 +173,9 @@ end -- one runs onDone. The item PC's messages never log off by themselves, which -- is _PlayersPC's `.loop`: a refusal drops back into the same menu. function ItemPcMenu:say(pages, onDone) - self.message = { pages = pages, page = 1, onDone = onDone } + Typer.say(self, pages, onDone, { expand = function(line) + return (line:gsub("{PLAYER}", self:playerName())) + end }) end function ItemPcMenu:close() @@ -302,7 +313,7 @@ end function ItemPcMenu:chooseWithdraw() local row = self.rows[self.listIndex] if not row then - self.phase = "menu" + self:setPhase("menu") return end -- .Submenu: an item without a quantity attribute (a KEY ITEM in the PC) is @@ -332,7 +343,7 @@ function ItemPcMenu:enterDeposit() self:say({ { "No items here!" } }) return end - self.phase = "deposit" + self:setPhase("deposit") -- DepositSellPack: the PACK as a chooser, held and drawn by this screen the -- way the mart holds its sell PACK. `world = {}` keeps field items inert. self.pack = Screens.build(self.game, "Gen2PackMenu", { @@ -346,7 +357,7 @@ end function ItemPcMenu:leaveDeposit() self.pack = nil - self.phase = "menu" + self:setPhase("menu") end function ItemPcMenu:offerToDeposit(id, count) @@ -367,7 +378,7 @@ end function ItemPcMenu:chooseToss() local row = self.rows[self.listIndex] if not row then - self.phase = "menu" + self:setPhase("menu") return end -- TossItemFromPC .key_item -> .CantToss. @@ -398,7 +409,7 @@ function ItemPcMenu:choose() if not entry then return end local game = self.game if entry.id == "withdraw" or entry.id == "toss" then - self.phase = entry.id + self:setPhase(entry.id) self.listIndex = 1 self.scroll = 0 -- engine/events/pokecenter_pc.asm:569 @@ -484,10 +495,12 @@ function ItemPcMenu:update(_dt) if self:tickRepeatSfx() then return end if self.message then + Typer.step(self) + if Typer.typing(self) then return end if input:wasPressed("a") or input:wasPressed("b") then local m = self.message if m.page < #m.pages then - m.page = m.page + 1 + Typer.turn(self, m) return end self.message = nil @@ -540,7 +553,7 @@ function ItemPcMenu:update(_dt) if self.pack then self.pack:update(_dt) else - self.phase = "menu" + self:setPhase("menu") end return end @@ -561,7 +574,7 @@ function ItemPcMenu:update(_dt) elseif input:wasPressed("b") then -- engine/menus/scrolling_menu.asm:24 self:playSfx("Sfx_ReadText2") - self.phase = "menu" + self:setPhase("menu") self.switching = nil elseif input:wasPressed("a") then self:playSfx("Sfx_ReadText2") @@ -606,7 +619,9 @@ end function ItemPcMenu:drawList() Chrome.box(0, 0, 20, 12) -- engine/events/pokecenter_pc.asm:628 .a_1 -> home/menu.asm:50 - local picked = (self.message or self.qtyState or self.confirm) and true or false + -- engine/events/pokecenter_pc.asm:605 .moving_stuff_around + local picked = not self.switching + and (self.message or self.qtyState or self.confirm) and true or false for row = 1, VISIBLE_ROWS do local i = row + self.scroll local ty = row * 2 @@ -644,7 +659,8 @@ function ItemPcMenu:drawList() end function ItemPcMenu:drawPanel() - Chrome.clear() + -- ../pokecrystal/engine/pokemon/bills_pc_top.asm:231 + if self.isOpaque then Chrome.clear() end if self.phase == "deposit" and self.pack then self.pack:drawPanel() @@ -679,7 +695,8 @@ function ItemPcMenu:drawPanel() Chrome.print("NO", 16, 10) Chrome.cursor(15, self.confirm.choice == 1 and 8 or 10) elseif self.message then - self:drawBottomLines(self.message.pages[self.message.page]) + self:drawBottomLines( + Typer.text(self, self.message.pages[self.message.page])) end love.graphics.setColor(1, 1, 1, 1) @@ -689,17 +706,6 @@ function ItemPcMenu:draw() self:drawPanel() end -function ItemPcMenu:drawWidescreen(winW, winH) - local G = love.graphics - Chrome.letterbox(winW, winH, 1, 1, 1) - local scale = Chrome.fitScale(winW, winH) - G.push() - G.translate(Chrome.fitOrigin(winW, winH, scale)) - G.scale(scale, scale) - self:drawPanel() - G.pop() -end - ItemPcMenu.ENTRIES = ENTRIES ItemPcMenu.PC_ITEM_CAPACITY = PC_ITEM_CAPACITY diff --git a/src/ui/gen2/MainMenu.lua b/src/ui/gen2/MainMenu.lua index 0092de39..0c22bb2a 100644 --- a/src/ui/gen2/MainMenu.lua +++ b/src/ui/gen2/MainMenu.lua @@ -8,7 +8,6 @@ -- needs the link cable, so it is not offered here. -- -- With a save present the menu also shows the clock box --- (MainMenu_PrintCurrentTimeAndDay): a 4x13 textbox at (0,12) with the day of -- the week and the current time. Its .PlaceTime calls UpdateTime before it -- reads hHours, so it prints the GAME clock -- the RTC through the save's own -- wStartHour / wStartMinute base -- and not the raw RTC. That is the same @@ -20,12 +19,18 @@ local Chrome = require("src.ui.gen2.Chrome") local Clock = require("src.core.gen2.Clock") +local InitClock = require("src.ui.gen2.InitClock") local Logger = require("src.core.Logger") local Music = require("src.core.Music") local Runtime = require("src.mods.Runtime") local Save = require("src.core.gen2.Save") +local SaveMenu = require("src.ui.gen2.SaveMenu") local Strings = require("src.core.Strings") +-- ../pokecrystal/engine/menus/intro_menu.asm:487 +local PANEL = SaveMenu.PANEL +local PANEL_Y = 8 + local MainMenu = {} MainMenu.__index = MainMenu MainMenu.isOpaque = true @@ -162,36 +167,40 @@ function MainMenu:clockParts() return Clock.hour(save), Clock.minute(save), Clock.weekday(save) + 1 end +-- ../pokecrystal/engine/rtc/timeset.asm:675 +function MainMenu.timeString(hour, minute) + return InitClock.timeString(hour, minute) +end + function MainMenu:drawClockBox() - -- Textbox at (0,12) with 4 interior rows and 13 interior columns. - Chrome.textbox(0, 12, 13, 4) + -- ../pokecrystal/engine/menus/main_menu.asm:286 + Chrome.textbox(0, 14, 18, 2) local hour, minute, weekday = self:clockParts() - Chrome.print(Clock.weekdayName(weekday) or "DAY", 1, 14) - -- PrintHour prints 1-12 with no leading zero, then ':' then two zero-padded - -- minutes; the AM/PM half is drawn by PrintHour itself. - local display = hour % 12 - if display == 0 then display = 12 end - local half = Strings(hour < 12 and "AM" or "PM") - Chrome.print(("%s:%s %s"):format( - Chrome.number(display, 2), Chrome.number(minute, 2, true), half), 4, 16) + Chrome.print(Clock.weekdayName(weekday) or "DAY", 1, 15) + Chrome.print(MainMenu.timeString(hour, minute), 4, 16) end function MainMenu:drawSavePanel() local summary = Save.summary(self.save) - -- DisplaySaveInfoOnContinue: a box down the right side listing the trainer. - Chrome.textbox(4, 0, 14, 9) + Chrome.box(PANEL.x, PANEL_Y, PANEL.w, PANEL.h) if not summary then - Chrome.print(Strings("NO SAVE FILE"), 5, 2) + Chrome.print(Strings("NO SAVE FILE"), PANEL.labelX, PANEL_Y + PANEL.labelDy) return end - Chrome.print(Strings("PLAYER %s", summary.name), 5, 2) - Chrome.print(Strings("BADGES"), 5, 4) - Chrome.printRight(tostring(summary.badges), 17, 4) - Chrome.print(Strings("POKรฉDEX"), 5, 6) - Chrome.printRight(tostring(summary.caught), 17, 6) - Chrome.print(Strings("TIME"), 5, 8) - Chrome.printRight(("%d:%s"):format( - summary.hours, Chrome.number(summary.minutes, 2, true)), 17, 8) + local labelY = PANEL_Y + PANEL.labelDy + Chrome.print(Strings("PLAYER %s", summary.name), PANEL.labelX, labelY) + Chrome.print(Strings("BADGES"), PANEL.labelX, labelY + 2) + Chrome.print(Strings("POKรฉDEX"), PANEL.labelX, labelY + 4) + Chrome.print(Strings("TIME"), PANEL.labelX, labelY + 6) + -- ../pokecrystal/engine/menus/intro_menu.asm:555 + Chrome.print(Chrome.number(summary.badges, 2), PANEL.badgesX, + PANEL_Y + PANEL.badgesDy) + Chrome.print(Chrome.number(summary.caught, 3), PANEL.dexX, + PANEL_Y + PANEL.dexDy) + local timeY = PANEL_Y + PANEL.timeDy + Chrome.print(Chrome.number(summary.hours, 3), PANEL.timeX, timeY) + Chrome.print(":", PANEL.timeX + 3, timeY) + Chrome.print(Chrome.number(summary.minutes, 2, true), PANEL.timeX + 4, timeY) end function MainMenu:drawPanel() @@ -200,10 +209,9 @@ function MainMenu:drawPanel() self:drawSavePanel() return end - -- MenuHeader: menu_coords 0, 0, 14, 7 -- a box from (0,0) to (14,7), which -- is exactly two rows per entry plus the border. The extra EXIT GAME row -- grows it the way AutomaticGetMenuBottomCoord would. - Chrome.box(0, 0, 15, + Chrome.box(0, 0, 17, math.min(#self.list.items * 2 + 2, Chrome.SCREEN_H)) self.list:draw() if self.hasSave then self:drawClockBox() end @@ -225,5 +233,7 @@ function MainMenu:drawWidescreen(winW, winH) end MainMenu.DAYS = DAYS +MainMenu.PANEL = PANEL +MainMenu.PANEL_Y = PANEL_Y return MainMenu diff --git a/src/ui/gen2/MartMenu.lua b/src/ui/gen2/MartMenu.lua index 45e7aa29..4552e838 100644 --- a/src/ui/gen2/MartMenu.lua +++ b/src/ui/gen2/MartMenu.lua @@ -68,6 +68,7 @@ local CommonText = require("src.core.gen2.CommonText") local Save = require("src.core.gen2.Save") local Screens = require("src.ui.Screens") local Sound = require("src.core.Sound") +local Typer = require("src.ui.gen2.Typer") -- PlayTransactionSound (engine/items/mart.asm): `call WaitSFX` then -- SFX_TRANSACTION. Both tills ring it -- the buy flow at BuyMenuLoop's @@ -471,14 +472,17 @@ end -- mart.asm: the box holds until a button, and multi-page strings advance a -- page per press. function MartMenu:say(list, onDone) - self.message = { pages = list or {}, page = 1, onDone = onDone } + -- ../pokecrystal/engine/items/mart.asm:167 + Typer.say(self, list, onDone) end function MartMenu:updateMessage(input) + Typer.step(self) + if Typer.typing(self) then return end if not (input:wasPressed("a") or input:wasPressed("b")) then return end local message = self.message if message.page < #message.pages then - message.page = message.page + 1 + Typer.turn(self, message) return end self.message = nil @@ -490,13 +494,16 @@ end function MartMenu:ask(list, onYes, onNo) self.confirm = { pages = list or {}, page = 1, choice = 1, onYes = onYes, onNo = onNo } + Typer.begin(self, self.confirm) end function MartMenu:updateConfirm(input) local confirm = self.confirm + Typer.step(self) + if Typer.typing(self) then return end if confirm.page < #confirm.pages then if input:wasPressed("a") or input:wasPressed("b") then - confirm.page = confirm.page + 1 + Typer.turn(self, confirm) end return end @@ -1002,13 +1009,14 @@ function MartMenu:drawPanel() end if self.message then - self:drawTextBox(self.message.pages[self.message.page]) + self:drawTextBox(Typer.text(self, self.message.pages[self.message.page])) -- LoadBlinkingCursor puts the โ–ผ at hlcoord 18, 17 while a `para` waits. - if self.message.page < #self.message.pages then + if self.message.page < #self.message.pages + and (self.typer == nil or self.typer:done()) then Chrome.print(DOWN_ARROW, 18, 17) end elseif self.confirm then - self:drawTextBox(self.confirm.pages[self.confirm.page]) + self:drawTextBox(Typer.text(self, self.confirm.pages[self.confirm.page])) if self.confirm.page >= #self.confirm.pages then self:drawYesNo(self.confirm.choice) end diff --git a/src/ui/gen2/MenuFade.lua b/src/ui/gen2/MenuFade.lua new file mode 100644 index 00000000..e029381d --- /dev/null +++ b/src/ui/gen2/MenuFade.lua @@ -0,0 +1,165 @@ +-- ../pokecrystal/home/map.asm:1910-1917 FadeToMenu +-- ../pokecrystal/home/map.asm:1919-1940 CloseSubmenu / FinishExitMenu + +local Chrome = require("src.ui.gen2.Chrome") + +local MenuFade = {} +MenuFade.__index = MenuFade +MenuFade.isOpaque = false +MenuFade.BG_WORLD_DIM = 0 + +-- ../pokecrystal/engine/tilesets/timeofday_pals.asm:122-127, :277-287 +local OUT_STEPS = 4 +local STEP_FRAMES = 2 +local OUT_FRAMES = OUT_STEPS * STEP_FRAMES +-- ../pokecrystal/engine/tilesets/timeofday_pals.asm:115-120, :289-299 +local IN_STEPS = 3 +local IN_RAMP_FRAMES = IN_STEPS * STEP_FRAMES + +-- ../pokecrystal/engine/gfx/load_font.asm:67-89 +-- ../pokecrystal/engine/gfx/dma_transfer.asm:483, :548 +local PARTY_FONT_FRAMES = 3 +-- ../pokecrystal/engine/menus/start_menu.asm:503-518 +local PARTY_WHITE = 4 + PARTY_FONT_FRAMES + 4 +-- ../pokecrystal/engine/gfx/mon_icons.asm:287-297 +local PARTY_ICON_FRAMES = 3 +-- ../pokecrystal/engine/items/pack.asm:1330-1365, :1439-1444 +local PACK_RELOAD_FRAMES = 2 +local PACK_WHITE = 4 + 4 + PACK_RELOAD_FRAMES + 2 + 4 +-- ../pokecrystal/engine/pokegear/pokegear.asm:73-111, :291-301 +local GEAR_RELOAD_FRAMES = 5 +local GEAR_WHITE = 4 + 4 + GEAR_RELOAD_FRAMES + 7 +-- ../pokecrystal/engine/pokegear/pokegear.asm:53-63 +local GEAR_EXIT_WHITE = 4 +-- ../pokecrystal/engine/menus/trainer_card.asm:41-72 +local CARD_RELOAD_FRAMES = 2 +local CARD_WHITE = 4 + 4 + CARD_RELOAD_FRAMES + 4 +-- ../pokecrystal/engine/pokedex/pokedex.asm:44, :78-81, :216-252, :2422-2450, :2566-2573 +local DEX_RELOAD_FRAMES = 6 +local DEX_WHITE = 4 + 4 + DEX_RELOAD_FRAMES + 1 + 8 + 4 +-- ../pokecrystal/engine/menus/options_menu.asm:19-51 +local OPTION_WHITE = 4 + 4 + +MenuFade.OUT_FRAMES = OUT_FRAMES +MenuFade.OUT_WHITE_FRAMES = STEP_FRAMES +MenuFade.IN_RAMP_FRAMES = IN_RAMP_FRAMES +MenuFade.PARTY_FONT_FRAMES = PARTY_FONT_FRAMES +MenuFade.PARTY_WHITE = PARTY_WHITE +MenuFade.PARTY_ICON_FRAMES = PARTY_ICON_FRAMES +MenuFade.PACK_RELOAD_FRAMES = PACK_RELOAD_FRAMES +MenuFade.PACK_WHITE = PACK_WHITE +MenuFade.GEAR_RELOAD_FRAMES = GEAR_RELOAD_FRAMES +MenuFade.GEAR_WHITE = GEAR_WHITE +MenuFade.GEAR_EXIT_WHITE = GEAR_EXIT_WHITE +MenuFade.CARD_RELOAD_FRAMES = CARD_RELOAD_FRAMES +MenuFade.CARD_WHITE = CARD_WHITE +MenuFade.DEX_RELOAD_FRAMES = DEX_RELOAD_FRAMES +MenuFade.DEX_WHITE = DEX_WHITE +MenuFade.OPTION_WHITE = OPTION_WHITE + +-- ../pokecrystal/engine/menus/start_menu.asm:444-518 +local OPEN_WHITE = { + pokemon = function(partySize) + return PARTY_WHITE + PARTY_ICON_FRAMES * (partySize or 0) + end, + pack = function() return PACK_WHITE end, + pokegear = function() return GEAR_WHITE end, + status = function() return CARD_WHITE end, + pokedex = function() return DEX_WHITE end, + option = function() return OPTION_WHITE end, +} + +function MenuFade.openWhite(id, partySize) + local page = OPEN_WHITE[id] + return page and page(partySize) or nil +end + +function MenuFade.exitWhite() + return require("src.world.gen2.World").MENU_EXIT_WHITE_FRAMES +end + +function MenuFade.closeWhite(id) + if not OPEN_WHITE[id] then return nil end + local white = MenuFade.exitWhite() + if id == "pokegear" then white = white + GEAR_EXIT_WHITE end + return white +end + +function MenuFade.framesOut(white) return OUT_FRAMES + white end +function MenuFade.framesIn(white) return white + IN_RAMP_FRAMES end + +function MenuFade.new(game, opts) + opts = opts or {} + local kind = opts.kind or "out" + local white = opts.white or 0 + local self = setmetatable({ + game = game, + kind = kind, + white = white, + onDone = opts.onDone, + frame = 1, + }, MenuFade) + self.total = (kind == "out") and MenuFade.framesOut(white) + or MenuFade.framesIn(white) + return self +end + +function MenuFade:level() + local f = self.frame + if self.kind == "out" then + if f >= OUT_FRAMES then return 1 end + return math.ceil(f / STEP_FRAMES) / OUT_STEPS + end + if f <= self.white then return 1 end + local k = math.ceil((f - self.white) / STEP_FRAMES) + return math.max(0, (OUT_STEPS - k) / OUT_STEPS) +end + +function MenuFade:update() + self.frame = self.frame + 1 + if self.frame <= self.total then return end + local done = self.onDone + self.onDone = nil + local stack = self.game and self.game.stack + if stack and stack:top() == self then stack:pop() end + if done then done() end +end + +function MenuFade:bgMode() return "world" end + +function MenuFade:drawsWidescreen() return true end + +local function sheet(w, h, a) + local G = love.graphics + G.setColor(1, 1, 1, a) + G.rectangle("fill", 0, 0, w, h) + G.setColor(1, 1, 1, 1) +end + +function MenuFade:drawWidescreen(w, h) + local G = love.graphics + local stack = self.game and self.game.stack + if stack and stack.states then + local world = self.game.world + local scale = (world and world.fitScale) and world:fitScale() + or Chrome.fitScale(w, h) + local ox, oy = Chrome.fitOrigin(w, h, scale) + G.push() + G.translate(ox, oy) + G.scale(scale, scale) + for i = stack:visibleBase(), #stack.states do + local state = stack.states[i] + if state ~= self and state.draw and stack:renderVisible(state) then + state:draw() + end + end + G.pop() + end + sheet(w, h, self:level()) +end + +function MenuFade:draw() + sheet(Chrome.SCREEN_W * 8, Chrome.SCREEN_H * 8, self:level()) +end + +return MenuFade diff --git a/src/ui/gen2/OptionsMenu.lua b/src/ui/gen2/OptionsMenu.lua index b9e61ca9..ccfb41ba 100644 --- a/src/ui/gen2/OptionsMenu.lua +++ b/src/ui/gen2/OptionsMenu.lua @@ -461,6 +461,7 @@ local function buildRows() local osName = love.system and love.system.getOS and love.system.getOS() local showTouch = env == "1" or (env ~= "0" and (osName == "Android" or osName == "iOS")) + local isNX = require("src.core.Platform").isNX() for i, row in ipairs(ROWS) do -- PRINT is wGBPrinterBrightness, how dark the Game Boy Printer prints -- (GBPRINTER_LIGHTEST..DARKEST, constants/ram_constants.asm:67-70). @@ -470,6 +471,7 @@ local function buildRows() -- The descriptor and the save key stay, so a build that grows a printer -- only has to drop this test. local hidden = row.key == "print" + or (isNX and row.key == "videoMode") or (not showTouch and (row.id == "touchControls" or row.id == "touchLayout" or row.id == "haptics" or row.id == "hotbar")) diff --git a/src/ui/gen2/PackMenu.lua b/src/ui/gen2/PackMenu.lua index abd9c217..faf064e1 100644 --- a/src/ui/gen2/PackMenu.lua +++ b/src/ui/gen2/PackMenu.lua @@ -401,6 +401,11 @@ function PackMenu:exitToField() local stack = self.game and self.game.stack if stack and stack.clear then stack:clear() + -- ../pokecrystal/home/map.asm:1927-1940 + local world = self.game.world + if world and world.exitMenusFade and not world.mapSetup then + world:exitMenusFade() + end elseif self.onClose then -- No clear on this stack (a test harness, or a screen pushed on its own): -- at least give the pack back. @@ -1062,7 +1067,9 @@ end -- row SELECT armed with the hollow โ–ท while the solid โ–ถ goes on looking. function PackMenu:drawList(listX, listY) -- engine/menus/scrolling_menu.asm:86 .a_button -> home/menu.asm:50 - local picked = (self.submenu or self.qtyState or self.confirm or self.message) + -- engine/items/pack.asm:1301 .select + local picked = not self.switching + and (self.submenu or self.qtyState or self.confirm or self.message) and true or false for row = 1, VISIBLE_ROWS do local i = row + self.scroll diff --git a/src/ui/gen2/PartyMenu.lua b/src/ui/gen2/PartyMenu.lua index 64bb9a94..03300d20 100644 --- a/src/ui/gen2/PartyMenu.lua +++ b/src/ui/gen2/PartyMenu.lua @@ -17,6 +17,7 @@ local Assets = require("src.render.Assets") local Chrome = require("src.ui.gen2.Chrome") +local FieldMoves = require("src.world.gen2.FieldMoves") local Font = require("src.render.Font") local GbcPalette = require("src.render.GbcPalette") local HpBar = require("src.battle.gen2.HpBar") @@ -27,6 +28,7 @@ local Mon = require("src.battle.gen2.Mon") local Runtime = require("src.mods.Runtime") local Screens = require("src.ui.Screens") local Sound = require("src.core.Sound") +local Strings = require("src.core.Strings") local PartyMenu = {} PartyMenu.__index = PartyMenu @@ -139,6 +141,9 @@ function PartyMenu.new(game, opts) self.submenu = nil -- The held slot while SwitchPartyMons' second pick is open; nil otherwise. self.switchFrom = nil + -- ../pokecrystal/engine/items/item_effects.asm:2016 + self.softboiledFrom = nil + self.softboiledCost = nil -- wPartyMenuCursor lives ACROSS openings: InitPartyMenuWithCancel / -- InitPartyMenuNoCancel seed wMenuCursorY from it and fall back to row 1 only -- when it is zero or no longer inside the party (`and a / jr z, .skip / inc b @@ -160,7 +165,7 @@ end function PartyMenu:count() -- CANCEL is one past the last mon. SwitchPartyMons reopens the list -- through InitPartyMenuNoCancel, which caps the cursor at the last mon. - if self.switchFrom then return #self.party end + if self.switchFrom or self.softboiledFrom then return #self.party end return #self.party + 1 end @@ -339,6 +344,55 @@ function PartyMenu:updateSwitch(input) end end +-- ../pokecrystal/engine/items/item_effects.asm:2016 .SelectMilkDrinkRecipient +function PartyMenu:beginSoftboiled(slot, cost) + self.softboiledFrom = slot + self.softboiledCost = cost +end + +-- ../pokecrystal/engine/items/item_effects.asm:2020 .SelectMilkDrinkRecipient +function PartyMenu:updateSoftboiled(input) + local total = #self.party + if input:wasPressed("up") then + self.index = self.index > 1 and self.index - 1 or total + elseif input:wasPressed("down") then + self.index = self.index < total and self.index + 1 or 1 + elseif input:wasPressed("b") then + self.softboiledFrom, self.softboiledCost = nil, nil + elseif input:wasPressed("a") then + self:finishSoftboiled() + end +end + +-- ../pokecrystal/engine/items/item_effects.asm:1986 Softboiled_MilkDrinkFunction +function PartyMenu:finishSoftboiled() + local slot = self.index + local userSlot = self.softboiledFrom + local user = self.party[userSlot] + local target = self.party[slot] + local userBefore = user and user.hp or 0 + local before, after = + FieldMoves.softboiledTransfer(user, target, self.softboiledCost or 0) + if not before then + self:showItemResult(slot, { text = ItemEffects.TEXT_CANT_USE_ON_MON }) + return + end + self.softboiledFrom, self.softboiledCost = nil, nil + -- data/text/common_1.asm:40 _RecoveredSomeHPText + local climb = { + fromHp = before, toHp = after, sfx = "Sfx_Potion", + text = Strings("%s\nrecovered %dHP!", + target.nickname or target.name or target.species or "?", + after - before), + } + -- ../pokecrystal/engine/items/item_effects.asm:1999 HealHP_SFX_GFX + self:showItemResult(userSlot, { + fromHp = userBefore, toHp = user.hp, sfx = "Sfx_Potion", auto = true, + holdSlot = slot, holdHp = before, + onDone = function() self:showItemResult(slot, climb) end, + }) +end + -- OpenPartyStats (engine/pokemon/mon_menu.asm): wMonType is cleared to -- PARTYMON, the volume is dropped for the cry, and StatsScreenInit runs over -- the party list -- so the summary is pushed on top rather than replacing it, @@ -373,14 +427,35 @@ function PartyMenu:useFieldMove(moveId, mon) if not (world and world.useFieldMove) then return end local result = world:useFieldMove(moveId, mon) if not (result and result.ok) then return end + if result.inMenu then + self:beginSoftboiled(self.index, result.cost) + return + end + -- ../pokecrystal/engine/events/overworld.asm:1357 RockSmashFromMenuScript + if result.action == "rocksmash" then + world.queuedFieldMove = nil + local script = FieldMoves.rockSmashFromMenuScript( + world.stdScripts, world.vm and world.vm.scripts, + function(name) return world:specialIdNamed(name) end) + if not script then return end + -- ../pokecrystal/engine/events/overworld.asm:1341 GetFacingObject + if world.vm then world.vm.lastTalked = result.lastTalked end + world.queuedScript = script + self:exitToField() + return + end if result.action == "fly" and world.openFlyMap then world.queuedFieldMove = nil local opened = world:openFlyMap(mon, { onChosen = function(spawnId) result.flySpawn = spawnId world.queuedFieldMove = result - -- home/map.asm:2281 - if world.exitMenusFade then world:exitMenusFade() end + -- engine/pokegear/pokegear.asm:2078, home/map.asm:1927 + if world.exitMenusFadeForFly then + world:exitMenusFadeForFly() + elseif world.exitMenusFade then + world:exitMenusFade() + end self:exitToField() end, onCancel = function() end, @@ -453,6 +528,11 @@ function PartyMenu:exitToField() local stack = self.game and self.game.stack if stack and stack.clear then stack:clear() + -- ../pokecrystal/home/map.asm:1927-1940 + local world = self.game.world + if world and world.exitMenusFade and not world.mapSetup then + world:exitMenusFade() + end elseif self.onCancel then self.onCancel() end @@ -528,6 +608,9 @@ function PartyMenu:showItemResult(slot, opts) text = opts.text, delay = PartyMenu.ACTION_TEXT_DELAY, onDone = opts.onDone, + auto = opts.auto, + holdSlot = opts.holdSlot, + holdHp = opts.holdHp, } if opts.sfx then self:playSfx(opts.sfx) end end @@ -540,7 +623,10 @@ end -- engine/battle/anim_hp_bar.asm:246 function PartyMenu:shownHpFor(slot, mon) local r = self.itemResult - if r and r.slot == slot and r.shown then return r.shown end + if r then + if r.slot == slot and r.shown then return r.shown end + if r.holdSlot == slot and r.holdHp then return r.holdHp end + end return mon and mon.hp end @@ -552,6 +638,11 @@ function PartyMenu:updateItemResult(input) r.shown = HpBar.stepToward(r.shown, r.target, maxHp) return end + if r.auto then + self.itemResult = nil + if r.onDone then r.onDone() end + return + end if r.delay > 0 then r.delay = r.delay - 1 return @@ -578,6 +669,10 @@ function PartyMenu:update(_dt) self:updateSwitch(input) return end + if self.softboiledFrom then + self:updateSoftboiled(input) + return + end local total = self:count() local grid if self:gridNavigation() then @@ -893,7 +988,9 @@ function PartyMenu:drawPanel() end else Chrome.box(0, 14, 20, 4) - local prompt = self.switchFrom and PartyMenu.PROMPTS.moveTo or self.prompt + -- ../pokecrystal/engine/items/item_effects.asm:2016 + local prompt = self.switchFrom and PartyMenu.PROMPTS.moveTo + or (self.softboiledFrom and PartyMenu.PROMPTS.useItem) or self.prompt Chrome.print(#self.party == 0 and PartyMenu.PROMPTS.none or prompt, 1, 16) end -- PokemonActionSubmenu clears (1,15) 2x18 before MonSubmenu draws, so the diff --git a/src/ui/gen2/SaveMenu.lua b/src/ui/gen2/SaveMenu.lua index 8b2ff4b8..610fe8a1 100644 --- a/src/ui/gen2/SaveMenu.lua +++ b/src/ui/gen2/SaveMenu.lua @@ -17,7 +17,7 @@ -- from the cursor and skips the blank row. -- SavingDontTurnOffThePower, which is a timed sequence and not a prompt: -- "SAVINGโ€ฆ DON'T TURN / OFF THE POWER." for 16 frames, the write, 32 --- frames, " saved / the game.", SFX_SAVE, then 30 more. +-- more (SavedTheGame, engine/menus/save.asm:242-262). -- -- Overwriting an existing file gets a second yes/no first -- (AskOverwriteSaveFile), which is the whole reason this is a state and not a @@ -28,10 +28,12 @@ local Logger = require("src.core.Logger") local Save = require("src.core.gen2.Save") local Sound = require("src.core.Sound") local Strings = require("src.core.Strings") +local Typer = require("src.ui.gen2.Typer") local SaveMenu = {} SaveMenu.__index = SaveMenu -SaveMenu.isOpaque = true +-- ../pokecrystal/engine/menus/save.asm:1 +SaveMenu.isOpaque = false -- SFX_SAVE ($25, constants/sfx_constants.asm:40) is what plays as the file is -- written: `ld de, SFX_SAVE / call PlaySFX` right after ResumeGameLogic in @@ -42,8 +44,11 @@ SaveMenu.isOpaque = true local SFX_SAVE = 0x25 -- SavingDontTurnOffThePower's DelayFrames counts, at the 60 Hz logic clock. +-- ../pokecrystal/engine/menus/save.asm:242 SavedTheGame local SAVING_FRAMES = 16 -local SAVED_FRAMES = 32 + 30 +local SAVED_GAP_FRAMES = 32 +local SAVED_TAIL_FRAMES = 30 +local SAVED_FRAMES = SAVED_GAP_FRAMES + SAVED_TAIL_FRAMES -- MenuBox coordinates after _OffsetMenuHeader(4, 0). local PANEL_X, PANEL_Y, PANEL_W, PANEL_H = 4, 0, 16, 10 @@ -96,7 +101,6 @@ local function twoLines(text) end function SaveMenu:wantsFillScale() return true end -function SaveMenu:drawsWidescreen() return true end -- opts: save, onDone(saved), existed (override), writer (injected for tests) function SaveMenu.new(game, opts) @@ -137,10 +141,20 @@ function SaveMenu:playerName() end -- The write itself, which the cart does between the two messages. +-- ../pokecrystal/engine/menus/save.asm:244 _SaveGameData function SaveMenu:writeNow() local ok = self.writer(self.save) self.saved = ok and true or false - if ok then self:playSfx(SFX_SAVE) end +end + +-- ../pokecrystal/engine/menus/save.asm:346 +function SaveMenu:enterPhase(phase) + self.phase = phase + self.timer = 0 + self.typedPhase = phase + local pinned = (phase == "saving" or phase == "done") and "MID" or nil + self.typer = Typer.new(self.game, { speed = pinned }) + self.typer:start(self:prompt()) end function SaveMenu:accept() @@ -150,12 +164,11 @@ function SaveMenu:accept() return end if self.existed then - self.phase = "overwrite" self.choice = 1 + self:enterPhase("overwrite") return end - self.phase = "saving" - self.timer = 0 + self:enterPhase("saving") return end if self.phase == "overwrite" then @@ -163,31 +176,45 @@ function SaveMenu:accept() self:finish(false) return end - self.phase = "saving" - self.timer = 0 + self:enterPhase("saving") end end function SaveMenu:update(_dt) + if self.typedPhase ~= self.phase then self:enterPhase(self.phase) end + local typed = Typer.step(self) + -- The saving and saved messages are DelayFrames, not prompts: no button -- does anything until the sequence runs out. + -- ../pokecrystal/engine/menus/save.asm:352 if self.phase == "saving" then + if not typed then return end self.timer = self.timer + 1 - if self.timer >= SAVING_FRAMES then - self:writeNow() - self.phase = "done" - self.timer = 0 + if self.timer == SAVING_FRAMES then self:writeNow() end + if self.timer >= SAVING_FRAMES + SAVED_GAP_FRAMES then + self:enterPhase("done") end return end if self.phase == "done" then + if not typed then return end + -- ../pokecrystal/engine/menus/save.asm:259 WaitPlaySFX / home/audio.asm:220 + if self.saved and not self.rang then + self.rang = true + Sound.waitSfxDone() + self:playSfx(SFX_SAVE) + return + end + -- ../pokecrystal/engine/menus/save.asm:260 WaitSFX + if Sound.sfxBusy() then return end self.timer = self.timer + 1 - if self.timer >= SAVED_FRAMES then self:finish(self.saved) end + if self.timer >= SAVED_TAIL_FRAMES then self:finish(self.saved) end return end local input = self.game and self.game.input if not input then return end + if Typer.typing(self) then return end if input:wasPressed("up") or input:wasPressed("down") then self.choice = self.choice == 1 and 2 or 1 return @@ -222,7 +249,6 @@ function SaveMenu:prompt() end function SaveMenu:drawPanel() - Chrome.clear() local summary = Save.summary(self.save) Chrome.box(PANEL_X, PANEL_Y, PANEL_W, PANEL_H) if summary then @@ -242,6 +268,7 @@ function SaveMenu:drawPanel() -- and (1,16) -- `line` is the box's lower line, two rows down. Chrome.textbox(0, 12, 18, 4) local lines = self:prompt() + if self.typedPhase == self.phase then lines = Typer.text(self, lines) end Chrome.print(lines[1] or "", 1, 14) Chrome.print(lines[2] or "", 1, 16) @@ -258,19 +285,19 @@ function SaveMenu:draw() self:drawPanel() end -function SaveMenu:drawWidescreen(winW, winH) - local G = love.graphics - Chrome.letterbox(winW, winH, 1, 1, 1) - local scale = Chrome.fitScale(winW, winH) - G.push() - G.translate(Chrome.fitOrigin(winW, winH, scale)) - G.scale(scale, scale) - self:drawPanel() - G.pop() -end +-- ../pokecrystal/engine/menus/intro_menu.asm:479 +SaveMenu.PANEL = { + x = PANEL_X, y = PANEL_Y, w = PANEL_W, h = PANEL_H, + labelX = LABEL_X, labelDy = LABEL_Y, + badgesX = BADGES_X, badgesDy = BADGES_Y, + dexX = DEX_X, dexDy = DEX_Y, + timeX = TIME_X, timeDy = TIME_Y, +} SaveMenu.SFX_SAVE = SFX_SAVE SaveMenu.SAVING_FRAMES = SAVING_FRAMES +SaveMenu.SAVED_GAP_FRAMES = SAVED_GAP_FRAMES +SaveMenu.SAVED_TAIL_FRAMES = SAVED_TAIL_FRAMES SaveMenu.SAVED_FRAMES = SAVED_FRAMES SaveMenu.OVERWRITE_PROMPT_SOURCE = OVERWRITE_PROMPT_SOURCE SaveMenu.SAVING_PROMPT_SOURCE = SAVING_PROMPT_SOURCE diff --git a/src/ui/gen2/SummaryMenu.lua b/src/ui/gen2/SummaryMenu.lua index 280a245c..a5fdc9d1 100644 --- a/src/ui/gen2/SummaryMenu.lua +++ b/src/ui/gen2/SummaryMenu.lua @@ -64,7 +64,7 @@ local GbcPalette = require("src.render.GbcPalette") local HpBar = require("src.battle.gen2.HpBar") local ItemEffects = require("src.core.gen2.ItemEffects") local Mon = require("src.battle.gen2.Mon") -local MonAnim = require("src.render.MonAnim") +local MonAnimView = require("src.render.MonAnimView") local Palettes = require("src.world.gen2.Palettes") local Pokerus = require("src.core.gen2.Pokerus") local Unown = require("src.core.gen2.Unown") @@ -330,48 +330,25 @@ end -- with no `anim` row -- every Gold and Silver one -- leaves picAnim nil. -- ../pokecrystal/engine/pokemon/stats_screen.asm:889-901 function SummaryMenu:startPicAnim() - self.picAnim = nil local mon = self.mon - local def = mon and self.pokemon and self.pokemon[mon.species] - if not def then return end - local data = def.anim - if mon.species == Unown.SPECIES and def.letters then - local entry = def.letters[Unown.name(Unown.monLetter(mon))] - if entry and entry.anim then data = entry.anim end - end - if not data then return end - local sheet = self:picImage(data.sheet) - if not sheet then return end - local runner = MonAnim.new(data, "menu") - if not runner then return end - self.picAnim = { runner = runner, sheet = sheet, size = data.tiles * 8, - quads = {} } + self.picAnim = MonAnimView.start( + mon and self.pokemon and self.pokemon[mon.species], mon, "menu", + function(path) return self:picImage(path) end) end -- AnimateFrontpic's .loop, one scene command per frame. -- ../pokecrystal/engine/gfx/pic_animation.asm:79-89 function SummaryMenu:stepPicAnim() - local state = self.picAnim - if not state then return end - state.runner:update() - if state.runner:finished() then self.picAnim = nil end + local anim = self.picAnim + if not anim then return end + if anim:step() then self.picAnim = nil end end -- The sheet is one column of whole pictures, base picture first. function SummaryMenu:picAnimFrame() - local state = self.picAnim - if not state then return nil end - local frame = state.runner:currentFrame() - if frame <= 0 then return nil end - local quad = state.quads[frame] - if not quad then - local w, h = state.sheet:getDimensions() - if (frame + 1) * state.size > h then return nil end - quad = love.graphics.newQuad(0, frame * state.size, state.size, state.size, - w, h) - state.quads[frame] = quad - end - return state.sheet, quad, state.size + local anim = self.picAnim + if not anim then return nil end + return anim:frame() end function SummaryMenu:speciesDef() diff --git a/src/ui/gen2/TradeAnim.lua b/src/ui/gen2/TradeAnim.lua index 76dc62d1..4add092b 100644 --- a/src/ui/gen2/TradeAnim.lua +++ b/src/ui/gen2/TradeAnim.lua @@ -47,6 +47,7 @@ local Assets = require("src.render.Assets") local Chrome = require("src.ui.gen2.Chrome") local Font = require("src.render.Font") local GbcPalette = require("src.render.GbcPalette") +local MonAnimView = require("src.render.MonAnimView") local Music = require("src.core.Music") local Palettes = require("src.world.gen2.Palettes") local Sound = require("src.core.Sound") @@ -280,9 +281,27 @@ function TradeAnimView:step() if index ~= self.beatIndex then self.beatIndex = index self:cue(beat.cue) + -- ../pokecrystal/engine/movie/trade_animation.asm:64-65 + if beat.id == "getmon_hold" then self:startPicAnim() end end end +-- ../pokecrystal/engine/gfx/trademon_frontpic.asm:36-37 +function TradeAnimView:startPicAnim() + local record = self.get + local species = record and record.species + self.picAnim = MonAnimView.start( + species and self.data.pokemon and self.data.pokemon[species], record, + "trade", function(path) return self:image(path) end, + function() self:playCry(species) end) +end + +function TradeAnimView:getAnimData() + local species = self.get and self.get.species + local def = species and self.data.pokemon and self.data.pokemon[species] + return MonAnimView.animData(def, self.get) +end + function TradeAnimView:cue(cue) if cue == "show_give" then -- TradeAnim_ShowGivemonData: the stats, the frontpic, then the cry. @@ -300,8 +319,10 @@ function TradeAnimView:cue(cue) elseif cue == "drop" then self:playSfx("Sfx_BallPoof") elseif cue == "show_get" then + -- ../pokecrystal/engine/movie/trade_animation.asm:787-800 self:playSfx("Sfx_BallPoof") - self:playCry(self.get.species) + -- ../pokegold/engine/movie/trade_animation.asm:784-789 + if not self:getAnimData() then self:playCry(self.get.species) end end end @@ -319,6 +340,11 @@ function TradeAnimView:update(_dt) if input and (input:wasPressed("b") or input:wasPressed("start")) then return self:finish() end + -- ../pokecrystal/engine/gfx/pic_animation.asm:79-89 + if self.picAnim then + if self.picAnim:step() then self.picAnim = nil end + return + end if self.frame + 1 >= Anim.TOTAL then return self:finish() end self:step() end @@ -540,13 +566,22 @@ function TradeAnimView:drawPic(record, offset) if not image then return end local G = love.graphics local w, h = image:getDimensions() + -- ../pokecrystal/engine/gfx/pic_animation.asm:431-435 + local sheet, quad, size + if self.picAnim and record == self.get then + sheet, quad, size = self.picAnim:frame() + end + if sheet then w, h = size, size end local box = PIC_TILES * 8 local px = PIC_TILE_X * 8 + math.floor((box - w) / 2) - offset local py = PIC_TILE_Y * 8 + (box - h) G.setColor(1, 1, 1, 1) local colors = Palettes.monColors(self.palettes, record.species, record.shiny) - local function body() G.draw(image, px, py) end + local function body() + if sheet then return G.draw(sheet, quad, px, py) end + G.draw(image, px, py) + end if colors and GbcPalette.available() then GbcPalette.with(colors, body) else diff --git a/src/ui/gen2/TradeMenu.lua b/src/ui/gen2/TradeMenu.lua index f6061725..f84a34e8 100644 --- a/src/ui/gen2/TradeMenu.lua +++ b/src/ui/gen2/TradeMenu.lua @@ -75,6 +75,7 @@ local SEPARATORS = "([^" .. LINE .. PAGE .. SCROLL .. "]*)([" .. -- Split a decoded text stream into pages of up to two lines. function TradeMenu.paginate(body) + body = require("src.core.gen2.CommonText").plain(body) local pages, current = {}, {} local function flush(scroll) if #current > 0 then pages[#pages + 1] = current end diff --git a/src/ui/gen2/Typer.lua b/src/ui/gen2/Typer.lua new file mode 100644 index 00000000..81d4d45c --- /dev/null +++ b/src/ui/gen2/Typer.lua @@ -0,0 +1,161 @@ +-- ../pokecrystal/home/print_text.asm:1 PrintLetterDelay +-- ../pokecrystal/home/text.asm:660 PrintTextboxTextAt + +local Font = require("src.render.Font") + +local Typer = {} +Typer.__index = Typer + +-- ../pokecrystal/home/print_text.asm:5 +local DELAYS = { FAST = 1, MID = 3, SLOW = 5 } + +local function spansOf(text) + if Font.split then return Font.split(text) end + local spans = {} + local i, n = 1, #text + while i <= n do + local last = i + if text:byte(i) >= 0xC0 then + local k = i + 1 + while k <= n do + local b = text:byte(k) + if b < 0x80 or b > 0xBF then break end + last, k = k, k + 1 + end + end + spans[#spans + 1] = { from = i, to = last } + i = last + 1 + end + return spans +end + +local function linesOf(page) + if page == nil then return {} end + if type(page) == "string" then + local lines = {} + for part in (page .. "\n"):gmatch("(.-)\n") do lines[#lines + 1] = part end + return lines + end + return page +end + +-- ../pokecrystal/engine/events/pokecenter_pc.asm:314 +function Typer.new(game, opts) + opts = opts or {} + return setmetatable({ + game = game, + speed = opts.speed, + instant = opts.instant and true or false, + expand = opts.expand, + shown = 0, + total = 0, + timer = 0, + page = {}, + }, Typer) +end + +function Typer:start(page) + local out, total = {}, 0 + for i, line in ipairs(linesOf(page)) do + local text = self.expand and self.expand(line) or line + out[i] = text + total = total + #spansOf(text) + end + self.page = out + self.total = total + self.shown = self.instant and total or 0 + self.timer = 0 +end + +-- ../pokecrystal/home/print_text.asm:52 +function Typer:delay() + if self.instant then return 0 end + local options = self.game and self.game.save and self.game.save.options + local raw = self.speed or (options and options.textSpeed) + local delay = DELAYS[raw] or tonumber(raw) or 3 + if delay ~= 1 and delay ~= 3 and delay ~= 5 then delay = 3 end + local input = self.game and self.game.input + if input and input.isDown + and (input:isDown("a") or input:isDown("b")) then + delay = 1 + end + return delay +end + +function Typer:done() + return self.shown >= self.total +end + +function Typer:tick() + if self:done() then return true end + local delay = self:delay() + if delay <= 0 then + self.shown = self.total + return true + end + if self.timer >= delay then self.timer = delay - 1 end + self.timer = self.timer + 1 + while self.timer >= delay and self.shown < self.total do + self.timer = self.timer - delay + self.shown = self.shown + 1 + end + return self:done() +end + +function Typer:lines() + if self:done() then return self.page end + local out, left = {}, self.shown + for i, line in ipairs(self.page) do + local spans = spansOf(line) + if left >= #spans then + out[i] = line + elseif left <= 0 then + out[i] = "" + else + out[i] = line:sub(1, spans[left].to) + end + left = left - #spans + end + return out +end + +-- ../pokecrystal/home/menu.asm:328 +function Typer.say(screen, pages, onDone, opts) + screen.message = { pages = pages or {}, page = 1, onDone = onDone } + screen.typer = Typer.new(screen.game, opts) + screen.typer:start(screen.message.pages[1]) + return screen.message +end + +function Typer.begin(screen, record, opts) + screen.typer = Typer.new(screen.game, opts) + screen.typer:start(record.pages and record.pages[record.page or 1]) + return record +end + +function Typer.turn(screen, record) + record.page = record.page + 1 + if screen.typer then screen.typer:start(record.pages[record.page]) end +end + +function Typer.step(screen) + local typer = screen.typer + if not typer then return true end + return typer:tick() +end + +-- ../pokecrystal/home/print_text.asm:59 +function Typer.typing(screen) + local typer = screen.typer + return typer ~= nil and not typer:done() +end + +function Typer.text(screen, fallback) + local typer = screen.typer + if not typer then return fallback end + return typer:lines() +end + +Typer.DELAYS = DELAYS + +return Typer diff --git a/src/ui/kit/Kit.lua b/src/ui/kit/Kit.lua index 90c25d26..d493fd67 100644 --- a/src/ui/kit/Kit.lua +++ b/src/ui/kit/Kit.lua @@ -1189,7 +1189,7 @@ function Kit.spinner(cx, cy, r, t) local head = math.floor((t * 10) % ticks) if has("setLineWidth") then G.setLineWidth(math.max(2, 2 * Kit.scale)) end for i = 0, ticks - 1 do - local a = ((ticks - ((i - head) % ticks)) / ticks) + local a = ((ticks - ((head - i) % ticks)) / ticks) local ang = i * step - math.pi / 2 local c, s = math.cos(ang), math.sin(ang) Theme.col(PAL.ink, a * a) diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index 11b6bb8d..70fb37c5 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -2601,22 +2601,25 @@ function OverworldState:tryCardKeyDoor(fx, fy) t._CardKeyFailText or romText(Game.data, "_CardKeyFailText", "Darn! It needs a\nCARD KEY!"))) return true end - require("src.core.Sound").play(Game.data, "Go_Inside") local bx, by = math.floor(fx / 2), math.floor(fy / 2) - self:replaceBlock(bx, by, openBlock) - -- opened doors stay open across reloads (the per-door unlock events - -- the floors' gate callbacks check, EVENT_SILPH_CO_n_UNLOCKED_DOOR*) - local closedDoors = FieldDefaults.fieldValue(Game.data, "cardKeyDoors", - "closedDoors") - for _, door in ipairs(closedDoors and closedDoors[self.map.id] or {}) do - if door.bx == bx and door.by == by then - Flags.set(Game.save, door.event) - break + -- ../pokered/engine/events/card_key.asm:29-56 + local function openDoor() + self:replaceBlock(bx, by, openBlock) + local closedDoors = FieldDefaults.fieldValue(Game.data, "cardKeyDoors", + "closedDoors") + for _, door in ipairs(closedDoors and closedDoors[self.map.id] or {}) do + if door.bx == bx and door.by == by then + Flags.set(Game.save, door.event) + break + end end + require("src.core.Sound").play(Game.data, "Go_Inside") end + -- ../pokered/engine/events/card_key.asm:63-67 Game.stack:push(TextBox.new(Game, (t._CardKeySuccessText1 or Strings("Bingo!")) - .. (t._CardKeySuccessText2 or romText(Game.data, "_CardKeySuccessText2", "\nThe CARD KEY\nopened the door!")))) + .. (t._CardKeySuccessText2 or romText(Game.data, "_CardKeySuccessText2", "\nThe CARD KEY\nopened the door!")), + openDoor, TextBox.soundOpts(Game, "Get_Item1"))) return true end diff --git a/src/world/gen2/FieldMoves.lua b/src/world/gen2/FieldMoves.lua index b45734ca..bd902653 100644 --- a/src/world/gen2/FieldMoves.lua +++ b/src/world/gen2/FieldMoves.lua @@ -62,6 +62,8 @@ local FieldMoves = {} FieldMoves.TEXT = { BADGE_REQUIRED = Strings.source("Sorry! A new BADGE\nis required."), CANT_USE_HERE = Strings.source("Can't use that\nhere."), + -- ../pokecrystal/data/text/common_2.asm:1516 _PokemonNotEnoughHPText + NOT_ENOUGH_HP = Strings.source("Not enough HP!"), USE_CUT = Strings.source("{STRBUF} used\nCUT!"), CUT_NOTHING = Strings.source("There's nothing to\nCUT here."), @@ -714,6 +716,47 @@ function FieldMoves.headbuttFromMenu(ctx) return { ok = true, action = "headbutt" } end +-- ../pokecrystal/constants/map_object_constants.asm:145 +local SPRITEMOVEDATA_SMASHABLE_ROCK = 0x18 + +-- ../pokecrystal/engine/events/overworld.asm:1317 TryRockSmashFromMenu +function FieldMoves.rockSmashFromMenu(ctx) + local object = ctx.facingObject + local def = object and object.def + if not def or def.movement ~= SPRITEMOVEDATA_SMASHABLE_ROCK then + return { ok = false, text = FieldMoves.TEXT.CANT_USE_HERE } + end + return { + ok = true, action = "rocksmash", object = object, + lastTalked = (def.index or 0) + 1, + } +end + +-- ../pokecrystal/engine/events/std_scripts.asm:241 SmashRockScript +function FieldMoves.rockSmashScriptKey(stdScripts, scripts) + local entry = stdScripts and stdScripts.scripts + and stdScripts.scripts.SmashRockScript + local smash = entry and entry.key and scripts and scripts[entry.key] + local jump = smash and smash[1] + local ask = jump and jump.op == "farsjump" and jump.script + and scripts[jump.script] + for _, cmd in ipairs(ask or {}) do + if cmd.op == "iftrue" and cmd.script then return cmd.script end + end + return nil +end + +-- ../pokecrystal/engine/events/overworld.asm:1357 RockSmashFromMenuScript +function FieldMoves.rockSmashFromMenuScript(stdScripts, scripts, specialId) + local key = FieldMoves.rockSmashScriptKey(stdScripts, scripts) + if not key then return nil end + local script = { { op = "refreshmap" } } + local id = specialId and specialId("UpdateTimePals") + if id then script[#script + 1] = { op = "special", id = id } end + script[#script + 1] = { op = "sjump", script = key } + return script +end + -- SweetScentFromMenu (engine/events/sweet_scent.asm): QueueScript then an -- unconditional `ld a, $1 / ld [wFieldMoveSucceeded], a` -- no badge, no -- tile test, nothing that can refuse the press. Whether anything actually @@ -750,6 +793,36 @@ function FieldMoves.teleportFromMenu(ctx) } end +-- ../pokecrystal/engine/pokemon/mon_menu.asm:744 .CheckMonHasEnoughHP +function FieldMoves.softboiledFromMenu(ctx) + local mon = ctx.mon + local maxHp = mon and (mon.maxHp or (mon.stats and mon.stats.hp)) or 0 + local cost = math.floor(maxHp / 5) + if not mon or (mon.hp or 0) <= cost then + return { ok = false, text = FieldMoves.TEXT.NOT_ENOUGH_HP } + end + return { ok = true, action = "softboiled", cost = cost, inMenu = true } +end + +local function softboiledMaxHp(mon) + return mon and (mon.maxHp or (mon.stats and mon.stats.hp)) or 0 +end + +-- ../pokecrystal/engine/items/item_effects.asm:2043 .cant_use +function FieldMoves.softboiledTargetOk(user, target) + if not (user and target) or target == user or target.isEgg then return false end + return (target.hp or 0) > 0 and (target.hp or 0) < softboiledMaxHp(target) +end + +-- ../pokecrystal/engine/items/item_effects.asm:1997 RemoveHP / :2005 RestoreHealth +function FieldMoves.softboiledTransfer(user, target, cost) + if not FieldMoves.softboiledTargetOk(user, target) then return nil end + local before = target.hp or 0 + user.hp = math.max(0, (user.hp or 0) - (cost or 0)) + target.hp = math.min(softboiledMaxHp(target), before + (cost or 0)) + return before, target.hp +end + FieldMoves.FROM_MENU = { CUT = FieldMoves.cutFromMenu, FLASH = FieldMoves.flashFromMenu, @@ -759,14 +832,16 @@ FieldMoves.FROM_MENU = { WATERFALL = FieldMoves.waterfallFromMenu, WHIRLPOOL = FieldMoves.whirlpoolFromMenu, HEADBUTT = FieldMoves.headbuttFromMenu, + ROCK_SMASH = FieldMoves.rockSmashFromMenu, SWEET_SCENT = FieldMoves.sweetScentFromMenu, DIG = FieldMoves.digFromMenu, TELEPORT = FieldMoves.teleportFromMenu, + -- ../pokecrystal/engine/pokemon/mon_menu.asm:138 MonMenu_Softboiled_MilkDrink + SOFTBOILED = FieldMoves.softboiledFromMenu, + MILK_DRINK = FieldMoves.softboiledFromMenu, } --- The party submenu's field-move row. Anything the port has no routine for --- (SOFTBOILED, ROCK_SMASH, MILK_DRINK) lands on FieldMoveFailed's line, which --- is what the cart's own unimplemented-here branches print. +-- ../pokecrystal/engine/events/overworld.asm:1330 TryRockSmashFromMenu .no_rock function FieldMoves.fromMenu(moveId, ctx) local fn = FieldMoves.FROM_MENU[moveId] if not fn then diff --git a/src/world/gen2/MapNameSign.lua b/src/world/gen2/MapNameSign.lua index 8a9626c5..1c791e22 100644 --- a/src/world/gen2/MapNameSign.lua +++ b/src/world/gen2/MapNameSign.lua @@ -12,7 +12,10 @@ local MapNameSign = {} -- ../pokecrystal/engine/events/map_name_sign.asm:1 MapNameSign.TILES = 14 +-- ../pokecrystal/engine/events/map_name_sign.asm:42-44 MapNameSign.DURATION = 60 +-- ../pokecrystal/engine/overworld/events.asm:177-191 +MapNameSign.FRAMES_PER_TICK = 2 MapNameSign.SHEET = "assets/generated/fonts/map_entry_sign.png" -- ../pokecrystal/engine/events/map_name_sign.asm:134-140 @@ -102,6 +105,8 @@ function MapNameSign.init(world, via) if id == false or NO_SIGN[id] then return end world.mapSign = { timer = MapNameSign.DURATION, + frames = 0, + shown = false, -- ../pokecrystal/engine/events/map_name_sign.asm:143-145 -- ../pokecrystal/constants/charmap.asm:9 name = ((world:landmarkName() or ""):gsub("\n", " ")), @@ -113,32 +118,29 @@ function MapNameSign.cancel(world) if world then world.mapSign = nil end end -local function logicSpeed(world) - local game = world and world.game - if not game or not game.logicSpeed then return 1 end - local ok, n = pcall(game.logicSpeed, game) - if ok then - n = tonumber(n) - if n and n > 1 then return n end - end - return 1 -end - --- ../pokecrystal/engine/events/map_name_sign.asm:99-117 --- ../pokecrystal/home/window.asm:42 -function MapNameSign.tick(world) - local s = world.mapSign - if not s then return end - if world.textbox then - MapNameSign.cancel(world) +-- ../pokecrystal/engine/events/map_name_sign.asm:99-125 +local function place(world, s) + if s.timer <= 0 then + world.mapSign = nil return end - local speed = logicSpeed(world) - s.accum = (s.accum or 0) + 1 - if s.accum < speed then return end - s.accum = s.accum - speed s.timer = s.timer - 1 - if s.timer <= 0 then world.mapSign = nil end + if s.timer <= MapNameSign.DURATION - 2 then s.shown = true end +end + +-- ../pokecrystal/engine/overworld/events.asm:177-191 +function MapNameSign.frame(world) + local s = world and world.mapSign + if not s or world.mapSetup or world.textbox then return end + s.frames = (s.frames or 0) + 1 + if s.frames < MapNameSign.FRAMES_PER_TICK then return end + s.frames = 0 + place(world, s) +end + +-- ../pokecrystal/home/window.asm:42 +function MapNameSign.tick(world) + if world.mapSign and world.textbox then MapNameSign.cancel(world) end end -- ../pokecrystal/engine/events/map_name_sign.asm:197-233 @@ -177,7 +179,7 @@ end function MapNameSign.draw(world, w, h, posLift) local s = world.mapSign - if not s or GameVersion.engine() ~= "crystal" then return end + if not s or not s.shown or GameVersion.engine() ~= "crystal" then return end if world.textbox then return end local img = loadSheet(world) if not img then return end diff --git a/src/world/gen2/World.lua b/src/world/gen2/World.lua index e495d4c2..f9220a18 100644 --- a/src/world/gen2/World.lua +++ b/src/world/gen2/World.lua @@ -231,8 +231,16 @@ local MAPSETUP_ROAM_JUMP = { [MAPSETUP.TELEPORT] = true } local FADE_STEPS = 4 local FADE_STEP_FRAMES = 2 --- ExitAllMenus opens on ClearBGPalettes (home/map.asm:2282), whose WaitBGMap -local MENU_BLANK_FRAMES = 4 +-- home/map.asm:1927-1940 over home/tilemap.asm:12-25 +local MENU_EXIT_RELOAD_FRAMES = 9 +local MENU_EXIT_WHITE_FRAMES = 4 + MENU_EXIT_RELOAD_FRAMES + (4 + 4) + 2 +-- engine/pokegear/pokegear.asm:2074-2078, home/menu.asm:80-83 +local FLY_EXIT_WHITE_FRAMES = 4 + 4 + MENU_EXIT_WHITE_FRAMES +-- engine/pokegear/pokegear.asm:2027-2046 over home/gfx.asm:189-262 +local FLY_MAP_BUILD_FRAMES = 4 + 4 + 7 + 1 + 7 + 3 + 2 +-- engine/events/overworld.asm:593-597, engine/menus/start_menu.asm:503-518 +local FLY_CANCEL_BLANK_FRAMES = 4 + 8 + 4 + 5 +local FLY_CANCEL_ICON_FRAMES = 3 -- A New Game starts in the bedroom, not outside: engine/menus/intro_menu.asm -- NewGame sets wDefaultSpawnpoint = SPAWN_HOME and warps there, and @@ -949,8 +957,8 @@ function World:load() -- `hold` is the same story one argument along: the cart `pause` a held box -- stands through (FindItemInBallScript's `pause 60`). Dropping it made the -- box hand back the instant it finished typing. - showText = function(body, onDone, stay, hold, sfxWait) - self:showText(body, onDone, stay, hold, sfxWait) + showText = function(body, onDone, stay, hold, sfxWait, arrows) + self:showText(body, onDone, stay, hold, sfxWait, arrows) end, facePlayer = function() if self.talkNpc and self.player then @@ -3518,13 +3526,26 @@ function World:runMapSetup(method, load, fly) end -- ramp with no load between the halves (home/map.asm:2281-2292). -function World:exitMenusFade() +function World:exitMenusFade(whiteFrames) self.fade, self.fadeLevel = "white", 1 self.mapSetup = { - phase = "in", step = FADE_STEPS, wait = MENU_BLANK_FRAMES, + phase = "in", step = FADE_STEPS, + wait = whiteFrames or MENU_EXIT_WHITE_FRAMES, } end +-- engine/pokegear/pokegear.asm:2078, home/menu.asm:83, home/map.asm:1928 +function World:exitMenusFadeForFly() + self:exitMenusFade(FLY_EXIT_WHITE_FRAMES) + -- engine/events/overworld.asm:569-572, :611 + self.flyHidden = "from" +end + +-- engine/menus/start_menu.asm:511, engine/gfx/mon_icons.asm:287-297 +function World.flyCancelBlankFrames(partySize) + return FLY_CANCEL_BLANK_FRAMES + FLY_CANCEL_ICON_FRAMES * (partySize or 0) +end + -- --------------------------------------------------------------------------- -- The three roaming beasts (engine/overworld/wildmons.asm). -- --------------------------------------------------------------------------- @@ -5288,22 +5309,41 @@ function World:updateHeadbutt() self:showText(Strings(TEXT_HEADBUTT_NOTHING)) end +-- ../pokecrystal/engine/events/treemons.asm:126 GetTreeMon's two RandomRange +function World:treeRandom(n) + if self.treemonRandom then return self.treemonRandom(n) end + if love and love.math and love.math.random then + return love.math.random(n) - 1 + end + return math.random(n) - 1 +end + -- Headbutt. A tree's own map entry decides which of the two tree sets is -- rolled, and whether anything is home at all (engine/events/treemons.asm -- TreeMonEncounter). Returns "battle", "nothing" or nil. function World:tryHeadbutt(cx, cy) local game, map = self.game, self.map if not (game and map and self.encounters) then return nil end - local roll = Encounter.treeSlot(self.encounters, map.id, cx, cy, nil) + local save = game.save + local GameVersion = require("src.core.GameVersion") + local engine = GameVersion.engine((save and save.version) or GameVersion.get()) + local roll = Encounter.treeSlot(self.encounters, map.id, cx, cy, + function(n) return self:treeRandom(n) end, + { otId = (save and save.player and save.player.id) or 0, engine = engine }) if not roll or not roll.species then return "nothing" end local wild = Mon.new(game.data, roll.species, roll.level) if not wild then return "nothing" end - local save = game.save + -- LoadEnemyMon's .TreeMon arm (../pokecrystal/engine/battle/core.asm:6249) + if Encounter.treeMonAsleep(roll.species, self.tod or self.daytime or "DAY", + engine, self.encounters) then + wild.status = "sleep" + wild.statusTurns = Encounter.TREEMON_SLEEP_TURNS + end if save then save.pokedex = save.pokedex or { seen = {}, caught = {} } save.pokedex.seen[roll.species] = true end - self:startBattle({ wild = wild }) + self:startBattle({ wild = wild, battleType = "tree" }) return "battle" end @@ -6015,6 +6055,10 @@ function World:useFieldMove(moveId, mon) local ctx = self:fieldContext(mon) local result = withBlockIndex(FieldMoves.fromMenu(moveId, ctx), ctx) result.mon = result.mon or mon + -- ../pokecrystal/engine/pokemon/mon_menu.asm:735 + if result.ok and result.inMenu then + return result + end if result.ok then self.queuedFieldMove = result elseif result.text then @@ -6138,6 +6182,8 @@ end local FLY = { FROM_FRAMES = 128, TO_FRAMES = 64, HOVER = 0x40, AMP_MAX = 0x40, TO_AMP = 11 * 8, RISE = 84, + -- engine/tilesets/timeofday_pals.asm:289-299, engine/events/field_moves.asm:305-311 + FROM_PREROLL = 2 + 4, -- engine/events/field_moves.asm:341 (depixel 31, 10, 4, 0 up to 10 * 8 + 4) TO_RISE = 88, -- engine/events/field_moves.asm:307 over data/sprite_anims/oam.asm:314 @@ -6207,6 +6253,7 @@ function World:startFlyAnim(phase, mon, onDone) hover = landing and 0 or FLY.HOVER, amp = landing and FLY.TO_AMP or 0, y = landing and -FLY.TO_RISE or 0, + preroll = landing and 0 or FLY.FROM_PREROLL, } return true end @@ -6216,6 +6263,10 @@ end function World:stepFlyAnim() local fa = self.flyAnim if not fa then return end + if (fa.preroll or 0) > 0 then + fa.preroll = fa.preroll - 1 + return + end local left = fa.left if left <= 0 then local done = fa.onDone @@ -6328,6 +6379,7 @@ end function World:drawFlyAnim(s, billboard) local fa = self.flyAnim if not (fa and fa.icon) then return end + if (fa.preroll or 0) > 0 then return end local G = love.graphics local bx, by = World.birdScreenPos(fa) if not World.offGbScreen(bx, by, 16, 16) then @@ -6356,6 +6408,7 @@ function World:flyTo(spawnId, mon) local spawn = self.landmarks and self.landmarks.spawns and self.landmarks.spawns[spawnId] if not (spawn and spawn.map and self.maps and self.maps[spawn.map]) then + if self.flyHidden == "from" then self.flyHidden = nil end return false end local function warp() @@ -6413,13 +6466,22 @@ function World:openFlyMap(mon, opts) end, onClose = function() self.game.stack:pop() + if opts then + -- engine/pokegear/pokegear.asm:2062-2078, engine/menus/start_menu.asm:503-518 + local party = self.game.save and self.game.save.party + Screens.push(self.game, "Gen2BlankScreen", { + frames = World.flyCancelBlankFrames(party and #party or 0), + onDone = function() self.game.stack:pop() end, + }) + end if opts and opts.onCancel then opts.onCancel() end end, }) end if opts then - -- engine/pokegear/pokegear.asm:1979 + -- engine/pokegear/pokegear.asm:2027-2046 Screens.push(self.game, "Gen2BlankScreen", { + frames = FLY_MAP_BUILD_FRAMES, onDone = function() self.game.stack:pop() pushGear() @@ -7613,7 +7675,9 @@ end -- stack the overworld and the VM under it do not tick at all -- so the wait has -- to be counted by the box itself. Frames, already doubled by Vm:pauseFrames -- the way Script_pause's `ld c, 2 / call DelayFrames` doubles the operand. -function World:showText(body, onDone, stay, hold, sfxWait) +-- ../pokecrystal/home/joypad.asm:302 WaitButton +function World:showText(body, onDone, stay, hold, sfxWait, arrows) + local waitButton = not arrows local game = self.game -- The box a PREVIOUS `stay` left standing (TextBox's contract is "whoever -- pushed it owns the pop", src/render/TextBox.lua:40). `yesorno` consumes it @@ -7656,6 +7720,7 @@ function World:showText(body, onDone, stay, hold, sfxWait) end if onDone then onDone() end end }, + waitButton = waitButton, }) game.stack:push(box) return @@ -7663,7 +7728,7 @@ function World:showText(body, onDone, stay, hold, sfxWait) game.stack:push(TextBox.new(game, body, function() self.textbox = nil if onDone then onDone() end - end, sfxWait and { sfxWait = true } or nil)) + end, { sfxWait = sfxWait and true or nil, waitButton = waitButton })) end function World:pooledNpc(mapId, obj) @@ -10681,20 +10746,27 @@ function World:drawPeople(s, billboard) local p = self.player local cam = self.camera local hideAll, hidePlayer = self:flyHides() + -- ../pokecrystal/engine/overworld/map_objects.asm:2191-2205 + local filter = self.spriteFilter local drawList = {} if not hideAll then if not hidePlayer then drawList[1] = { kind = "player", py = p.py, ox = 0, oy = 0 } end for _, npc in ipairs(self.npcs) do - drawList[#drawList + 1] = { - kind = "npc", npc = npc, ox = 0, oy = 0, py = npc.py, - } + if not filter or filter(npc) then + drawList[#drawList + 1] = { + kind = "npc", npc = npc, ox = 0, oy = 0, py = npc.py, + } + end end for _, g in ipairs(self.ghosts) do - drawList[#drawList + 1] = { - kind = "npc", npc = g.npc, ox = g.ox, oy = g.oy, py = g.oy + g.npc.py, - } + if not filter or filter(g.npc) then + drawList[#drawList + 1] = { + kind = "npc", npc = g.npc, ox = g.ox, oy = g.oy, + py = g.oy + g.npc.py, + } + end end end table.sort(drawList, function(a, b) return a.py < b.py end) @@ -10780,7 +10852,8 @@ end function World:drawWorldBody(s) self:drawGround(s) - self:drawPeople(s) + if self.bgOverlay then self.bgOverlay(s) end + if not self.peopleHidden then self:drawPeople(s) end end -- Gold's half of the world-pipeline seam: same ctx keys, same order and the @@ -10871,6 +10944,7 @@ function World:drawTilted(w, h, s, gw, gh) G.push() G.origin() self:drawGround(s) + if self.bgOverlay then self.bgOverlay(s) end G.pop() G.setCanvas(previous) @@ -11070,4 +11144,10 @@ end -- rather than duplicated there World.PLAYER_SPRITE = PLAYER_SPRITE +World.FLY_MAP_BUILD_FRAMES = FLY_MAP_BUILD_FRAMES +World.MENU_EXIT_RELOAD_FRAMES = MENU_EXIT_RELOAD_FRAMES +World.MENU_EXIT_WHITE_FRAMES = MENU_EXIT_WHITE_FRAMES +World.FLY_EXIT_WHITE_FRAMES = FLY_EXIT_WHITE_FRAMES +World.FLY_FROM_PREROLL = FLY.FROM_PREROLL + return World diff --git a/src/world/gen2/WorldAPI.lua b/src/world/gen2/WorldAPI.lua index 52bdcfe6..5b4b5de2 100644 --- a/src/world/gen2/WorldAPI.lua +++ b/src/world/gen2/WorldAPI.lua @@ -31,6 +31,7 @@ local Bike = require("src.world.gen2.Bike") local FieldMoves = require("src.world.gen2.FieldMoves") local Permissions = require("src.world.gen2.Permissions") local Mail = require("src.core.gen2.Mail") +local Strings = require("src.core.Strings") local WorldAPI = {} WorldAPI.__index = WorldAPI @@ -50,11 +51,55 @@ local FIELD_ACTIONS = { { id = "teleport", move = "TELEPORT" }, } +-- ../pokecrystal/engine/pokemon/mon_menu.asm:138 MonMenu_Softboiled_MilkDrink +local HEAL_ACTIONS = { + { id = "softboiled", move = "SOFTBOILED" }, + { id = "milk_drink", move = "MILK_DRINK" }, +} + local function validPartySlot(party, slot) return type(slot) == "number" and slot == math.floor(slot) and party[slot] ~= nil end +local function maxHpOf(mon) + return (mon and (mon.maxHp or (mon.stats and mon.stats.hp))) or 0 +end + +local function knows(mon, moveId) + for _, move in ipairs((mon and mon.moves) or {}) do + if (type(move) == "table" and move.id or move) == moveId then return true end + end + return false +end + +local function monInfo(game, mon, slot) + local def = (game.data.pokemon or {})[mon.species] or {} + return { slot = slot, species = mon.species, + name = mon.nickname or def.name or mon.species, level = mon.level, + hp = mon.hp, maxHp = maxHpOf(mon) } +end + +-- ../pokecrystal/engine/items/item_effects.asm:2016 .SelectMilkDrinkRecipient +local function healSources(game, moveId) + local party, sources = (game.save and game.save.party) or {}, {} + for sourceSlot, source in ipairs(party) do + local cost = math.floor(maxHpOf(source) / 5) + if knows(source, moveId) and (source.hp or 0) > cost then + local info = monInfo(game, source, sourceSlot) + info.cost = cost + info.targets = {} + for targetSlot, target in ipairs(party) do + if FieldMoves.softboiledTargetOk(source, target) then + info.targets[#info.targets + 1] = monInfo(game, target, targetSlot) + end + end + if #info.targets > 0 then sources[#sources + 1] = info end + end + end + return sources +end + function WorldAPI.new(game, modId) return setmetatable({ game = game, modId = modId }, WorldAPI) end @@ -157,6 +202,14 @@ function WorldAPI:availableFieldActions() end end + for _, row in ipairs(HEAL_ACTIONS) do + local sources = healSources(game, row.move) + if #sources > 0 then + out[#out + 1] = { id = row.id, label = row.move:gsub("_", " "), + sources = sources } + end + end + if (inventory.SQUIRTBOTTLE or 0) > 0 and world:squirtbottleTreeScript() then out[#out + 1] = { id = "squirtbottle", @@ -192,6 +245,26 @@ function WorldAPI:useFieldAction(id, opts) elseif id == "squirtbottle" then local outcome = world:useFieldItem("SQUIRTBOTTLE") if outcome and outcome ~= "nowhere" then return true end + elseif id == "softboiled" or id == "milk_drink" then + local party = (self.game.save and self.game.save.party) or {} + local sourceSlot = opts and tonumber(opts.sourceSlot) + local targetSlot = opts and tonumber(opts.targetSlot) + local cost + for _, source in ipairs(found.sources or {}) do + if source.slot == sourceSlot then + for _, target in ipairs(source.targets or {}) do + if target.slot == targetSlot then cost = source.cost break end + end + end + end + if not cost then return nil, "softboiled target unavailable" end + local user, target = party[sourceSlot], party[targetSlot] + local before, after = FieldMoves.softboiledTransfer(user, target, cost) + if not before then return nil, "softboiled target unavailable" end + -- data/text/common_1.asm:40 _RecoveredSomeHPText + world:showText(Strings("%s\nrecovered %dHP!", + target.nickname or target.species, after - before)) + return true end for _, row in ipairs(FIELD_ACTIONS) do if row.id == id then diff --git a/tests/data/shaderfx/es/bevel.lua b/tests/data/shaderfx/es/bevel.lua new file mode 100644 index 00000000..45f28d1c --- /dev/null +++ b/tests/data/shaderfx/es/bevel.lua @@ -0,0 +1,75 @@ +return {["parameter_overrides"]={},["pass_count"]=1,["passes"]={{["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 100\ +precision highp float;\ +precision highp int;\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + lowp int FrameCount;\ + float BEVEL_LEVEL;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform highp sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +\ +vec3 bevel(vec2 pos, vec3 color)\ +{\ + float r = sqrt(dot(pos, vec2(1.0)));\ + vec3 delta = mix(vec3(LIBRA_PUSH_FRAGMENT_INSTANCE.BEVEL_LEVEL), vec3(1.0 - LIBRA_PUSH_FRAGMENT_INSTANCE.BEVEL_LEVEL), color);\ + vec3 weight = delta * (1.0 - r);\ + return color + weight;\ +}\ +\ +void main()\ +{\ + vec2 position = fract(LIBRA_VARYING_0 * LIBRA_PUSH_FRAGMENT_INSTANCE.SourceSize.xy);\ + vec3 color = pow(texture2D(LIBRA_TEXTURE_Source, LIBRA_VARYING_0).xyz, vec3(2.400000095367431640625));\ + vec2 param = position;\ + vec3 param_1 = color;\ + color = clamp(bevel(param, param_1), vec3(0.0), vec3(1.0));\ + gl_FragData[0] = vec4(pow(color, vec3(0.4545454680919647216796875)), 1.0);\ +}\ +\ +",["frame_count_mod"]=0,["id"]="0",["mipmap_input"]=false,["parameters"]={{["description"]="Bevel Level",["id"]="BEVEL_LEVEL",["initial"]=0.2,["maximum"]=0.5,["minimum"]=0,["step"]=0.01,},},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["size_uniforms"]={{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="unique",["name"]="FrameCount",["semantic"]="FrameCount",},{["index"]=0,["kind"]="unique",["name"]="BEVEL_LEVEL",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 100\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + lowp int FrameCount;\ + float BEVEL_LEVEL;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord;\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},},["textures"]={},} diff --git a/tests/data/shaderfx/es/gameboy.lua b/tests/data/shaderfx/es/gameboy.lua new file mode 100644 index 00000000..211d458b --- /dev/null +++ b/tests/data/shaderfx/es/gameboy.lua @@ -0,0 +1,983 @@ +return {["parameter_overrides"]={{["name"]="response_time",["value"]=0.33,},},["pass_count"]=5,["passes"]={{["alias"]="PASS0",["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 100\ +precision highp float;\ +precision highp int;\ +\ +struct ResType\ +{\ + vec2 _m0;\ + vec2 _m1;\ +};\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ + vec4 OriginalHistorySize1;\ + float color_toggle;\ + float pixel_size;\ + float pixel_softness;\ + float sharpening_amount;\ + float integer_mode;\ + float video_scale;\ + float baseline_alpha;\ + float grey_balance;\ + float response_time;\ + float brightness_mode;\ + float sharp_mode;\ + float pixel_shape;\ + float palette;\ + float auto_soften;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform highp sampler2D LIBRA_TEXTURE_Source;\ +uniform highp sampler2D LIBRA_TEXTURE_OriginalHistory1;\ +uniform highp sampler2D LIBRA_TEXTURE_OriginalHistory2;\ +uniform highp sampler2D LIBRA_TEXTURE_OriginalHistory3;\ +uniform highp sampler2D LIBRA_TEXTURE_OriginalHistory4;\ +uniform highp sampler2D LIBRA_TEXTURE_OriginalHistory5;\ +uniform highp sampler2D LIBRA_TEXTURE_OriginalHistory6;\ +uniform highp sampler2D LIBRA_TEXTURE_OriginalHistory7;\ +uniform highp sampler2D LIBRA_TEXTURE_COLOR_PALETTE;\ +\ +varying vec2 LIBRA_VARYING_0;\ +varying vec2 LIBRA_VARYING_9;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_4;\ +varying vec2 LIBRA_VARYING_5;\ +varying vec2 LIBRA_VARYING_6;\ +varying float LIBRA_VARYING_11;\ +varying float LIBRA_VARYING_10;\ +\ +float intersect_line_segment(float pixel_start, float pixel_end, float dot_start, float dot_end)\ +{\ + float overlap_start = max(pixel_start, dot_start);\ + float overlap_end = min(pixel_end, dot_end);\ + return max(overlap_end - overlap_start, 0.0);\ +}\ +\ +float intersect_rect_area(vec4 px_square, vec4 rect)\ +{\ + vec2 bl = max(px_square.xy, rect.xy);\ + vec2 tr = min(px_square.zw, rect.zw);\ + vec2 coverage = max(tr - bl, vec2(0.0));\ + return coverage.x * coverage.y;\ +}\ +\ +void main()\ +{\ + vec2 final_tex_coord = LIBRA_VARYING_0;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.integer_mode > 0.5)\ + {\ + vec2 centered_coord = abs(LIBRA_VARYING_0 - vec2(0.5));\ + bool _96 = centered_coord.x > (LIBRA_VARYING_9.x * 0.5);\ + bool _106;\ + if (!_96)\ + {\ + _106 = centered_coord.y > (LIBRA_VARYING_9.y * 0.5);\ + }\ + else\ + {\ + _106 = _96;\ + }\ + if (_106)\ + {\ + gl_FragData[0] = vec4(0.0);\ + return;\ + }\ + final_tex_coord = ((LIBRA_VARYING_0 - vec2(0.5)) / LIBRA_VARYING_9) + vec2(0.5);\ + }\ + vec3 foreground_source = texture2D(LIBRA_TEXTURE_Source, final_tex_coord).xyz;\ + vec3 curr_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_Source, final_tex_coord).xyz);\ + vec3 prev0_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory1, final_tex_coord).xyz);\ + vec3 prev1_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory2, final_tex_coord).xyz);\ + vec3 prev2_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory3, final_tex_coord).xyz);\ + vec3 prev3_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory4, final_tex_coord).xyz);\ + vec3 prev4_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory5, final_tex_coord).xyz);\ + vec3 prev5_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory6, final_tex_coord).xyz);\ + vec3 prev6_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory7, final_tex_coord).xyz);\ + float is_on_dot = 0.0;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.integer_mode > 0.5)\ + {\ + bool _218 = mod(final_tex_coord.x, LIBRA_VARYING_2.x) > LIBRA_VARYING_3.x;\ + bool _229;\ + if (_218)\ + {\ + _229 = mod(final_tex_coord.y, LIBRA_VARYING_2.y) > LIBRA_VARYING_3.y;\ + }\ + else\ + {\ + _229 = _218;\ + }\ + if (_229)\ + {\ + is_on_dot = 1.0;\ + }\ + }\ + else\ + {\ + ResType _238;\ + _238._m1 = vec2(ivec2(LIBRA_VARYING_4));\ + _238._m0 = LIBRA_VARYING_4 - _238._m1;\ + vec2 tx_coord_i = _238._m1;\ + vec2 tx_coord_f = _238._m0;\ + vec2 pixel_center = (tx_coord_f - vec2(0.5)) * LIBRA_VARYING_5;\ + vec4 pixel_rect = vec4(pixel_center - (LIBRA_VARYING_5 * 0.5), pixel_center + (LIBRA_VARYING_5 * 0.5));\ + vec4 dot_rect = vec4((-LIBRA_VARYING_6) * 0.5, LIBRA_VARYING_6 * 0.5);\ + float param = pixel_rect.x;\ + float param_1 = pixel_rect.z;\ + float param_2 = dot_rect.x;\ + float param_3 = dot_rect.z;\ + float x_coverage = intersect_line_segment(param, param_1, param_2, param_3) / LIBRA_VARYING_5.x;\ + float param_4 = pixel_rect.y;\ + float param_5 = pixel_rect.w;\ + float param_6 = dot_rect.y;\ + float param_7 = dot_rect.w;\ + float y_coverage = intersect_line_segment(param_4, param_5, param_6, param_7) / LIBRA_VARYING_5.y;\ + float rect_linear = x_coverage * y_coverage;\ + float rect_sharpened;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.sharp_mode < 0.5)\ + {\ + float sharp_factor = 1.0 / max(LIBRA_VARYING_11, 0.001000000047497451305389404296875);\ + rect_sharpened = pow(x_coverage, sharp_factor) * pow(y_coverage, sharp_factor);\ + }\ + else\ + {\ + float sigmoid_strength = 10.0 / max(LIBRA_VARYING_11, 0.001000000047497451305389404296875);\ + float x_sharp = 1.0 / (1.0 + exp((-sigmoid_strength) * (x_coverage - 0.5)));\ + float y_sharp = 1.0 / (1.0 + exp((-sigmoid_strength) * (y_coverage - 0.5)));\ + rect_sharpened = x_sharp * y_sharp;\ + }\ + float rect_coverage = mix(rect_linear, rect_sharpened, LIBRA_PUSH_FRAGMENT_INSTANCE.sharpening_amount);\ + vec4 param_8 = pixel_rect;\ + vec4 param_9 = dot_rect;\ + float circ_linear = intersect_rect_area(param_8, param_9) / (LIBRA_VARYING_5.x * LIBRA_VARYING_5.y);\ + float circ_sharpened;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.sharp_mode < 0.5)\ + {\ + circ_sharpened = pow(circ_linear, 1.0 / max(LIBRA_VARYING_11, 0.001000000047497451305389404296875));\ + }\ + else\ + {\ + float sigmoid_strength_1 = 10.0 / max(LIBRA_VARYING_11, 0.001000000047497451305389404296875);\ + circ_sharpened = 1.0 / (1.0 + exp((-sigmoid_strength_1) * (circ_linear - 0.5)));\ + }\ + float circ_coverage = mix(circ_linear, circ_sharpened, LIBRA_PUSH_FRAGMENT_INSTANCE.sharpening_amount);\ + is_on_dot = mix(circ_coverage, rect_coverage, LIBRA_PUSH_FRAGMENT_INSTANCE.pixel_shape);\ + }\ + vec3 input_rgb = curr_rgb;\ + input_rgb += ((prev0_rgb - input_rgb) * LIBRA_PUSH_FRAGMENT_INSTANCE.response_time);\ + input_rgb += ((prev1_rgb - input_rgb) * pow(LIBRA_PUSH_FRAGMENT_INSTANCE.response_time, 2.0));\ + input_rgb += ((prev2_rgb - input_rgb) * pow(LIBRA_PUSH_FRAGMENT_INSTANCE.response_time, 3.0));\ + input_rgb += ((prev3_rgb - input_rgb) * pow(LIBRA_PUSH_FRAGMENT_INSTANCE.response_time, 4.0));\ + input_rgb += ((prev4_rgb - input_rgb) * pow(LIBRA_PUSH_FRAGMENT_INSTANCE.response_time, 5.0));\ + input_rgb += ((prev5_rgb - input_rgb) * pow(LIBRA_PUSH_FRAGMENT_INSTANCE.response_time, 6.0));\ + input_rgb += ((prev6_rgb - input_rgb) * pow(LIBRA_PUSH_FRAGMENT_INSTANCE.response_time, 7.0));\ + float brightness;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.brightness_mode < 0.5)\ + {\ + brightness = (input_rgb.x + input_rgb.y) + input_rgb.z;\ + }\ + else\ + {\ + brightness = ((0.2125999927520751953125 * input_rgb.x) + (0.715200006961822509765625 * input_rgb.y)) + (0.072200000286102294921875 * input_rgb.z);\ + }\ + float grey_balance_adjusted = LIBRA_PUSH_FRAGMENT_INSTANCE.grey_balance / LIBRA_VARYING_10;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.brightness_mode >= 0.5)\ + {\ + grey_balance_adjusted /= 3.0;\ + }\ + float rgb_to_alpha = (brightness / grey_balance_adjusted) + LIBRA_PUSH_FRAGMENT_INSTANCE.baseline_alpha;\ + vec3 foreground_color;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 0.5)\ + {\ + foreground_color = texture2D(LIBRA_TEXTURE_COLOR_PALETTE, vec2(0.75, 0.5)).xyz;\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 1.5)\ + {\ + foreground_color = vec3(0.0670000016689300537109375, 0.097999997437000274658203125, 0.13300000131130218505859375);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 2.5)\ + {\ + foreground_color = vec3(0.125);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 3.5)\ + {\ + foreground_color = vec3(0.0);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 4.5)\ + {\ + foreground_color = vec3(0.114000000059604644775390625, 0.41600000858306884765625, 0.4199999868869781494140625);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 5.5)\ + {\ + foreground_color = vec3(0.0, 0.324999988079071044921875, 0.20000000298023223876953125);\ + }\ + else\ + {\ + foreground_color = vec3(0.0, 0.324999988079071044921875, 0.31400001049041748046875);\ + }\ + }\ + }\ + }\ + }\ + }\ + vec4 out_color;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.color_toggle == 0.0)\ + {\ + out_color = vec4(foreground_color, rgb_to_alpha);\ + }\ + else\ + {\ + out_color = vec4(foreground_source, rgb_to_alpha);\ + }\ + out_color.w *= is_on_dot;\ + gl_FragData[0] = out_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="0",["mipmap_input"]=false,["parameters"]={{["description"]="=== GAME BOY DOT MATRIX SHADER v1.2 ===",["id"]="GAMEBOY_SHADER",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== LCD effects ==",["id"]="LCD_EFFECTS",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Turn OFF Integer Scale in Settings > Video > Scaling",["id"]="NOTE1",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * GBC: Turn OFF Core > Color Correction & Interframe Blending",["id"]="NOTE2",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * GBA: Turn ON Core > Color Correction & Interframe Blending",["id"]="NOTE3",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Soft / circular pixels = less grid distortion",["id"]="NOTE5",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Screen position ==",["id"]="SCREEN_POSITIONING",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Blend amount",["id"]="adjacent_texel_alpha_blending",["initial"]=0.1755,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Auto soften (reduce distortion)",["id"]="auto_soften",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Base pixel transparency",["id"]="baseline_alpha",["initial"]=0.1,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]=" โ†ณ Background texture smoothing",["id"]="bg_smoothing",["initial"]=0.75,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]="== Pixel blending mode == (0=Blend gaps, 1=Blend all)",["id"]="blending_mode",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Pixel transparency detection == (0=Simple, 1=Perceptual)",["id"]="brightness_mode",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Color mode == (0=Grayscale, 1=Color)",["id"]="color_toggle",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Contrast",["id"]="contrast",["initial"]=0.95,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Grey balance",["id"]="grey_balance",["initial"]=3,["maximum"]=4,["minimum"]=0,["step"]=0.1,},{["description"]="== Display mode == (0=Full, 1=Pixel Perfect, 2=Scale factor)",["id"]="integer_mode",["initial"]=0,["maximum"]=2,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Palette (0=IMG, 1/2=Pocket, 3=B&W, 4=DMG, 5/6=Light)",["id"]="palette",["initial"]=0,["maximum"]=6,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Pixel opacity",["id"]="pixel_opacity",["initial"]=1,["maximum"]=1,["minimum"]=0.01,["step"]=0.01,},{["description"]=" โ†ณ Pixel shape [Sharp mode] (Circle/Rectangle)",["id"]="pixel_shape",["initial"]=1,["maximum"]=1.3,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Pixel size",["id"]="pixel_size",["initial"]=0.8,["maximum"]=1.1,["minimum"]=0.2,["step"]=0.05,},{["description"]=" โ†ณ Pixel softness",["id"]="pixel_softness",["initial"]=1,["maximum"]=5,["minimum"]=0.2,["step"]=0.05,},{["description"]=" โ†ณ Latency",["id"]="response_time",["initial"]=0,["maximum"]=0.777,["minimum"]=0,["step"]=0.111,},{["description"]=" โ†ณ Screen light",["id"]="screen_light",["initial"]=1,["maximum"]=2,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Screen offset horizontal",["id"]="screen_offset_x",["initial"]=0,["maximum"]=5,["minimum"]=-5,["step"]=1,},{["description"]=" โ†ณ Screen offset vertical",["id"]="screen_offset_y",["initial"]=0,["maximum"]=5,["minimum"]=-5,["step"]=1,},{["description"]="== Drop shadows == (OFF/ON)",["id"]="shadow_enable",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Max offset",["id"]="shadow_max_offset",["initial"]=2.5,["maximum"]=30,["minimum"]=0,["step"]=0.5,},{["description"]=" โ†ณ Shadow motion (OFF/ON)",["id"]="shadow_motion",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Shadow offset horizontal",["id"]="shadow_offset_x",["initial"]=1,["maximum"]=5,["minimum"]=-5,["step"]=0.5,},{["description"]=" โ†ณ Shadow offset vertical",["id"]="shadow_offset_y",["initial"]=1,["maximum"]=5,["minimum"]=-5,["step"]=0.5,},{["description"]=" โ†ณ Shadow opacity",["id"]="shadow_opacity",["initial"]=0.55,["maximum"]=1,["minimum"]=0.01,["step"]=0.01,},{["description"]="== Fullscreen pixels == (0=Soft, 1=Sharp)",["id"]="sharp_mode",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Pixel edge sharpness",["id"]="sharpening_amount",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Scale factor",["id"]="video_scale",["initial"]=5,["maximum"]=15,["minimum"]=2,["step"]=1,},},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},{["index"]=1,["name"]="OriginalHistory1",["semantic"]="OriginalHistory",},{["index"]=2,["name"]="OriginalHistory2",["semantic"]="OriginalHistory",},{["index"]=3,["name"]="OriginalHistory3",["semantic"]="OriginalHistory",},{["index"]=4,["name"]="OriginalHistory4",["semantic"]="OriginalHistory",},{["index"]=5,["name"]="OriginalHistory5",["semantic"]="OriginalHistory",},{["index"]=6,["name"]="OriginalHistory6",["semantic"]="OriginalHistory",},{["index"]=7,["name"]="OriginalHistory7",["semantic"]="OriginalHistory",},{["index"]=0,["name"]="COLOR_PALETTE",["semantic"]="User",["user_name"]="COLOR_PALETTE",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["size_uniforms"]={{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=1,["kind"]="texture",["name"]="OriginalHistorySize1",["semantic"]="OriginalHistory",},{["index"]=0,["kind"]="unique",["name"]="color_toggle",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="pixel_size",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="pixel_softness",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="sharpening_amount",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="integer_mode",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="video_scale",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="baseline_alpha",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="grey_balance",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="response_time",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="brightness_mode",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="sharp_mode",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="pixel_shape",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="palette",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="auto_soften",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 100\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ + vec4 OriginalHistorySize1;\ + float color_toggle;\ + float pixel_size;\ + float pixel_softness;\ + float sharpening_amount;\ + float integer_mode;\ + float video_scale;\ + float baseline_alpha;\ + float grey_balance;\ + float response_time;\ + float brightness_mode;\ + float sharp_mode;\ + float pixel_shape;\ + float palette;\ + float auto_soften;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +varying vec2 LIBRA_VARYING_9;\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_4;\ +varying vec2 LIBRA_VARYING_5;\ +varying vec2 LIBRA_VARYING_6;\ +varying float LIBRA_VARYING_11;\ +varying float LIBRA_VARYING_10;\ +float video_scale_factor;\ +vec2 original_coord;\ +vec2 screen_bounds;\ +\ +float intersect_line_segment(float pixel_start, float pixel_end, float dot_start, float dot_end)\ +{\ + float overlap_start = max(pixel_start, dot_start);\ + float overlap_end = min(pixel_end, dot_end);\ + return max(overlap_end - overlap_start, 0.0);\ +}\ +\ +float intersect_rect_area(vec4 px_square, vec4 rect)\ +{\ + vec2 bl = max(px_square.xy, rect.xy);\ + vec2 tr = min(px_square.zw, rect.zw);\ + vec2 coverage = max(tr - bl, vec2(0.0));\ + return coverage.x * coverage.y;\ +}\ +\ +float calculate_coverage_at_point(vec2 tx_coord_f, float pixel_size, float pixel_softness, float sharpening_amount, float pixel_shape, float sharp_mode)\ +{\ + vec2 pixel_center = tx_coord_f - vec2(0.5);\ + vec4 pixel_rect = vec4(pixel_center - vec2(0.5), pixel_center + vec2(0.5));\ + vec2 dot_size_in_px = vec2(pixel_size);\ + vec4 dot_rect = vec4((-dot_size_in_px) * 0.5, dot_size_in_px * 0.5);\ + float param = pixel_rect.x;\ + float param_1 = pixel_rect.z;\ + float param_2 = dot_rect.x;\ + float param_3 = dot_rect.z;\ + float x_coverage = intersect_line_segment(param, param_1, param_2, param_3);\ + float param_4 = pixel_rect.y;\ + float param_5 = pixel_rect.w;\ + float param_6 = dot_rect.y;\ + float param_7 = dot_rect.w;\ + float y_coverage = intersect_line_segment(param_4, param_5, param_6, param_7);\ + float rect_linear = x_coverage * y_coverage;\ + float rect_sharpened;\ + if (sharp_mode < 0.5)\ + {\ + float sharp_factor = 1.0 / max(pixel_softness, 0.001000000047497451305389404296875);\ + rect_sharpened = pow(x_coverage, sharp_factor) * pow(y_coverage, sharp_factor);\ + }\ + else\ + {\ + float sigmoid_strength = 10.0 / max(pixel_softness, 0.001000000047497451305389404296875);\ + float x_sharp = 1.0 / (1.0 + exp((-sigmoid_strength) * (x_coverage - 0.5)));\ + float y_sharp = 1.0 / (1.0 + exp((-sigmoid_strength) * (y_coverage - 0.5)));\ + rect_sharpened = x_sharp * y_sharp;\ + }\ + float rect_coverage = mix(rect_linear, rect_sharpened, sharpening_amount);\ + vec4 param_8 = pixel_rect;\ + vec4 param_9 = dot_rect;\ + float circ_linear = intersect_rect_area(param_8, param_9);\ + float circ_sharpened;\ + if (sharp_mode < 0.5)\ + {\ + circ_sharpened = pow(circ_linear, 1.0 / max(pixel_softness, 0.001000000047497451305389404296875));\ + }\ + else\ + {\ + float sigmoid_strength_1 = 10.0 / max(pixel_softness, 0.001000000047497451305389404296875);\ + circ_sharpened = 1.0 / (1.0 + exp((-sigmoid_strength_1) * (circ_linear - 0.5)));\ + }\ + float circ_coverage = mix(circ_linear, circ_sharpened, sharpening_amount);\ + return mix(circ_coverage, rect_coverage, pixel_shape);\ +}\ +\ +float sample_average_coverage(float pixel_size, float pixel_softness, float sharpening_amount, float pixel_shape, float sharp_mode)\ +{\ + float total = 0.0;\ + for (int y = 0; y < 4; y++)\ + {\ + for (int x = 0; x < 4; x++)\ + {\ + vec2 sample_pos = vec2((-0.375) + (float(x) * 0.25), (-0.375) + (float(y) * 0.25));\ + vec2 param = sample_pos;\ + float param_1 = pixel_size;\ + float param_2 = pixel_softness;\ + float param_3 = sharpening_amount;\ + float param_4 = pixel_shape;\ + float param_5 = sharp_mode;\ + total += calculate_coverage_at_point(param, param_1, param_2, param_3, param_4, param_5);\ + }\ + }\ + return total / 16.0;\ +}\ +\ +void main()\ +{\ + if (LIBRA_PUSH_VERTEX_INSTANCE.integer_mode > 0.5)\ + {\ + if (LIBRA_PUSH_VERTEX_INSTANCE.integer_mode > 1.5)\ + {\ + video_scale_factor = LIBRA_PUSH_VERTEX_INSTANCE.video_scale;\ + }\ + else\ + {\ + video_scale_factor = floor(LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.y * LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.w);\ + }\ + vec2 scaled_video_out = LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.xy * vec2(video_scale_factor);\ + LIBRA_VARYING_9 = scaled_video_out / LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.xy;\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord * 1.00010001659393310546875;\ + original_coord = TexCoord;\ + LIBRA_VARYING_2 = LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ + LIBRA_VARYING_3 = vec2(1.0) / (LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.xy * video_scale_factor);\ + screen_bounds = LIBRA_VARYING_9;\ + LIBRA_VARYING_4 = vec2(0.0);\ + LIBRA_VARYING_5 = vec2(0.0);\ + LIBRA_VARYING_6 = vec2(0.0);\ + }\ + else\ + {\ + video_scale_factor = 1.0;\ + LIBRA_VARYING_9 = vec2(1.0);\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord;\ + original_coord = TexCoord;\ + LIBRA_VARYING_4 = TexCoord * LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.xy;\ + LIBRA_VARYING_5 = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.xy / LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.xy;\ + LIBRA_VARYING_6 = LIBRA_VARYING_5 * LIBRA_PUSH_VERTEX_INSTANCE.pixel_size;\ + LIBRA_VARYING_2 = vec2(0.0);\ + LIBRA_VARYING_3 = vec2(0.0);\ + screen_bounds = vec2(1.0);\ + }\ + LIBRA_VARYING_11 = LIBRA_PUSH_VERTEX_INSTANCE.pixel_softness;\ + bool _417 = LIBRA_PUSH_VERTEX_INSTANCE.auto_soften > 0.5;\ + bool _423;\ + if (_417)\ + {\ + _423 = LIBRA_PUSH_VERTEX_INSTANCE.integer_mode < 0.5;\ + }\ + else\ + {\ + _423 = _417;\ + }\ + if (_423)\ + {\ + vec2 scale = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.xy / LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.xy;\ + vec2 fract_scale = fract(scale);\ + float fract_factor = max(0.5 - abs(fract_scale.x - 0.5), 0.5 - abs(fract_scale.y - 0.5));\ + LIBRA_VARYING_11 += (fract_factor * 4.0);\ + }\ + if (LIBRA_PUSH_VERTEX_INSTANCE.integer_mode < 0.5)\ + {\ + float param = 0.800000011920928955078125;\ + float param_1 = 0.800000011920928955078125;\ + float param_2 = 1.0;\ + float param_3 = 1.0;\ + float param_4 = LIBRA_PUSH_VERTEX_INSTANCE.sharp_mode;\ + float ref_brightness = sample_average_coverage(param, param_1, param_2, param_3, param_4);\ + float param_5 = LIBRA_PUSH_VERTEX_INSTANCE.pixel_size;\ + float param_6 = LIBRA_VARYING_11;\ + float param_7 = LIBRA_PUSH_VERTEX_INSTANCE.sharpening_amount;\ + float param_8 = LIBRA_PUSH_VERTEX_INSTANCE.pixel_shape;\ + float param_9 = LIBRA_PUSH_VERTEX_INSTANCE.sharp_mode;\ + float cur_brightness = sample_average_coverage(param_5, param_6, param_7, param_8, param_9);\ + float _492;\ + if (LIBRA_PUSH_VERTEX_INSTANCE.sharp_mode < 0.5)\ + {\ + _492 = 2.7999999523162841796875;\ + }\ + else\ + {\ + _492 = mix(2.5, 1.60000002384185791015625, LIBRA_PUSH_VERTEX_INSTANCE.pixel_shape);\ + }\ + float base_comp = _492;\ + float brightness_ratio = ref_brightness / max(cur_brightness, 0.001000000047497451305389404296875);\ + float _512;\ + if (LIBRA_PUSH_VERTEX_INSTANCE.pixel_size > 0.800000011920928955078125)\ + {\ + _512 = pow(LIBRA_PUSH_VERTEX_INSTANCE.pixel_size / 0.800000011920928955078125, 0.62000000476837158203125);\ + }\ + else\ + {\ + _512 = 1.0;\ + }\ + float size_comp = _512;\ + float _525;\ + if (LIBRA_VARYING_11 > 1.0)\ + {\ + _525 = pow(LIBRA_VARYING_11, 0.4440000057220458984375);\ + }\ + else\ + {\ + _525 = 1.0;\ + }\ + float softness_comp = _525;\ + float _537;\ + if (LIBRA_PUSH_VERTEX_INSTANCE.sharp_mode < 0.5)\ + {\ + _537 = mix(0.85699999332427978515625, 1.0, LIBRA_PUSH_VERTEX_INSTANCE.sharpening_amount);\ + }\ + else\ + {\ + _537 = mix(1.5, 1.0, LIBRA_PUSH_VERTEX_INSTANCE.sharpening_amount);\ + }\ + float sharpening_comp = _537;\ + bool _552 = LIBRA_PUSH_VERTEX_INSTANCE.sharp_mode >= 0.5;\ + bool _558;\ + if (_552)\ + {\ + _558 = LIBRA_PUSH_VERTEX_INSTANCE.pixel_shape > 1.0;\ + }\ + else\ + {\ + _558 = _552;\ + }\ + float _559;\ + if (_558)\ + {\ + _559 = 1.0 + ((LIBRA_PUSH_VERTEX_INSTANCE.pixel_shape - 1.0) * 0.666999995708465576171875);\ + }\ + else\ + {\ + _559 = 1.0;\ + }\ + float shape_comp = _559;\ + LIBRA_VARYING_10 = ((((base_comp * brightness_ratio) * size_comp) * softness_comp) * sharpening_comp) * shape_comp;\ + }\ + else\ + {\ + LIBRA_VARYING_10 = 1.0;\ + }\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},{["alias"]="PASS1",["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 100\ +precision highp float;\ +precision highp int;\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ + float blending_mode;\ + float adjacent_texel_alpha_blending;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform highp sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_6;\ +varying vec2 LIBRA_VARYING_7;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_4;\ +varying vec2 LIBRA_VARYING_5;\ +\ +float blending_modifier(float color)\ +{\ + float blend_bool = float(int(color == 0.0));\ + return clamp(blend_bool + LIBRA_PUSH_FRAGMENT_INSTANCE.blending_mode, 0.0, 1.0);\ +}\ +\ +void main()\ +{\ + vec4 out_color = texture2D(LIBRA_TEXTURE_Source, LIBRA_VARYING_0);\ + vec2 blur_coords_up_clamped = clamp(LIBRA_VARYING_2, LIBRA_VARYING_6, LIBRA_VARYING_7);\ + vec2 blur_coords_down_clamped = clamp(LIBRA_VARYING_3, LIBRA_VARYING_6, LIBRA_VARYING_7);\ + vec2 blur_coords_right_clamped = clamp(LIBRA_VARYING_4, LIBRA_VARYING_6, LIBRA_VARYING_7);\ + vec2 blur_coords_left_clamped = clamp(LIBRA_VARYING_5, LIBRA_VARYING_6, LIBRA_VARYING_7);\ + vec4 adjacent_texel_1 = texture2D(LIBRA_TEXTURE_Source, blur_coords_up_clamped);\ + vec4 adjacent_texel_2 = texture2D(LIBRA_TEXTURE_Source, blur_coords_down_clamped);\ + vec4 adjacent_texel_3 = texture2D(LIBRA_TEXTURE_Source, blur_coords_right_clamped);\ + vec4 adjacent_texel_4 = texture2D(LIBRA_TEXTURE_Source, blur_coords_left_clamped);\ + float param = out_color.w;\ + out_color.w -= ((((((out_color.w - adjacent_texel_1.w) + (out_color.w - adjacent_texel_2.w)) + (out_color.w - adjacent_texel_3.w)) + (out_color.w - adjacent_texel_4.w)) * LIBRA_PUSH_FRAGMENT_INSTANCE.adjacent_texel_alpha_blending) * blending_modifier(param));\ + gl_FragData[0] = out_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="1",["mipmap_input"]=false,["parameters"]={{["description"]="=== GAME BOY DOT MATRIX SHADER v1.2 ===",["id"]="GAMEBOY_SHADER",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== LCD effects ==",["id"]="LCD_EFFECTS",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Turn OFF Integer Scale in Settings > Video > Scaling",["id"]="NOTE1",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * GBC: Turn OFF Core > Color Correction & Interframe Blending",["id"]="NOTE2",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * GBA: Turn ON Core > Color Correction & Interframe Blending",["id"]="NOTE3",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Soft / circular pixels = less grid distortion",["id"]="NOTE5",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Screen position ==",["id"]="SCREEN_POSITIONING",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Blend amount",["id"]="adjacent_texel_alpha_blending",["initial"]=0.1755,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Auto soften (reduce distortion)",["id"]="auto_soften",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Base pixel transparency",["id"]="baseline_alpha",["initial"]=0.1,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]=" โ†ณ Background texture smoothing",["id"]="bg_smoothing",["initial"]=0.75,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]="== Pixel blending mode == (0=Blend gaps, 1=Blend all)",["id"]="blending_mode",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Pixel transparency detection == (0=Simple, 1=Perceptual)",["id"]="brightness_mode",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Color mode == (0=Grayscale, 1=Color)",["id"]="color_toggle",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Contrast",["id"]="contrast",["initial"]=0.95,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Grey balance",["id"]="grey_balance",["initial"]=3,["maximum"]=4,["minimum"]=0,["step"]=0.1,},{["description"]="== Display mode == (0=Full, 1=Pixel Perfect, 2=Scale factor)",["id"]="integer_mode",["initial"]=0,["maximum"]=2,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Palette (0=IMG, 1/2=Pocket, 3=B&W, 4=DMG, 5/6=Light)",["id"]="palette",["initial"]=0,["maximum"]=6,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Pixel opacity",["id"]="pixel_opacity",["initial"]=1,["maximum"]=1,["minimum"]=0.01,["step"]=0.01,},{["description"]=" โ†ณ Pixel shape [Sharp mode] (Circle/Rectangle)",["id"]="pixel_shape",["initial"]=1,["maximum"]=1.3,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Pixel size",["id"]="pixel_size",["initial"]=0.8,["maximum"]=1.1,["minimum"]=0.2,["step"]=0.05,},{["description"]=" โ†ณ Pixel softness",["id"]="pixel_softness",["initial"]=1,["maximum"]=5,["minimum"]=0.2,["step"]=0.05,},{["description"]=" โ†ณ Latency",["id"]="response_time",["initial"]=0,["maximum"]=0.777,["minimum"]=0,["step"]=0.111,},{["description"]=" โ†ณ Screen light",["id"]="screen_light",["initial"]=1,["maximum"]=2,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Screen offset horizontal",["id"]="screen_offset_x",["initial"]=0,["maximum"]=5,["minimum"]=-5,["step"]=1,},{["description"]=" โ†ณ Screen offset vertical",["id"]="screen_offset_y",["initial"]=0,["maximum"]=5,["minimum"]=-5,["step"]=1,},{["description"]="== Drop shadows == (OFF/ON)",["id"]="shadow_enable",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Max offset",["id"]="shadow_max_offset",["initial"]=2.5,["maximum"]=30,["minimum"]=0,["step"]=0.5,},{["description"]=" โ†ณ Shadow motion (OFF/ON)",["id"]="shadow_motion",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Shadow offset horizontal",["id"]="shadow_offset_x",["initial"]=1,["maximum"]=5,["minimum"]=-5,["step"]=0.5,},{["description"]=" โ†ณ Shadow offset vertical",["id"]="shadow_offset_y",["initial"]=1,["maximum"]=5,["minimum"]=-5,["step"]=0.5,},{["description"]=" โ†ณ Shadow opacity",["id"]="shadow_opacity",["initial"]=0.55,["maximum"]=1,["minimum"]=0.01,["step"]=0.01,},{["description"]="== Fullscreen pixels == (0=Soft, 1=Sharp)",["id"]="sharp_mode",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Pixel edge sharpness",["id"]="sharpening_amount",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Scale factor",["id"]="video_scale",["initial"]=5,["maximum"]=15,["minimum"]=2,["step"]=1,},},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["size_uniforms"]={{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="unique",["name"]="blending_mode",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="adjacent_texel_alpha_blending",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 100\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ + float blending_mode;\ + float adjacent_texel_alpha_blending;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_4;\ +varying vec2 LIBRA_VARYING_5;\ +varying vec2 LIBRA_VARYING_6;\ +varying vec2 LIBRA_VARYING_7;\ +vec2 texel;\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord * 1.00010001659393310546875;\ + texel = LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ + LIBRA_VARYING_3 = LIBRA_VARYING_0 + vec2(0.0, texel.y);\ + LIBRA_VARYING_2 = LIBRA_VARYING_0 + vec2(0.0, -texel.y);\ + LIBRA_VARYING_4 = LIBRA_VARYING_0 + vec2(texel.x, 0.0);\ + LIBRA_VARYING_5 = LIBRA_VARYING_0 + vec2(-texel.x, 0.0);\ + LIBRA_VARYING_6 = vec2(0.0);\ + LIBRA_VARYING_7 = texel * (LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.xy - vec2(2.0));\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},{["alias"]="PASS2",["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 100\ +precision highp float;\ +precision highp int;\ +\ +const float _17[5] = float[](0.0, 1.0, 2.0, 3.0, 4.0);\ +const float _24[5] = float[](0.134658336639404296875, 0.1305153369903564453125, 0.118835575878620147705078125, 0.10164546966552734375, 0.08167444169521331787109375);\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform highp sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_1;\ +\ +void main()\ +{\ + vec4 out_color = texture2D(LIBRA_TEXTURE_Source, clamp(LIBRA_VARYING_0, LIBRA_VARYING_2, LIBRA_VARYING_3)) * _24[0];\ + for (int i = 1; i < 5; i++)\ + {\ + out_color.w += (texture2D(LIBRA_TEXTURE_Source, clamp(LIBRA_VARYING_0 + vec2(_17[i] * LIBRA_VARYING_1.x, 0.0), LIBRA_VARYING_2, LIBRA_VARYING_3)).w * _24[i]);\ + out_color.w += (texture2D(LIBRA_TEXTURE_Source, clamp(LIBRA_VARYING_0 - vec2(_17[i] * LIBRA_VARYING_1.x, 0.0), LIBRA_VARYING_2, LIBRA_VARYING_3)).w * _24[i]);\ + }\ + gl_FragData[0] = out_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="2",["mipmap_input"]=false,["parameters"]={},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["size_uniforms"]={{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 100\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +varying vec2 LIBRA_VARYING_1;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord * 1.00010001659393310546875;\ + LIBRA_VARYING_1 = LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ + LIBRA_VARYING_2 = vec2(0.0);\ + LIBRA_VARYING_3 = LIBRA_VARYING_1 * (LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.xy - vec2(1.0));\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},{["alias"]="PASS3",["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 100\ +precision highp float;\ +precision highp int;\ +\ +const float _17[5] = float[](0.0, 1.0, 2.0, 3.0, 4.0);\ +const float _24[5] = float[](0.134658336639404296875, 0.1305153369903564453125, 0.118835575878620147705078125, 0.10164546966552734375, 0.08167444169521331787109375);\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform highp sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_1;\ +\ +void main()\ +{\ + vec4 out_color = texture2D(LIBRA_TEXTURE_Source, clamp(LIBRA_VARYING_0, LIBRA_VARYING_2, LIBRA_VARYING_3)) * _24[0];\ + for (int i = 1; i < 5; i++)\ + {\ + out_color.w += (texture2D(LIBRA_TEXTURE_Source, clamp(LIBRA_VARYING_0 + vec2(0.0, _17[i] * LIBRA_VARYING_1.y), LIBRA_VARYING_2, LIBRA_VARYING_3)).w * _24[i]);\ + out_color.w += (texture2D(LIBRA_TEXTURE_Source, clamp(LIBRA_VARYING_0 - vec2(0.0, _17[i] * LIBRA_VARYING_1.y), LIBRA_VARYING_2, LIBRA_VARYING_3)).w * _24[i]);\ + }\ + gl_FragData[0] = out_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="3",["mipmap_input"]=false,["parameters"]={},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["size_uniforms"]={{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 100\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +varying vec2 LIBRA_VARYING_1;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord * 1.00010001659393310546875;\ + LIBRA_VARYING_1 = LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ + LIBRA_VARYING_2 = vec2(0.0);\ + LIBRA_VARYING_3 = LIBRA_VARYING_1 * (LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.xy - vec2(1.0));\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},{["alias"]="PASS4",["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 100\ +precision highp float;\ +precision highp int;\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ + vec3 Gyroscope;\ + vec3 Accelerometer;\ + vec3 AccelerometerRest;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ + vec4 PassOutputSize1;\ + float contrast;\ + float screen_light;\ + float pixel_opacity;\ + float bg_smoothing;\ + float shadow_opacity;\ + float shadow_offset_x;\ + float shadow_offset_y;\ + float screen_offset_x;\ + float screen_offset_y;\ + float shadow_enable;\ + float palette;\ + float integer_mode;\ + float shadow_motion;\ + float shadow_max_offset;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform highp sampler2D LIBRA_TEXTURE_COLOR_PALETTE;\ +uniform highp sampler2D LIBRA_TEXTURE_PassOutput1;\ +uniform highp sampler2D LIBRA_TEXTURE_BACKGROUND;\ +uniform highp sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_1;\ +\ +vec4 get_bg_color()\ +{\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 0.5)\ + {\ + return texture2D(LIBRA_TEXTURE_COLOR_PALETTE, vec2(0.25, 0.5));\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 1.5)\ + {\ + return vec4(0.65100002288818359375, 0.675000011920928955078125, 0.51800000667572021484375, 1.0);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 2.5)\ + {\ + return vec4(0.736999988555908203125, 0.736999988555908203125, 0.736999988555908203125, 1.0);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 3.5)\ + {\ + return vec4(1.0);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 4.5)\ + {\ + return vec4(0.62699997425079345703125, 0.666999995708465576171875, 0.0240000002086162567138671875, 1.0);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 5.5)\ + {\ + return vec4(0.02700000070035457611083984375, 0.72899997234344482421875, 0.3689999878406524658203125, 1.0);\ + }\ + else\ + {\ + return vec4(0.02700000070035457611083984375, 0.72899997234344482421875, 0.607999980449676513671875, 1.0);\ + }\ + }\ + }\ + }\ + }\ + }\ +}\ +\ +void main()\ +{\ + vec2 tex = floor(LIBRA_PUSH_FRAGMENT_INSTANCE.PassOutputSize1.xy * LIBRA_VARYING_0);\ + tex = (tex + vec2(0.5)) * LIBRA_PUSH_FRAGMENT_INSTANCE.PassOutputSize1.zw;\ + vec4 bg_color_cached = get_bg_color();\ + vec2 shadow_sample = LIBRA_VARYING_0 - (LIBRA_VARYING_3 + vec2(LIBRA_PUSH_FRAGMENT_INSTANCE.screen_offset_x * LIBRA_VARYING_1.x, LIBRA_PUSH_FRAGMENT_INSTANCE.screen_offset_y * LIBRA_VARYING_1.y));\ + shadow_sample = clamp(shadow_sample, vec2(0.0), vec2(1.0) - LIBRA_VARYING_1);\ + vec4 foreground = texture2D(LIBRA_TEXTURE_PassOutput1, tex - vec2(LIBRA_PUSH_FRAGMENT_INSTANCE.screen_offset_x * LIBRA_VARYING_1.x, LIBRA_PUSH_FRAGMENT_INSTANCE.screen_offset_y * LIBRA_VARYING_1.y));\ + vec4 background = texture2D(LIBRA_TEXTURE_BACKGROUND, LIBRA_VARYING_0);\ + vec4 shadows = texture2D(LIBRA_TEXTURE_Source, shadow_sample);\ + foreground *= bg_color_cached;\ + float bg_test = 0.0;\ + if (foreground.w > 0.0)\ + {\ + bg_test = 1.0;\ + }\ + background -= (((background - vec4(0.5)) * LIBRA_PUSH_FRAGMENT_INSTANCE.bg_smoothing) * bg_test);\ + float _201 = background.x;\ + float _207 = background.y;\ + float _214 = background.z;\ + vec3 _221 = clamp(vec3(bg_color_cached.x + mix(-1.0, 1.0, _201), bg_color_cached.y + mix(-1.0, 1.0, _207), bg_color_cached.z + mix(-1.0, 1.0, _214)), vec3(0.0), vec3(1.0));\ + background.x = _221.x;\ + background.y = _221.y;\ + background.z = _221.z;\ + vec4 out_color = ((shadows * shadows.w) * ((LIBRA_PUSH_FRAGMENT_INSTANCE.contrast * LIBRA_PUSH_FRAGMENT_INSTANCE.shadow_opacity) * LIBRA_PUSH_FRAGMENT_INSTANCE.shadow_enable)) + (background * (1.0 - (shadows.w * ((LIBRA_PUSH_FRAGMENT_INSTANCE.contrast * LIBRA_PUSH_FRAGMENT_INSTANCE.shadow_opacity) * LIBRA_PUSH_FRAGMENT_INSTANCE.shadow_enable))));\ + out_color = ((foreground * foreground.w) * LIBRA_PUSH_FRAGMENT_INSTANCE.contrast) + (out_color * (LIBRA_PUSH_FRAGMENT_INSTANCE.screen_light - ((foreground.w * LIBRA_PUSH_FRAGMENT_INSTANCE.contrast) * LIBRA_PUSH_FRAGMENT_INSTANCE.pixel_opacity)));\ + gl_FragData[0] = out_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="4",["mipmap_input"]=false,["parameters"]={{["description"]="=== GAME BOY DOT MATRIX SHADER v1.2 ===",["id"]="GAMEBOY_SHADER",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== LCD effects ==",["id"]="LCD_EFFECTS",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Turn OFF Integer Scale in Settings > Video > Scaling",["id"]="NOTE1",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * GBC: Turn OFF Core > Color Correction & Interframe Blending",["id"]="NOTE2",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * GBA: Turn ON Core > Color Correction & Interframe Blending",["id"]="NOTE3",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Soft / circular pixels = less grid distortion",["id"]="NOTE5",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Screen position ==",["id"]="SCREEN_POSITIONING",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Blend amount",["id"]="adjacent_texel_alpha_blending",["initial"]=0.1755,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Auto soften (reduce distortion)",["id"]="auto_soften",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Base pixel transparency",["id"]="baseline_alpha",["initial"]=0.1,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]=" โ†ณ Background texture smoothing",["id"]="bg_smoothing",["initial"]=0.75,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]="== Pixel blending mode == (0=Blend gaps, 1=Blend all)",["id"]="blending_mode",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Pixel transparency detection == (0=Simple, 1=Perceptual)",["id"]="brightness_mode",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Color mode == (0=Grayscale, 1=Color)",["id"]="color_toggle",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Contrast",["id"]="contrast",["initial"]=0.95,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Grey balance",["id"]="grey_balance",["initial"]=3,["maximum"]=4,["minimum"]=0,["step"]=0.1,},{["description"]="== Display mode == (0=Full, 1=Pixel Perfect, 2=Scale factor)",["id"]="integer_mode",["initial"]=0,["maximum"]=2,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Palette (0=IMG, 1/2=Pocket, 3=B&W, 4=DMG, 5/6=Light)",["id"]="palette",["initial"]=0,["maximum"]=6,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Pixel opacity",["id"]="pixel_opacity",["initial"]=1,["maximum"]=1,["minimum"]=0.01,["step"]=0.01,},{["description"]=" โ†ณ Pixel shape [Sharp mode] (Circle/Rectangle)",["id"]="pixel_shape",["initial"]=1,["maximum"]=1.3,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Pixel size",["id"]="pixel_size",["initial"]=0.8,["maximum"]=1.1,["minimum"]=0.2,["step"]=0.05,},{["description"]=" โ†ณ Pixel softness",["id"]="pixel_softness",["initial"]=1,["maximum"]=5,["minimum"]=0.2,["step"]=0.05,},{["description"]=" โ†ณ Latency",["id"]="response_time",["initial"]=0,["maximum"]=0.777,["minimum"]=0,["step"]=0.111,},{["description"]=" โ†ณ Screen light",["id"]="screen_light",["initial"]=1,["maximum"]=2,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Screen offset horizontal",["id"]="screen_offset_x",["initial"]=0,["maximum"]=5,["minimum"]=-5,["step"]=1,},{["description"]=" โ†ณ Screen offset vertical",["id"]="screen_offset_y",["initial"]=0,["maximum"]=5,["minimum"]=-5,["step"]=1,},{["description"]="== Drop shadows == (OFF/ON)",["id"]="shadow_enable",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Max offset",["id"]="shadow_max_offset",["initial"]=2.5,["maximum"]=30,["minimum"]=0,["step"]=0.5,},{["description"]=" โ†ณ Shadow motion (OFF/ON)",["id"]="shadow_motion",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Shadow offset horizontal",["id"]="shadow_offset_x",["initial"]=1,["maximum"]=5,["minimum"]=-5,["step"]=0.5,},{["description"]=" โ†ณ Shadow offset vertical",["id"]="shadow_offset_y",["initial"]=1,["maximum"]=5,["minimum"]=-5,["step"]=0.5,},{["description"]=" โ†ณ Shadow opacity",["id"]="shadow_opacity",["initial"]=0.55,["maximum"]=1,["minimum"]=0.01,["step"]=0.01,},{["description"]="== Fullscreen pixels == (0=Soft, 1=Sharp)",["id"]="sharp_mode",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Pixel edge sharpness",["id"]="sharpening_amount",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Scale factor",["id"]="video_scale",["initial"]=5,["maximum"]=15,["minimum"]=2,["step"]=1,},},["samplers"]={{["index"]=0,["name"]="COLOR_PALETTE",["semantic"]="User",["user_name"]="COLOR_PALETTE",},{["index"]=1,["name"]="PassOutput1",["semantic"]="PassOutput",},{["index"]=1,["name"]="BACKGROUND",["semantic"]="User",["user_name"]="BACKGROUND",},{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["size_uniforms"]={{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=1,["kind"]="texture",["name"]="PassOutputSize1",["semantic"]="PassOutput",},{["index"]=0,["kind"]="unique",["name"]="contrast",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="screen_light",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="pixel_opacity",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="bg_smoothing",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="shadow_opacity",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="shadow_offset_x",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="shadow_offset_y",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="screen_offset_x",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="screen_offset_y",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="shadow_enable",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="palette",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="integer_mode",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="shadow_motion",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="shadow_max_offset",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},{["index"]=0,["kind"]="unique",["name"]="Gyroscope",["semantic"]="Gyroscope",},{["index"]=0,["kind"]="unique",["name"]="Accelerometer",["semantic"]="Accelerometer",},{["index"]=0,["kind"]="unique",["name"]="AccelerometerRest",["semantic"]="AccelerometerRest",},},["srgb_framebuffer"]=false,["vertex"]="#version 100\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ + vec3 Gyroscope;\ + vec3 Accelerometer;\ + vec3 AccelerometerRest;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ + vec4 PassOutputSize1;\ + float contrast;\ + float screen_light;\ + float pixel_opacity;\ + float bg_smoothing;\ + float shadow_opacity;\ + float shadow_offset_x;\ + float shadow_offset_y;\ + float screen_offset_x;\ + float screen_offset_y;\ + float shadow_enable;\ + float palette;\ + float integer_mode;\ + float shadow_motion;\ + float shadow_max_offset;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_1;\ +float resolution_scale;\ +\ +vec2 getOrientedTilt(vec3 accel)\ +{\ + float magnitude = length(accel);\ + if (magnitude < 0.00999999977648258209228515625)\ + {\ + return vec2(0.0);\ + }\ + vec3 dir = accel / vec3(magnitude);\ + return vec2(-dir.x, dir.y);\ +}\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord * 1.00010001659393310546875;\ + float scale_x = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.x / 640.0;\ + float scale_y = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.y / 480.0;\ + resolution_scale = sqrt(scale_x * scale_y);\ + vec2 gravity = vec2(0.0);\ + if (LIBRA_PUSH_VERTEX_INSTANCE.shadow_motion > 0.5)\ + {\ + vec3 accel = LIBRA_UBO_VERTEX_INSTANCE.Accelerometer;\ + bool has_sensors = length(accel) > 0.00999999977648258209228515625;\ + if (has_sensors)\ + {\ + vec3 param = accel;\ + gravity = getOrientedTilt(param);\ + }\ + }\ + if (LIBRA_PUSH_VERTEX_INSTANCE.shadow_motion > 0.5)\ + {\ + vec2 base = vec2(LIBRA_PUSH_VERTEX_INSTANCE.shadow_offset_x, LIBRA_PUSH_VERTEX_INSTANCE.shadow_offset_y);\ + LIBRA_VARYING_3 = ((base + (gravity * LIBRA_PUSH_VERTEX_INSTANCE.shadow_max_offset)) * resolution_scale) * LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ + }\ + else\ + {\ + LIBRA_VARYING_3 = (vec2(LIBRA_PUSH_VERTEX_INSTANCE.shadow_offset_x, LIBRA_PUSH_VERTEX_INSTANCE.shadow_offset_y) * resolution_scale) * LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ + }\ + LIBRA_VARYING_1 = LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},},["textures"]={{["filter_mode"]="nearest",["mipmap"]=false,["name"]="COLOR_PALETTE",["path"]="/Library/Application Support/LOVE/pokemon-love2d/shaders/handheld/shaders/gameboy/resources/palette.png",["wrap_mode"]="clamp_to_border",},{["filter_mode"]="linear",["mipmap"]=false,["name"]="BACKGROUND",["path"]="/Library/Application Support/LOVE/pokemon-love2d/shaders/handheld/shaders/gameboy/resources/background.png",["wrap_mode"]="clamp_to_border",},},} diff --git a/tests/data/shaderfx/es/gb-palette-dmg.lua b/tests/data/shaderfx/es/gb-palette-dmg.lua new file mode 100644 index 00000000..5aa850cb --- /dev/null +++ b/tests/data/shaderfx/es/gb-palette-dmg.lua @@ -0,0 +1,64 @@ +return {["parameter_overrides"]={},["pass_count"]=1,["passes"]={{["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 100\ +precision highp float;\ +precision highp int;\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + lowp int FrameCount;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform highp sampler2D LIBRA_TEXTURE_Source;\ +uniform highp sampler2D LIBRA_TEXTURE_COLOR_PALETTE;\ +\ +varying vec2 LIBRA_VARYING_0;\ +\ +void main()\ +{\ + vec4 out_color = texture2D(LIBRA_TEXTURE_Source, LIBRA_VARYING_0);\ + vec2 palette_coordinate = vec2(0.5, (abs(1.0 - out_color.x) * 0.75) + 0.125);\ + out_color = vec4(texture2D(LIBRA_TEXTURE_COLOR_PALETTE, palette_coordinate).xyz, ceil(abs(1.0 - out_color.x)));\ + gl_FragData[0] = out_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="0",["mipmap_input"]=false,["parameters"]={},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},{["index"]=0,["name"]="COLOR_PALETTE",["semantic"]="User",["user_name"]="COLOR_PALETTE",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["size_uniforms"]={{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="unique",["name"]="FrameCount",["semantic"]="FrameCount",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 100\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + lowp int FrameCount;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord * 1.00010001659393310546875;\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},},["textures"]={{["filter_mode"]="nearest",["mipmap"]=false,["name"]="COLOR_PALETTE",["path"]="/Library/Application Support/LOVE/pokemon-love2d/shaders/handheld/shaders/gb-palette/resources/palette-dmg.png",["wrap_mode"]="clamp_to_border",},},} diff --git a/tests/data/shaderfx/es/lcd3x.lua b/tests/data/shaderfx/es/lcd3x.lua new file mode 100644 index 00000000..c8901b94 --- /dev/null +++ b/tests/data/shaderfx/es/lcd3x.lua @@ -0,0 +1,73 @@ +return {["parameter_overrides"]={},["pass_count"]=1,["passes"]={{["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 100\ +precision highp float;\ +precision highp int;\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + lowp int FrameCount;\ + float brighten_scanlines;\ + float brighten_lcd;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform highp sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +vec2 omega;\ +\ +void main()\ +{\ + omega = vec2(6.283185482025146484375) * LIBRA_PUSH_FRAGMENT_INSTANCE.OriginalSize.xy;\ + vec3 res = texture2D(LIBRA_TEXTURE_Source, LIBRA_VARYING_0).xyz;\ + vec2 angle = LIBRA_VARYING_0 * omega;\ + float yfactor = (LIBRA_PUSH_FRAGMENT_INSTANCE.brighten_scanlines + sin(angle.y)) / (LIBRA_PUSH_FRAGMENT_INSTANCE.brighten_scanlines + 1.0);\ + vec3 xfactors = (vec3(LIBRA_PUSH_FRAGMENT_INSTANCE.brighten_lcd) + sin(vec3(angle.x) + vec3(1.57079637050628662109375, -0.52359879016876220703125, -2.617993831634521484375))) / vec3(LIBRA_PUSH_FRAGMENT_INSTANCE.brighten_lcd + 1.0);\ + vec3 color = (xfactors * yfactor) * res;\ + gl_FragData[0] = vec4(color.x, color.y, color.z, 1.0);\ +}\ +\ +",["frame_count_mod"]=0,["id"]="0",["mipmap_input"]=false,["parameters"]={{["description"]="Brighten LCD",["id"]="brighten_lcd",["initial"]=4,["maximum"]=12,["minimum"]=1,["step"]=0.1,},{["description"]="Brighten Scanlines",["id"]="brighten_scanlines",["initial"]=16,["maximum"]=32,["minimum"]=1,["step"]=0.5,},},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["size_uniforms"]={{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="unique",["name"]="FrameCount",["semantic"]="FrameCount",},{["index"]=0,["kind"]="unique",["name"]="brighten_scanlines",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="brighten_lcd",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 100\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + lowp int FrameCount;\ + float brighten_scanlines;\ + float brighten_lcd;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +vec2 omega;\ +\ +void main()\ +{\ + omega = vec2(6.283185482025146484375) * LIBRA_PUSH_VERTEX_INSTANCE.OriginalSize.xy;\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord;\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},},["textures"]={},} diff --git a/tests/data/shaderfx/es/pixel_transparency.lua b/tests/data/shaderfx/es/pixel_transparency.lua new file mode 100644 index 00000000..10c5a53b --- /dev/null +++ b/tests/data/shaderfx/es/pixel_transparency.lua @@ -0,0 +1,503 @@ +return {["parameter_overrides"]={{["name"]="PT_PALETTE",["value"]=1,},{["name"]="PT_POLARIZER",["value"]=0,},{["name"]="PT_HIGHLIGHTS",["value"]=0,},},["pass_count"]=1,["passes"]={{["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 100\ +precision highp float;\ +precision highp int;\ +\ +const vec2 _356[5] = vec2[](vec2(0.0), vec2(-1.0, 0.0), vec2(1.0, 0.0), vec2(0.0, -1.0), vec2(0.0, 1.0));\ +const vec2 _414[9] = vec2[](vec2(0.0), vec2(-1.0, 0.0), vec2(1.0, 0.0), vec2(0.0, -1.0), vec2(0.0, 1.0), vec2(-1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0));\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ + vec3 Gyroscope;\ + vec3 Accelerometer;\ + vec3 AccelerometerRest;\ + float PT_HIDE_CONTENT;\ + float PT_PALETTE;\ + float PT_PALETTE_INTENSITY;\ + float PT_BACKING_BRIGHTNESS;\ + float PT_POLARIZER;\ + float PT_POLARIZER_TINT;\ + float PT_SATURATION;\ + float PT_HIGHLIGHTS;\ + float PT_WHITE_BOOST;\ + float PT_WHITE_TRANSPARENCY;\ + float PT_BRIGHTNESS_GRID;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 SourceSize;\ + vec4 OutputSize;\ + float PT_ACCEL_ENABLE;\ + float PT_ACCEL_SENSITIVITY;\ + float PT_SHADOW_MOTION;\ + float PT_SHADOW_MAX_OFFSET;\ + float PT_SHADOW_MOTION_X;\ + float PT_SHADOW_MOTION_Y;\ + float PT_SHADOW_OFFSET_X;\ + float PT_SHADOW_OFFSET_Y;\ + float PT_ENABLE;\ + float PT_BRIGHTNESS_MODE;\ + float PT_SHADOW_ENABLE;\ + float PT_SHADOW_BLUR;\ + float PT_SHADOW_OPACITY;\ + float PT_THRESHOLD;\ + float PT_SHADOW_FAST_BLUR;\ + float PT_PIXEL_MODE;\ + float PT_BASE_ALPHA;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform highp sampler2D LIBRA_TEXTURE_Source;\ +uniform highp sampler2D LIBRA_TEXTURE_Original;\ +\ +varying vec2 LIBRA_VARYING_0;\ +varying vec2 LIBRA_VARYING_3;\ +varying float LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_1;\ +\ +float getPerceptualBrightness(vec3 color)\ +{\ + return ((0.2125999927520751953125 * color.x) + (0.715200006961822509765625 * color.y)) + (0.072200000286102294921875 * color.z);\ +}\ +\ +bool isWhitePixel(vec3 color, float threshold)\ +{\ + vec3 param = color;\ + float brightness = getPerceptualBrightness(param);\ + float min_channel = min(min(color.x, color.y), color.z);\ + bool _111 = brightness > threshold;\ + bool _119;\ + if (_111)\ + {\ + _119 = min_channel > (threshold * 0.89999997615814208984375);\ + }\ + else\ + {\ + _119 = _111;\ + }\ + return _119;\ +}\ +\ +float hash(vec2 p)\ +{\ + vec3 p3 = fract(p.xyx * 0.103100001811981201171875);\ + p3 += vec3(dot(p3, p3.yzx + vec3(33.3300018310546875)));\ + return fract((p3.x + p3.y) * p3.z);\ +}\ +\ +float paperNoise(vec2 uv, float scale)\ +{\ + vec2 p = (uv * scale) * 512.0;\ + float n = 0.0;\ + float amplitude = 0.5;\ + float frequency = 1.0;\ + for (int i = 0; i < 3; i++)\ + {\ + vec2 param = p * frequency;\ + n += (hash(param) * amplitude);\ + amplitude *= 0.5;\ + frequency *= 2.0;\ + }\ + return n;\ +}\ +\ +vec3 generateProceduralBackground(vec2 uv)\ +{\ + vec3 baseColor = vec3(LIBRA_UBO_FRAGMENT_INSTANCE.PT_BACKING_BRIGHTNESS);\ + vec2 param = uv;\ + float param_1 = 0.25;\ + float grain = paperNoise(param, param_1);\ + float grainOffset = (grain - 0.4375) * 0.064999997615814208984375;\ + return baseColor + vec3(grainOffset);\ +}\ +\ +float getGameBoyRGBSum(vec3 color)\ +{\ + return ((color.x + color.y) + color.z) / 3.0;\ +}\ +\ +float getBrightness(vec3 color)\ +{\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_BRIGHTNESS_MODE < 0.5)\ + {\ + vec3 param = color;\ + return getGameBoyRGBSum(param);\ + }\ + else\ + {\ + vec3 param_1 = color;\ + return getPerceptualBrightness(param_1);\ + }\ +}\ +\ +void main()\ +{\ + vec4 lcd_color = texture2D(LIBRA_TEXTURE_Source, LIBRA_VARYING_0);\ + vec4 output_color = lcd_color;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_ENABLE < 0.5)\ + {\ + gl_FragData[0] = output_color;\ + return;\ + }\ + vec3 original_pixel = texture2D(LIBRA_TEXTURE_Original, LIBRA_VARYING_0).xyz;\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_HIDE_CONTENT > 0.5)\ + {\ + lcd_color = vec4(1.0);\ + original_pixel = vec3(1.0);\ + output_color = lcd_color;\ + }\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_HIGHLIGHTS < (-0.001000000047497451305389404296875))\ + {\ + vec3 param = lcd_color.xyz;\ + float luma = getPerceptualBrightness(param);\ + vec4 _274 = lcd_color;\ + vec3 _276 = _274.xyz * max(1.0 + (LIBRA_UBO_FRAGMENT_INSTANCE.PT_HIGHLIGHTS * luma), 0.0);\ + lcd_color.x = _276.x;\ + lcd_color.y = _276.y;\ + lcd_color.z = _276.z;\ + }\ + vec3 param_1 = original_pixel;\ + float param_2 = LIBRA_PUSH_FRAGMENT_INSTANCE.PT_THRESHOLD;\ + bool current_is_white = isWhitePixel(param_1, param_2);\ + vec2 param_3 = LIBRA_VARYING_0;\ + vec4 background = vec4(generateProceduralBackground(param_3), 1.0);\ + bool _303 = LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_ENABLE > 0.5;\ + bool _310;\ + if (_303)\ + {\ + _310 = LIBRA_UBO_FRAGMENT_INSTANCE.PT_HIDE_CONTENT < 1.5;\ + }\ + else\ + {\ + _310 = _303;\ + }\ + if (_310)\ + {\ + vec2 shadow_offset = ((-LIBRA_VARYING_3) * LIBRA_VARYING_2) * LIBRA_VARYING_1;\ + float shadow_strength = 0.0;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_BLUR > 0.100000001490116119384765625)\ + {\ + vec2 blur_dist = LIBRA_VARYING_1 * (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_BLUR * LIBRA_VARYING_2);\ + float blurred_shadow = 0.0;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_FAST_BLUR < 0.5)\ + {\ + for (int i = 0; i < 5; i++)\ + {\ + vec2 sample_pos = (LIBRA_VARYING_0 + shadow_offset) + (_356[i] * blur_dist);\ + vec3 blur_sample = texture2D(LIBRA_TEXTURE_Original, sample_pos).xyz;\ + float w = (i == 0) ? 4.0 : 2.0;\ + vec3 param_4 = blur_sample;\ + float blur_brightness = getBrightness(param_4);\ + blurred_shadow += ((1.0 - blur_brightness) * w);\ + }\ + shadow_strength = (blurred_shadow / 12.0) * LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_OPACITY;\ + }\ + else\ + {\ + float _442;\ + for (int i_1 = 0; i_1 < 9; i_1++)\ + {\ + vec2 sample_pos_1 = (LIBRA_VARYING_0 + shadow_offset) + (_414[i_1] * blur_dist);\ + vec3 blur_sample_1 = texture2D(LIBRA_TEXTURE_Original, sample_pos_1).xyz;\ + if (i_1 == 0)\ + {\ + _442 = 4.0;\ + }\ + else\ + {\ + _442 = (i_1 < 5) ? 2.0 : 1.0;\ + }\ + float w_1 = _442;\ + vec3 param_5 = blur_sample_1;\ + float blur_brightness_1 = getBrightness(param_5);\ + blurred_shadow += ((1.0 - blur_brightness_1) * w_1);\ + }\ + shadow_strength = (blurred_shadow / 16.0) * LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_OPACITY;\ + }\ + }\ + else\ + {\ + vec3 center_source = texture2D(LIBRA_TEXTURE_Original, LIBRA_VARYING_0 + shadow_offset).xyz;\ + vec3 param_6 = center_source;\ + float shadow_source_brightness = getBrightness(param_6);\ + shadow_strength = (1.0 - shadow_source_brightness) * LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_OPACITY;\ + }\ + float deadzone = (1.0 - LIBRA_PUSH_FRAGMENT_INSTANCE.PT_THRESHOLD) * LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_OPACITY;\ + if (deadzone > 0.001000000047497451305389404296875)\ + {\ + shadow_strength = smoothstep(0.0, deadzone, shadow_strength) * shadow_strength;\ + }\ + vec4 _502 = background;\ + vec4 _504 = background;\ + vec3 _510 = mix(_502.xyz, _504.xyz * 0.20000000298023223876953125, vec3(shadow_strength));\ + background.x = _510.x;\ + background.y = _510.y;\ + background.z = _510.z;\ + }\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_PALETTE > 0.5)\ + {\ + vec3 bg_palette_color;\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_PALETTE < 1.5)\ + {\ + bg_palette_color = vec3(0.65100002288818359375, 0.675000011920928955078125, 0.51800000667572021484375);\ + }\ + else\ + {\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_PALETTE < 2.5)\ + {\ + bg_palette_color = vec3(0.7200000286102294921875, 0.730000019073486328125, 0.660000026226043701171875);\ + }\ + else\ + {\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_PALETTE < 3.5)\ + {\ + bg_palette_color = vec3(0.765999972820281982421875, 0.730000019073486328125, 0.763000011444091796875);\ + }\ + else\ + {\ + bg_palette_color = vec3(0.7599999904632568359375);\ + }\ + }\ + }\ + float max_ch = max(bg_palette_color.x, max(bg_palette_color.y, bg_palette_color.z));\ + bg_palette_color *= (0.675000011920928955078125 / max_ch);\ + vec3 tinted_background = clamp(vec3(bg_palette_color.x + mix(-1.0, 1.0, background.x), bg_palette_color.y + mix(-1.0, 1.0, background.y), bg_palette_color.z + mix(-1.0, 1.0, background.z)), vec3(0.0), vec3(1.0));\ + vec4 _592 = background;\ + vec3 _599 = mix(_592.xyz, tinted_background, vec3(LIBRA_UBO_FRAGMENT_INSTANCE.PT_PALETTE_INTENSITY));\ + background.x = _599.x;\ + background.y = _599.y;\ + background.z = _599.z;\ + }\ + vec3 brightness_source = original_pixel;\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_BRIGHTNESS_GRID > 0.001000000047497451305389404296875)\ + {\ + brightness_source = lcd_color.xyz;\ + if (current_is_white)\ + {\ + float white_blend = smoothstep(0.0500000007450580596923828125, 1.0, LIBRA_UBO_FRAGMENT_INSTANCE.PT_BRIGHTNESS_GRID);\ + brightness_source = mix(original_pixel, lcd_color.xyz, vec3(white_blend));\ + }\ + }\ + bool _632 = LIBRA_PUSH_FRAGMENT_INSTANCE.PT_PIXEL_MODE > 0.5;\ + bool _638;\ + if (_632)\ + {\ + _638 = LIBRA_PUSH_FRAGMENT_INSTANCE.PT_PIXEL_MODE < 1.5;\ + }\ + else\ + {\ + _638 = _632;\ + }\ + if (_638)\ + {\ + vec3 param_7 = brightness_source;\ + float pixel_intensity = getBrightness(param_7);\ + float transparency = (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_BASE_ALPHA * pixel_intensity) * 2.66499996185302734375;\ + bool _659;\ + if (current_is_white)\ + {\ + _659 = LIBRA_UBO_FRAGMENT_INSTANCE.PT_WHITE_BOOST > 0.5;\ + }\ + else\ + {\ + _659 = current_is_white;\ + }\ + if (_659)\ + {\ + transparency = max(transparency, LIBRA_UBO_FRAGMENT_INSTANCE.PT_WHITE_TRANSPARENCY);\ + }\ + transparency = clamp(transparency, 0.0, 1.0);\ + vec3 _674 = mix(lcd_color.xyz, background.xyz, vec3(transparency));\ + output_color.x = _674.x;\ + output_color.y = _674.y;\ + output_color.z = _674.z;\ + }\ + else\ + {\ + bool should_apply = (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_PIXEL_MODE < 0.5) ? current_is_white : true;\ + if (should_apply)\ + {\ + vec3 param_8 = brightness_source;\ + float pixel_intensity_1 = getBrightness(param_8);\ + float pixel_alpha = (pixel_intensity_1 / 3.0) + LIBRA_PUSH_FRAGMENT_INSTANCE.PT_BASE_ALPHA;\ + bool _708;\ + if (current_is_white)\ + {\ + _708 = LIBRA_UBO_FRAGMENT_INSTANCE.PT_WHITE_BOOST > 0.5;\ + }\ + else\ + {\ + _708 = current_is_white;\ + }\ + if (_708)\ + {\ + pixel_alpha = max(pixel_alpha, LIBRA_UBO_FRAGMENT_INSTANCE.PT_WHITE_TRANSPARENCY);\ + }\ + pixel_alpha = clamp(pixel_alpha, 0.0, 1.0);\ + vec3 _723 = mix(lcd_color.xyz, background.xyz, vec3(pixel_alpha));\ + output_color.x = _723.x;\ + output_color.y = _723.y;\ + output_color.z = _723.z;\ + }\ + }\ + bool _733 = LIBRA_UBO_FRAGMENT_INSTANCE.PT_POLARIZER > 0.5;\ + bool _739;\ + if (_733)\ + {\ + _739 = LIBRA_UBO_FRAGMENT_INSTANCE.PT_POLARIZER_TINT > 0.001000000047497451305389404296875;\ + }\ + else\ + {\ + _739 = _733;\ + }\ + if (_739)\ + {\ + vec3 polarizer = mix(vec3(1.0), vec3(0.939999997615814208984375, 1.0, 0.8650000095367431640625), vec3(LIBRA_UBO_FRAGMENT_INSTANCE.PT_POLARIZER_TINT));\ + vec4 _751 = output_color;\ + vec3 _753 = _751.xyz * polarizer;\ + output_color.x = _753.x;\ + output_color.y = _753.y;\ + output_color.z = _753.z;\ + }\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_HIGHLIGHTS > 0.001000000047497451305389404296875)\ + {\ + vec3 param_9 = output_color.xyz;\ + float luma_1 = getPerceptualBrightness(param_9);\ + vec4 _770 = output_color;\ + vec3 _780 = clamp(_770.xyz * (1.0 + (LIBRA_UBO_FRAGMENT_INSTANCE.PT_HIGHLIGHTS * luma_1)), vec3(0.0), vec3(1.0));\ + output_color.x = _780.x;\ + output_color.y = _780.y;\ + output_color.z = _780.z;\ + }\ + if (abs(LIBRA_UBO_FRAGMENT_INSTANCE.PT_SATURATION - 1.0) > 0.001000000047497451305389404296875)\ + {\ + vec3 param_10 = output_color.xyz;\ + float luma_2 = getPerceptualBrightness(param_10);\ + vec4 _801 = output_color;\ + vec3 _809 = clamp(mix(vec3(luma_2), _801.xyz, vec3(LIBRA_UBO_FRAGMENT_INSTANCE.PT_SATURATION)), vec3(0.0), vec3(1.0));\ + output_color.x = _809.x;\ + output_color.y = _809.y;\ + output_color.z = _809.z;\ + }\ + vec2 param_11 = gl_FragCoord.xy;\ + vec4 _825 = output_color;\ + vec3 _828 = _825.xyz + vec3((hash(param_11) - 0.5) / 255.0);\ + output_color.x = _828.x;\ + output_color.y = _828.y;\ + output_color.z = _828.z;\ + gl_FragData[0] = output_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="0",["mipmap_input"]=false,["parameters"]={{["description"]=" * Turn ON Core > Color Correction & Interframe Blending",["id"]="NOTE2",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Motion enabled on latest RetroArch nightly build.",["id"]="NOTE3",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== MOTION == (OFF/ON)",["id"]="PT_ACCEL_ENABLE",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Tilt sensitivity",["id"]="PT_ACCEL_SENSITIVITY",["initial"]=1,["maximum"]=5,["minimum"]=0.1,["step"]=0.05,},{["description"]=" โ†ณ Brightness",["id"]="PT_BACKING_BRIGHTNESS",["initial"]=0.48,["maximum"]=0.65,["minimum"]=0.2,["step"]=0.01,},{["description"]=" โ†ณ Transparency amount",["id"]="PT_BASE_ALPHA",["initial"]=0.2,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]="--- Gridline intensity (0=Less, 1=More)",["id"]="PT_BRIGHTNESS_GRID",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]="--- Brightness mode (0=Simple, 1=Natural)",["id"]="PT_BRIGHTNESS_MODE",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="=== Pixel Transparency v2.2 === (OFF/ON)",["id"]="PT_ENABLE",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Append to any LCD shader as last pass. Try with GBC & GBA.",["id"]="PT_HIDE_CONTENT",["initial"]=0,["maximum"]=2,["minimum"]=0,["step"]=1,},{["description"]="--- Pixel brightness (0=OFF, -Dim / +Bright)",["id"]="PT_HIGHLIGHTS",["initial"]=0.05,["maximum"]=1,["minimum"]=-1,["step"]=0.01,},{["description"]="--- Backing tint (0=OFF, 1/2=Warm, 3/4=Neutral)",["id"]="PT_PALETTE",["initial"]=3,["maximum"]=4,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Intensity",["id"]="PT_PALETTE_INTENSITY",["initial"]=1,["maximum"]=2,["minimum"]=0,["step"]=0.05,},{["description"]="--- Transparent pixels (0=White only, 1=Bright, 2=All)",["id"]="PT_PIXEL_MODE",["initial"]=1,["maximum"]=2,["minimum"]=0,["step"]=1,},{["description"]="--- Front polarizer tint (OFF/ON)",["id"]="PT_POLARIZER",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Intensity",["id"]="PT_POLARIZER_TINT",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]="--- Color saturation",["id"]="PT_SATURATION",["initial"]=1,["maximum"]=1.5,["minimum"]=0.5,["step"]=0.05,},{["description"]=" โ†ณ Blur amount",["id"]="PT_SHADOW_BLUR",["initial"]=1,["maximum"]=5,["minimum"]=0,["step"]=0.1,},{["description"]="== SHADOWS == (OFF/ON)",["id"]="PT_SHADOW_ENABLE",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Shadow quality (0=Low, 1=High)",["id"]="PT_SHADOW_FAST_BLUR",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Max offset",["id"]="PT_SHADOW_MAX_OFFSET",["initial"]=2.5,["maximum"]=30,["minimum"]=0,["step"]=0.5,},{["description"]=" โ†ณ Shadow motion (OFF/ON) ",["id"]="PT_SHADOW_MOTION",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Neutral X",["id"]="PT_SHADOW_MOTION_X",["initial"]=2,["maximum"]=30,["minimum"]=-30,["step"]=0.5,},{["description"]=" โ†ณ Neutral Y",["id"]="PT_SHADOW_MOTION_Y",["initial"]=2,["maximum"]=30,["minimum"]=-30,["step"]=0.5,},{["description"]=" โ†ณ X offset",["id"]="PT_SHADOW_OFFSET_X",["initial"]=3,["maximum"]=30,["minimum"]=-30,["step"]=0.5,},{["description"]=" โ†ณ Y offset",["id"]="PT_SHADOW_OFFSET_Y",["initial"]=3,["maximum"]=30,["minimum"]=-30,["step"]=0.5,},{["description"]=" โ†ณ Opacity",["id"]="PT_SHADOW_OPACITY",["initial"]=0.5,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]="--- White pixel detection threshold",["id"]="PT_THRESHOLD",["initial"]=0.9,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]="--- Boost white pixel transparency (OFF/ON)",["id"]="PT_WHITE_BOOST",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Boost amount",["id"]="PT_WHITE_TRANSPARENCY",["initial"]=0.5,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]="== COLOR ==",["id"]="SECTION1",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== TRANSPARENCY ==",["id"]="SECTION2",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== PERFORMANCE ==",["id"]="SECTION_PERF",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},{["index"]=0,["name"]="Original",["semantic"]="Original",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["size_uniforms"]={{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="unique",["name"]="PT_ACCEL_ENABLE",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_ACCEL_SENSITIVITY",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_MOTION",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_MAX_OFFSET",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_MOTION_X",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_MOTION_Y",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_OFFSET_X",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_OFFSET_Y",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_ENABLE",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_BRIGHTNESS_MODE",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_ENABLE",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_BLUR",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_OPACITY",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_THRESHOLD",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_FAST_BLUR",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_PIXEL_MODE",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_BASE_ALPHA",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},{["index"]=0,["kind"]="unique",["name"]="Gyroscope",["semantic"]="Gyroscope",},{["index"]=0,["kind"]="unique",["name"]="Accelerometer",["semantic"]="Accelerometer",},{["index"]=0,["kind"]="unique",["name"]="AccelerometerRest",["semantic"]="AccelerometerRest",},{["index"]=0,["kind"]="unique",["name"]="PT_HIDE_CONTENT",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_PALETTE",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_PALETTE_INTENSITY",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_BACKING_BRIGHTNESS",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_POLARIZER",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_POLARIZER_TINT",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SATURATION",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_HIGHLIGHTS",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_WHITE_BOOST",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_WHITE_TRANSPARENCY",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_BRIGHTNESS_GRID",["semantic"]="FloatParameter",},},["srgb_framebuffer"]=false,["vertex"]="#version 100\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ + vec3 Gyroscope;\ + vec3 Accelerometer;\ + vec3 AccelerometerRest;\ + float PT_HIDE_CONTENT;\ + float PT_PALETTE;\ + float PT_PALETTE_INTENSITY;\ + float PT_BACKING_BRIGHTNESS;\ + float PT_POLARIZER;\ + float PT_POLARIZER_TINT;\ + float PT_SATURATION;\ + float PT_HIGHLIGHTS;\ + float PT_WHITE_BOOST;\ + float PT_WHITE_TRANSPARENCY;\ + float PT_BRIGHTNESS_GRID;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 SourceSize;\ + vec4 OutputSize;\ + float PT_ACCEL_ENABLE;\ + float PT_ACCEL_SENSITIVITY;\ + float PT_SHADOW_MOTION;\ + float PT_SHADOW_MAX_OFFSET;\ + float PT_SHADOW_MOTION_X;\ + float PT_SHADOW_MOTION_Y;\ + float PT_SHADOW_OFFSET_X;\ + float PT_SHADOW_OFFSET_Y;\ + float PT_ENABLE;\ + float PT_BRIGHTNESS_MODE;\ + float PT_SHADOW_ENABLE;\ + float PT_SHADOW_BLUR;\ + float PT_SHADOW_OPACITY;\ + float PT_THRESHOLD;\ + float PT_SHADOW_FAST_BLUR;\ + float PT_PIXEL_MODE;\ + float PT_BASE_ALPHA;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +varying vec2 LIBRA_VARYING_1;\ +varying float LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +\ +vec2 getOrientedTilt(vec3 accel)\ +{\ + float magnitude = length(accel);\ + if (magnitude < 0.00999999977648258209228515625)\ + {\ + return vec2(0.0);\ + }\ + vec3 dir = accel / vec3(magnitude);\ + return vec2(-dir.x, dir.y);\ +}\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord;\ + LIBRA_VARYING_1 = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.zw;\ + float scale_x = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.x / 640.0;\ + float scale_y = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.y / 480.0;\ + LIBRA_VARYING_2 = sqrt(scale_x * scale_y);\ + vec2 gravity = vec2(0.0);\ + bool has_sensors = false;\ + if (LIBRA_PUSH_VERTEX_INSTANCE.PT_ACCEL_ENABLE > 0.5)\ + {\ + vec3 accel = LIBRA_UBO_VERTEX_INSTANCE.Accelerometer;\ + has_sensors = length(accel) > 0.00999999977648258209228515625;\ + if (has_sensors)\ + {\ + vec3 param = accel;\ + gravity = getOrientedTilt(param);\ + gravity = clamp(gravity * LIBRA_PUSH_VERTEX_INSTANCE.PT_ACCEL_SENSITIVITY, vec2(-1.0), vec2(1.0));\ + }\ + }\ + bool _132 = LIBRA_PUSH_VERTEX_INSTANCE.PT_SHADOW_MOTION > 0.5;\ + bool _138;\ + if (_132)\ + {\ + _138 = LIBRA_PUSH_VERTEX_INSTANCE.PT_ACCEL_ENABLE > 0.5;\ + }\ + else\ + {\ + _138 = _132;\ + }\ + if (_138 && has_sensors)\ + {\ + vec2 base = vec2(LIBRA_PUSH_VERTEX_INSTANCE.PT_SHADOW_MOTION_X, LIBRA_PUSH_VERTEX_INSTANCE.PT_SHADOW_MOTION_Y);\ + LIBRA_VARYING_3 = base + (gravity * LIBRA_PUSH_VERTEX_INSTANCE.PT_SHADOW_MAX_OFFSET);\ + }\ + else\ + {\ + LIBRA_VARYING_3 = vec2(LIBRA_PUSH_VERTEX_INSTANCE.PT_SHADOW_OFFSET_X, LIBRA_PUSH_VERTEX_INSTANCE.PT_SHADOW_OFFSET_Y);\ + }\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},},["textures"]={},} diff --git a/tests/data/shaderfx/es/zfast-lcd.lua b/tests/data/shaderfx/es/zfast-lcd.lua new file mode 100644 index 00000000..db9e9e05 --- /dev/null +++ b/tests/data/shaderfx/es/zfast-lcd.lua @@ -0,0 +1,78 @@ +return {["parameter_overrides"]={},["pass_count"]=1,["passes"]={{["filter"]="linear",["float_framebuffer"]=false,["fragment"]="#version 100\ +precision highp float;\ +precision highp int;\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + lowp int FrameCount;\ + float BORDERMULT;\ + float GBAGAMMA;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform highp sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +\ +void main()\ +{\ + vec2 texcoordInPixels = LIBRA_VARYING_0 * LIBRA_PUSH_FRAGMENT_INSTANCE.SourceSize.xy;\ + vec2 centerCoord = floor(texcoordInPixels) + vec2(0.5);\ + vec2 distFromCenter = abs(centerCoord - texcoordInPixels);\ + float Y = max(distFromCenter.x, distFromCenter.y);\ + Y *= Y;\ + float YY = Y * Y;\ + float YYY = YY * Y;\ + float LineWeight = YY - (2.7000000476837158203125 * YYY);\ + LineWeight = 1.0 - (LIBRA_PUSH_FRAGMENT_INSTANCE.BORDERMULT * LineWeight);\ + vec3 colour = texture2D(LIBRA_TEXTURE_Source, LIBRA_PUSH_FRAGMENT_INSTANCE.SourceSize.zw * centerCoord).xyz * LineWeight;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.GBAGAMMA > 0.5)\ + {\ + colour *= (vec3(0.60000002384185791015625) + (colour * 0.4000000059604644775390625));\ + }\ + gl_FragData[0] = vec4(colour, 1.0);\ +}\ +\ +",["frame_count_mod"]=0,["id"]="0",["mipmap_input"]=false,["parameters"]={{["description"]="Border Multiplier",["id"]="BORDERMULT",["initial"]=14,["maximum"]=40,["minimum"]=-40,["step"]=1,},{["description"]="GBA Gamma Hack",["id"]="GBAGAMMA",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["size_uniforms"]={{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="unique",["name"]="FrameCount",["semantic"]="FrameCount",},{["index"]=0,["kind"]="unique",["name"]="BORDERMULT",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="GBAGAMMA",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 100\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + lowp int FrameCount;\ + float BORDERMULT;\ + float GBAGAMMA;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord;\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},},["textures"]={},} diff --git a/tests/data/shaderfx/gl/bevel.lua b/tests/data/shaderfx/gl/bevel.lua new file mode 100644 index 00000000..769c88f8 --- /dev/null +++ b/tests/data/shaderfx/gl/bevel.lua @@ -0,0 +1,75 @@ +return {["parameter_overrides"]={},["pass_count"]=1,["passes"]={{["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 120\ +#extension GL_EXT_gpu_shader4 : require\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + uint FrameCount;\ + float BEVEL_LEVEL;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +\ +vec3 bevel(vec2 pos, vec3 color)\ +{\ + float r = sqrt(dot(pos, vec2(1.0)));\ + vec3 delta = mix(vec3(LIBRA_PUSH_FRAGMENT_INSTANCE.BEVEL_LEVEL), vec3(1.0 - LIBRA_PUSH_FRAGMENT_INSTANCE.BEVEL_LEVEL), color);\ + vec3 weight = delta * (1.0 - r);\ + return color + weight;\ +}\ +\ +void main()\ +{\ + vec2 position = fract(LIBRA_VARYING_0 * LIBRA_PUSH_FRAGMENT_INSTANCE.SourceSize.xy);\ + vec3 color = pow(texture2D(LIBRA_TEXTURE_Source, LIBRA_VARYING_0).xyz, vec3(2.400000095367431640625));\ + vec2 param = position;\ + vec3 param_1 = color;\ + color = clamp(bevel(param, param_1), vec3(0.0), vec3(1.0));\ + gl_FragData[0] = vec4(pow(color, vec3(0.4545454680919647216796875)), 1.0);\ +}\ +\ +",["frame_count_mod"]=0,["id"]="0",["mipmap_input"]=false,["parameters"]={{["description"]="Bevel Level",["id"]="BEVEL_LEVEL",["initial"]=0.2,["maximum"]=0.5,["minimum"]=0,["step"]=0.01,},},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["size_uniforms"]={{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="unique",["name"]="FrameCount",["semantic"]="FrameCount",},{["index"]=0,["kind"]="unique",["name"]="BEVEL_LEVEL",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 120\ +#extension GL_EXT_gpu_shader4 : require\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + uint FrameCount;\ + float BEVEL_LEVEL;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord;\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},},["textures"]={},} diff --git a/tests/data/shaderfx/gl/gameboy.lua b/tests/data/shaderfx/gl/gameboy.lua new file mode 100644 index 00000000..4e26233a --- /dev/null +++ b/tests/data/shaderfx/gl/gameboy.lua @@ -0,0 +1,973 @@ +return {["parameter_overrides"]={{["name"]="response_time",["value"]=0.33,},},["pass_count"]=5,["passes"]={{["alias"]="PASS0",["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 120\ +\ +struct ResType\ +{\ + vec2 _m0;\ + vec2 _m1;\ +};\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ + vec4 OriginalHistorySize1;\ + float color_toggle;\ + float pixel_size;\ + float pixel_softness;\ + float sharpening_amount;\ + float integer_mode;\ + float video_scale;\ + float baseline_alpha;\ + float grey_balance;\ + float response_time;\ + float brightness_mode;\ + float sharp_mode;\ + float pixel_shape;\ + float palette;\ + float auto_soften;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform sampler2D LIBRA_TEXTURE_Source;\ +uniform sampler2D LIBRA_TEXTURE_OriginalHistory1;\ +uniform sampler2D LIBRA_TEXTURE_OriginalHistory2;\ +uniform sampler2D LIBRA_TEXTURE_OriginalHistory3;\ +uniform sampler2D LIBRA_TEXTURE_OriginalHistory4;\ +uniform sampler2D LIBRA_TEXTURE_OriginalHistory5;\ +uniform sampler2D LIBRA_TEXTURE_OriginalHistory6;\ +uniform sampler2D LIBRA_TEXTURE_OriginalHistory7;\ +uniform sampler2D LIBRA_TEXTURE_COLOR_PALETTE;\ +\ +varying vec2 LIBRA_VARYING_0;\ +varying vec2 LIBRA_VARYING_9;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_4;\ +varying vec2 LIBRA_VARYING_5;\ +varying vec2 LIBRA_VARYING_6;\ +varying float LIBRA_VARYING_11;\ +varying float LIBRA_VARYING_10;\ +\ +float intersect_line_segment(float pixel_start, float pixel_end, float dot_start, float dot_end)\ +{\ + float overlap_start = max(pixel_start, dot_start);\ + float overlap_end = min(pixel_end, dot_end);\ + return max(overlap_end - overlap_start, 0.0);\ +}\ +\ +float intersect_rect_area(vec4 px_square, vec4 rect)\ +{\ + vec2 bl = max(px_square.xy, rect.xy);\ + vec2 tr = min(px_square.zw, rect.zw);\ + vec2 coverage = max(tr - bl, vec2(0.0));\ + return coverage.x * coverage.y;\ +}\ +\ +void main()\ +{\ + vec2 final_tex_coord = LIBRA_VARYING_0;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.integer_mode > 0.5)\ + {\ + vec2 centered_coord = abs(LIBRA_VARYING_0 - vec2(0.5));\ + bool _96 = centered_coord.x > (LIBRA_VARYING_9.x * 0.5);\ + bool _106;\ + if (!_96)\ + {\ + _106 = centered_coord.y > (LIBRA_VARYING_9.y * 0.5);\ + }\ + else\ + {\ + _106 = _96;\ + }\ + if (_106)\ + {\ + gl_FragData[0] = vec4(0.0);\ + return;\ + }\ + final_tex_coord = ((LIBRA_VARYING_0 - vec2(0.5)) / LIBRA_VARYING_9) + vec2(0.5);\ + }\ + vec3 foreground_source = texture2D(LIBRA_TEXTURE_Source, final_tex_coord).xyz;\ + vec3 curr_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_Source, final_tex_coord).xyz);\ + vec3 prev0_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory1, final_tex_coord).xyz);\ + vec3 prev1_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory2, final_tex_coord).xyz);\ + vec3 prev2_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory3, final_tex_coord).xyz);\ + vec3 prev3_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory4, final_tex_coord).xyz);\ + vec3 prev4_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory5, final_tex_coord).xyz);\ + vec3 prev5_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory6, final_tex_coord).xyz);\ + vec3 prev6_rgb = abs(vec3(1.0) - texture2D(LIBRA_TEXTURE_OriginalHistory7, final_tex_coord).xyz);\ + float is_on_dot = 0.0;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.integer_mode > 0.5)\ + {\ + bool _218 = mod(final_tex_coord.x, LIBRA_VARYING_2.x) > LIBRA_VARYING_3.x;\ + bool _229;\ + if (_218)\ + {\ + _229 = mod(final_tex_coord.y, LIBRA_VARYING_2.y) > LIBRA_VARYING_3.y;\ + }\ + else\ + {\ + _229 = _218;\ + }\ + if (_229)\ + {\ + is_on_dot = 1.0;\ + }\ + }\ + else\ + {\ + ResType _238;\ + _238._m1 = vec2(ivec2(LIBRA_VARYING_4));\ + _238._m0 = LIBRA_VARYING_4 - _238._m1;\ + vec2 tx_coord_i = _238._m1;\ + vec2 tx_coord_f = _238._m0;\ + vec2 pixel_center = (tx_coord_f - vec2(0.5)) * LIBRA_VARYING_5;\ + vec4 pixel_rect = vec4(pixel_center - (LIBRA_VARYING_5 * 0.5), pixel_center + (LIBRA_VARYING_5 * 0.5));\ + vec4 dot_rect = vec4((-LIBRA_VARYING_6) * 0.5, LIBRA_VARYING_6 * 0.5);\ + float param = pixel_rect.x;\ + float param_1 = pixel_rect.z;\ + float param_2 = dot_rect.x;\ + float param_3 = dot_rect.z;\ + float x_coverage = intersect_line_segment(param, param_1, param_2, param_3) / LIBRA_VARYING_5.x;\ + float param_4 = pixel_rect.y;\ + float param_5 = pixel_rect.w;\ + float param_6 = dot_rect.y;\ + float param_7 = dot_rect.w;\ + float y_coverage = intersect_line_segment(param_4, param_5, param_6, param_7) / LIBRA_VARYING_5.y;\ + float rect_linear = x_coverage * y_coverage;\ + float rect_sharpened;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.sharp_mode < 0.5)\ + {\ + float sharp_factor = 1.0 / max(LIBRA_VARYING_11, 0.001000000047497451305389404296875);\ + rect_sharpened = pow(x_coverage, sharp_factor) * pow(y_coverage, sharp_factor);\ + }\ + else\ + {\ + float sigmoid_strength = 10.0 / max(LIBRA_VARYING_11, 0.001000000047497451305389404296875);\ + float x_sharp = 1.0 / (1.0 + exp((-sigmoid_strength) * (x_coverage - 0.5)));\ + float y_sharp = 1.0 / (1.0 + exp((-sigmoid_strength) * (y_coverage - 0.5)));\ + rect_sharpened = x_sharp * y_sharp;\ + }\ + float rect_coverage = mix(rect_linear, rect_sharpened, LIBRA_PUSH_FRAGMENT_INSTANCE.sharpening_amount);\ + vec4 param_8 = pixel_rect;\ + vec4 param_9 = dot_rect;\ + float circ_linear = intersect_rect_area(param_8, param_9) / (LIBRA_VARYING_5.x * LIBRA_VARYING_5.y);\ + float circ_sharpened;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.sharp_mode < 0.5)\ + {\ + circ_sharpened = pow(circ_linear, 1.0 / max(LIBRA_VARYING_11, 0.001000000047497451305389404296875));\ + }\ + else\ + {\ + float sigmoid_strength_1 = 10.0 / max(LIBRA_VARYING_11, 0.001000000047497451305389404296875);\ + circ_sharpened = 1.0 / (1.0 + exp((-sigmoid_strength_1) * (circ_linear - 0.5)));\ + }\ + float circ_coverage = mix(circ_linear, circ_sharpened, LIBRA_PUSH_FRAGMENT_INSTANCE.sharpening_amount);\ + is_on_dot = mix(circ_coverage, rect_coverage, LIBRA_PUSH_FRAGMENT_INSTANCE.pixel_shape);\ + }\ + vec3 input_rgb = curr_rgb;\ + input_rgb += ((prev0_rgb - input_rgb) * LIBRA_PUSH_FRAGMENT_INSTANCE.response_time);\ + input_rgb += ((prev1_rgb - input_rgb) * pow(LIBRA_PUSH_FRAGMENT_INSTANCE.response_time, 2.0));\ + input_rgb += ((prev2_rgb - input_rgb) * pow(LIBRA_PUSH_FRAGMENT_INSTANCE.response_time, 3.0));\ + input_rgb += ((prev3_rgb - input_rgb) * pow(LIBRA_PUSH_FRAGMENT_INSTANCE.response_time, 4.0));\ + input_rgb += ((prev4_rgb - input_rgb) * pow(LIBRA_PUSH_FRAGMENT_INSTANCE.response_time, 5.0));\ + input_rgb += ((prev5_rgb - input_rgb) * pow(LIBRA_PUSH_FRAGMENT_INSTANCE.response_time, 6.0));\ + input_rgb += ((prev6_rgb - input_rgb) * pow(LIBRA_PUSH_FRAGMENT_INSTANCE.response_time, 7.0));\ + float brightness;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.brightness_mode < 0.5)\ + {\ + brightness = (input_rgb.x + input_rgb.y) + input_rgb.z;\ + }\ + else\ + {\ + brightness = ((0.2125999927520751953125 * input_rgb.x) + (0.715200006961822509765625 * input_rgb.y)) + (0.072200000286102294921875 * input_rgb.z);\ + }\ + float grey_balance_adjusted = LIBRA_PUSH_FRAGMENT_INSTANCE.grey_balance / LIBRA_VARYING_10;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.brightness_mode >= 0.5)\ + {\ + grey_balance_adjusted /= 3.0;\ + }\ + float rgb_to_alpha = (brightness / grey_balance_adjusted) + LIBRA_PUSH_FRAGMENT_INSTANCE.baseline_alpha;\ + vec3 foreground_color;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 0.5)\ + {\ + foreground_color = texture2D(LIBRA_TEXTURE_COLOR_PALETTE, vec2(0.75, 0.5)).xyz;\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 1.5)\ + {\ + foreground_color = vec3(0.0670000016689300537109375, 0.097999997437000274658203125, 0.13300000131130218505859375);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 2.5)\ + {\ + foreground_color = vec3(0.125);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 3.5)\ + {\ + foreground_color = vec3(0.0);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 4.5)\ + {\ + foreground_color = vec3(0.114000000059604644775390625, 0.41600000858306884765625, 0.4199999868869781494140625);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 5.5)\ + {\ + foreground_color = vec3(0.0, 0.324999988079071044921875, 0.20000000298023223876953125);\ + }\ + else\ + {\ + foreground_color = vec3(0.0, 0.324999988079071044921875, 0.31400001049041748046875);\ + }\ + }\ + }\ + }\ + }\ + }\ + vec4 out_color;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.color_toggle == 0.0)\ + {\ + out_color = vec4(foreground_color, rgb_to_alpha);\ + }\ + else\ + {\ + out_color = vec4(foreground_source, rgb_to_alpha);\ + }\ + out_color.w *= is_on_dot;\ + gl_FragData[0] = out_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="0",["mipmap_input"]=false,["parameters"]={{["description"]="=== GAME BOY DOT MATRIX SHADER v1.2 ===",["id"]="GAMEBOY_SHADER",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== LCD effects ==",["id"]="LCD_EFFECTS",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Turn OFF Integer Scale in Settings > Video > Scaling",["id"]="NOTE1",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * GBC: Turn OFF Core > Color Correction & Interframe Blending",["id"]="NOTE2",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * GBA: Turn ON Core > Color Correction & Interframe Blending",["id"]="NOTE3",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Soft / circular pixels = less grid distortion",["id"]="NOTE5",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Screen position ==",["id"]="SCREEN_POSITIONING",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Blend amount",["id"]="adjacent_texel_alpha_blending",["initial"]=0.1755,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Auto soften (reduce distortion)",["id"]="auto_soften",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Base pixel transparency",["id"]="baseline_alpha",["initial"]=0.1,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]=" โ†ณ Background texture smoothing",["id"]="bg_smoothing",["initial"]=0.75,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]="== Pixel blending mode == (0=Blend gaps, 1=Blend all)",["id"]="blending_mode",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Pixel transparency detection == (0=Simple, 1=Perceptual)",["id"]="brightness_mode",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Color mode == (0=Grayscale, 1=Color)",["id"]="color_toggle",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Contrast",["id"]="contrast",["initial"]=0.95,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Grey balance",["id"]="grey_balance",["initial"]=3,["maximum"]=4,["minimum"]=0,["step"]=0.1,},{["description"]="== Display mode == (0=Full, 1=Pixel Perfect, 2=Scale factor)",["id"]="integer_mode",["initial"]=0,["maximum"]=2,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Palette (0=IMG, 1/2=Pocket, 3=B&W, 4=DMG, 5/6=Light)",["id"]="palette",["initial"]=0,["maximum"]=6,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Pixel opacity",["id"]="pixel_opacity",["initial"]=1,["maximum"]=1,["minimum"]=0.01,["step"]=0.01,},{["description"]=" โ†ณ Pixel shape [Sharp mode] (Circle/Rectangle)",["id"]="pixel_shape",["initial"]=1,["maximum"]=1.3,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Pixel size",["id"]="pixel_size",["initial"]=0.8,["maximum"]=1.1,["minimum"]=0.2,["step"]=0.05,},{["description"]=" โ†ณ Pixel softness",["id"]="pixel_softness",["initial"]=1,["maximum"]=5,["minimum"]=0.2,["step"]=0.05,},{["description"]=" โ†ณ Latency",["id"]="response_time",["initial"]=0,["maximum"]=0.777,["minimum"]=0,["step"]=0.111,},{["description"]=" โ†ณ Screen light",["id"]="screen_light",["initial"]=1,["maximum"]=2,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Screen offset horizontal",["id"]="screen_offset_x",["initial"]=0,["maximum"]=5,["minimum"]=-5,["step"]=1,},{["description"]=" โ†ณ Screen offset vertical",["id"]="screen_offset_y",["initial"]=0,["maximum"]=5,["minimum"]=-5,["step"]=1,},{["description"]="== Drop shadows == (OFF/ON)",["id"]="shadow_enable",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Max offset",["id"]="shadow_max_offset",["initial"]=2.5,["maximum"]=30,["minimum"]=0,["step"]=0.5,},{["description"]=" โ†ณ Shadow motion (OFF/ON)",["id"]="shadow_motion",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Shadow offset horizontal",["id"]="shadow_offset_x",["initial"]=1,["maximum"]=5,["minimum"]=-5,["step"]=0.5,},{["description"]=" โ†ณ Shadow offset vertical",["id"]="shadow_offset_y",["initial"]=1,["maximum"]=5,["minimum"]=-5,["step"]=0.5,},{["description"]=" โ†ณ Shadow opacity",["id"]="shadow_opacity",["initial"]=0.55,["maximum"]=1,["minimum"]=0.01,["step"]=0.01,},{["description"]="== Fullscreen pixels == (0=Soft, 1=Sharp)",["id"]="sharp_mode",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Pixel edge sharpness",["id"]="sharpening_amount",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Scale factor",["id"]="video_scale",["initial"]=5,["maximum"]=15,["minimum"]=2,["step"]=1,},},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},{["index"]=1,["name"]="OriginalHistory1",["semantic"]="OriginalHistory",},{["index"]=2,["name"]="OriginalHistory2",["semantic"]="OriginalHistory",},{["index"]=3,["name"]="OriginalHistory3",["semantic"]="OriginalHistory",},{["index"]=4,["name"]="OriginalHistory4",["semantic"]="OriginalHistory",},{["index"]=5,["name"]="OriginalHistory5",["semantic"]="OriginalHistory",},{["index"]=6,["name"]="OriginalHistory6",["semantic"]="OriginalHistory",},{["index"]=7,["name"]="OriginalHistory7",["semantic"]="OriginalHistory",},{["index"]=0,["name"]="COLOR_PALETTE",["semantic"]="User",["user_name"]="COLOR_PALETTE",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["size_uniforms"]={{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=1,["kind"]="texture",["name"]="OriginalHistorySize1",["semantic"]="OriginalHistory",},{["index"]=0,["kind"]="unique",["name"]="color_toggle",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="pixel_size",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="pixel_softness",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="sharpening_amount",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="integer_mode",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="video_scale",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="baseline_alpha",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="grey_balance",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="response_time",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="brightness_mode",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="sharp_mode",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="pixel_shape",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="palette",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="auto_soften",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 120\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ + vec4 OriginalHistorySize1;\ + float color_toggle;\ + float pixel_size;\ + float pixel_softness;\ + float sharpening_amount;\ + float integer_mode;\ + float video_scale;\ + float baseline_alpha;\ + float grey_balance;\ + float response_time;\ + float brightness_mode;\ + float sharp_mode;\ + float pixel_shape;\ + float palette;\ + float auto_soften;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +varying vec2 LIBRA_VARYING_9;\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_4;\ +varying vec2 LIBRA_VARYING_5;\ +varying vec2 LIBRA_VARYING_6;\ +varying float LIBRA_VARYING_11;\ +varying float LIBRA_VARYING_10;\ +float video_scale_factor;\ +vec2 original_coord;\ +vec2 screen_bounds;\ +\ +float intersect_line_segment(float pixel_start, float pixel_end, float dot_start, float dot_end)\ +{\ + float overlap_start = max(pixel_start, dot_start);\ + float overlap_end = min(pixel_end, dot_end);\ + return max(overlap_end - overlap_start, 0.0);\ +}\ +\ +float intersect_rect_area(vec4 px_square, vec4 rect)\ +{\ + vec2 bl = max(px_square.xy, rect.xy);\ + vec2 tr = min(px_square.zw, rect.zw);\ + vec2 coverage = max(tr - bl, vec2(0.0));\ + return coverage.x * coverage.y;\ +}\ +\ +float calculate_coverage_at_point(vec2 tx_coord_f, float pixel_size, float pixel_softness, float sharpening_amount, float pixel_shape, float sharp_mode)\ +{\ + vec2 pixel_center = tx_coord_f - vec2(0.5);\ + vec4 pixel_rect = vec4(pixel_center - vec2(0.5), pixel_center + vec2(0.5));\ + vec2 dot_size_in_px = vec2(pixel_size);\ + vec4 dot_rect = vec4((-dot_size_in_px) * 0.5, dot_size_in_px * 0.5);\ + float param = pixel_rect.x;\ + float param_1 = pixel_rect.z;\ + float param_2 = dot_rect.x;\ + float param_3 = dot_rect.z;\ + float x_coverage = intersect_line_segment(param, param_1, param_2, param_3);\ + float param_4 = pixel_rect.y;\ + float param_5 = pixel_rect.w;\ + float param_6 = dot_rect.y;\ + float param_7 = dot_rect.w;\ + float y_coverage = intersect_line_segment(param_4, param_5, param_6, param_7);\ + float rect_linear = x_coverage * y_coverage;\ + float rect_sharpened;\ + if (sharp_mode < 0.5)\ + {\ + float sharp_factor = 1.0 / max(pixel_softness, 0.001000000047497451305389404296875);\ + rect_sharpened = pow(x_coverage, sharp_factor) * pow(y_coverage, sharp_factor);\ + }\ + else\ + {\ + float sigmoid_strength = 10.0 / max(pixel_softness, 0.001000000047497451305389404296875);\ + float x_sharp = 1.0 / (1.0 + exp((-sigmoid_strength) * (x_coverage - 0.5)));\ + float y_sharp = 1.0 / (1.0 + exp((-sigmoid_strength) * (y_coverage - 0.5)));\ + rect_sharpened = x_sharp * y_sharp;\ + }\ + float rect_coverage = mix(rect_linear, rect_sharpened, sharpening_amount);\ + vec4 param_8 = pixel_rect;\ + vec4 param_9 = dot_rect;\ + float circ_linear = intersect_rect_area(param_8, param_9);\ + float circ_sharpened;\ + if (sharp_mode < 0.5)\ + {\ + circ_sharpened = pow(circ_linear, 1.0 / max(pixel_softness, 0.001000000047497451305389404296875));\ + }\ + else\ + {\ + float sigmoid_strength_1 = 10.0 / max(pixel_softness, 0.001000000047497451305389404296875);\ + circ_sharpened = 1.0 / (1.0 + exp((-sigmoid_strength_1) * (circ_linear - 0.5)));\ + }\ + float circ_coverage = mix(circ_linear, circ_sharpened, sharpening_amount);\ + return mix(circ_coverage, rect_coverage, pixel_shape);\ +}\ +\ +float sample_average_coverage(float pixel_size, float pixel_softness, float sharpening_amount, float pixel_shape, float sharp_mode)\ +{\ + float total = 0.0;\ + for (int y = 0; y < 4; y++)\ + {\ + for (int x = 0; x < 4; x++)\ + {\ + vec2 sample_pos = vec2((-0.375) + (float(x) * 0.25), (-0.375) + (float(y) * 0.25));\ + vec2 param = sample_pos;\ + float param_1 = pixel_size;\ + float param_2 = pixel_softness;\ + float param_3 = sharpening_amount;\ + float param_4 = pixel_shape;\ + float param_5 = sharp_mode;\ + total += calculate_coverage_at_point(param, param_1, param_2, param_3, param_4, param_5);\ + }\ + }\ + return total / 16.0;\ +}\ +\ +void main()\ +{\ + if (LIBRA_PUSH_VERTEX_INSTANCE.integer_mode > 0.5)\ + {\ + if (LIBRA_PUSH_VERTEX_INSTANCE.integer_mode > 1.5)\ + {\ + video_scale_factor = LIBRA_PUSH_VERTEX_INSTANCE.video_scale;\ + }\ + else\ + {\ + video_scale_factor = floor(LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.y * LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.w);\ + }\ + vec2 scaled_video_out = LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.xy * vec2(video_scale_factor);\ + LIBRA_VARYING_9 = scaled_video_out / LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.xy;\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord * 1.00010001659393310546875;\ + original_coord = TexCoord;\ + LIBRA_VARYING_2 = LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ + LIBRA_VARYING_3 = vec2(1.0) / (LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.xy * video_scale_factor);\ + screen_bounds = LIBRA_VARYING_9;\ + LIBRA_VARYING_4 = vec2(0.0);\ + LIBRA_VARYING_5 = vec2(0.0);\ + LIBRA_VARYING_6 = vec2(0.0);\ + }\ + else\ + {\ + video_scale_factor = 1.0;\ + LIBRA_VARYING_9 = vec2(1.0);\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord;\ + original_coord = TexCoord;\ + LIBRA_VARYING_4 = TexCoord * LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.xy;\ + LIBRA_VARYING_5 = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.xy / LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.xy;\ + LIBRA_VARYING_6 = LIBRA_VARYING_5 * LIBRA_PUSH_VERTEX_INSTANCE.pixel_size;\ + LIBRA_VARYING_2 = vec2(0.0);\ + LIBRA_VARYING_3 = vec2(0.0);\ + screen_bounds = vec2(1.0);\ + }\ + LIBRA_VARYING_11 = LIBRA_PUSH_VERTEX_INSTANCE.pixel_softness;\ + bool _417 = LIBRA_PUSH_VERTEX_INSTANCE.auto_soften > 0.5;\ + bool _423;\ + if (_417)\ + {\ + _423 = LIBRA_PUSH_VERTEX_INSTANCE.integer_mode < 0.5;\ + }\ + else\ + {\ + _423 = _417;\ + }\ + if (_423)\ + {\ + vec2 scale = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.xy / LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.xy;\ + vec2 fract_scale = fract(scale);\ + float fract_factor = max(0.5 - abs(fract_scale.x - 0.5), 0.5 - abs(fract_scale.y - 0.5));\ + LIBRA_VARYING_11 += (fract_factor * 4.0);\ + }\ + if (LIBRA_PUSH_VERTEX_INSTANCE.integer_mode < 0.5)\ + {\ + float param = 0.800000011920928955078125;\ + float param_1 = 0.800000011920928955078125;\ + float param_2 = 1.0;\ + float param_3 = 1.0;\ + float param_4 = LIBRA_PUSH_VERTEX_INSTANCE.sharp_mode;\ + float ref_brightness = sample_average_coverage(param, param_1, param_2, param_3, param_4);\ + float param_5 = LIBRA_PUSH_VERTEX_INSTANCE.pixel_size;\ + float param_6 = LIBRA_VARYING_11;\ + float param_7 = LIBRA_PUSH_VERTEX_INSTANCE.sharpening_amount;\ + float param_8 = LIBRA_PUSH_VERTEX_INSTANCE.pixel_shape;\ + float param_9 = LIBRA_PUSH_VERTEX_INSTANCE.sharp_mode;\ + float cur_brightness = sample_average_coverage(param_5, param_6, param_7, param_8, param_9);\ + float _492;\ + if (LIBRA_PUSH_VERTEX_INSTANCE.sharp_mode < 0.5)\ + {\ + _492 = 2.7999999523162841796875;\ + }\ + else\ + {\ + _492 = mix(2.5, 1.60000002384185791015625, LIBRA_PUSH_VERTEX_INSTANCE.pixel_shape);\ + }\ + float base_comp = _492;\ + float brightness_ratio = ref_brightness / max(cur_brightness, 0.001000000047497451305389404296875);\ + float _512;\ + if (LIBRA_PUSH_VERTEX_INSTANCE.pixel_size > 0.800000011920928955078125)\ + {\ + _512 = pow(LIBRA_PUSH_VERTEX_INSTANCE.pixel_size / 0.800000011920928955078125, 0.62000000476837158203125);\ + }\ + else\ + {\ + _512 = 1.0;\ + }\ + float size_comp = _512;\ + float _525;\ + if (LIBRA_VARYING_11 > 1.0)\ + {\ + _525 = pow(LIBRA_VARYING_11, 0.4440000057220458984375);\ + }\ + else\ + {\ + _525 = 1.0;\ + }\ + float softness_comp = _525;\ + float _537;\ + if (LIBRA_PUSH_VERTEX_INSTANCE.sharp_mode < 0.5)\ + {\ + _537 = mix(0.85699999332427978515625, 1.0, LIBRA_PUSH_VERTEX_INSTANCE.sharpening_amount);\ + }\ + else\ + {\ + _537 = mix(1.5, 1.0, LIBRA_PUSH_VERTEX_INSTANCE.sharpening_amount);\ + }\ + float sharpening_comp = _537;\ + bool _552 = LIBRA_PUSH_VERTEX_INSTANCE.sharp_mode >= 0.5;\ + bool _558;\ + if (_552)\ + {\ + _558 = LIBRA_PUSH_VERTEX_INSTANCE.pixel_shape > 1.0;\ + }\ + else\ + {\ + _558 = _552;\ + }\ + float _559;\ + if (_558)\ + {\ + _559 = 1.0 + ((LIBRA_PUSH_VERTEX_INSTANCE.pixel_shape - 1.0) * 0.666999995708465576171875);\ + }\ + else\ + {\ + _559 = 1.0;\ + }\ + float shape_comp = _559;\ + LIBRA_VARYING_10 = ((((base_comp * brightness_ratio) * size_comp) * softness_comp) * sharpening_comp) * shape_comp;\ + }\ + else\ + {\ + LIBRA_VARYING_10 = 1.0;\ + }\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},{["alias"]="PASS1",["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 120\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ + float blending_mode;\ + float adjacent_texel_alpha_blending;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_6;\ +varying vec2 LIBRA_VARYING_7;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_4;\ +varying vec2 LIBRA_VARYING_5;\ +\ +float blending_modifier(float color)\ +{\ + float blend_bool = float(int(color == 0.0));\ + return clamp(blend_bool + LIBRA_PUSH_FRAGMENT_INSTANCE.blending_mode, 0.0, 1.0);\ +}\ +\ +void main()\ +{\ + vec4 out_color = texture2D(LIBRA_TEXTURE_Source, LIBRA_VARYING_0);\ + vec2 blur_coords_up_clamped = clamp(LIBRA_VARYING_2, LIBRA_VARYING_6, LIBRA_VARYING_7);\ + vec2 blur_coords_down_clamped = clamp(LIBRA_VARYING_3, LIBRA_VARYING_6, LIBRA_VARYING_7);\ + vec2 blur_coords_right_clamped = clamp(LIBRA_VARYING_4, LIBRA_VARYING_6, LIBRA_VARYING_7);\ + vec2 blur_coords_left_clamped = clamp(LIBRA_VARYING_5, LIBRA_VARYING_6, LIBRA_VARYING_7);\ + vec4 adjacent_texel_1 = texture2D(LIBRA_TEXTURE_Source, blur_coords_up_clamped);\ + vec4 adjacent_texel_2 = texture2D(LIBRA_TEXTURE_Source, blur_coords_down_clamped);\ + vec4 adjacent_texel_3 = texture2D(LIBRA_TEXTURE_Source, blur_coords_right_clamped);\ + vec4 adjacent_texel_4 = texture2D(LIBRA_TEXTURE_Source, blur_coords_left_clamped);\ + float param = out_color.w;\ + out_color.w -= ((((((out_color.w - adjacent_texel_1.w) + (out_color.w - adjacent_texel_2.w)) + (out_color.w - adjacent_texel_3.w)) + (out_color.w - adjacent_texel_4.w)) * LIBRA_PUSH_FRAGMENT_INSTANCE.adjacent_texel_alpha_blending) * blending_modifier(param));\ + gl_FragData[0] = out_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="1",["mipmap_input"]=false,["parameters"]={{["description"]="=== GAME BOY DOT MATRIX SHADER v1.2 ===",["id"]="GAMEBOY_SHADER",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== LCD effects ==",["id"]="LCD_EFFECTS",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Turn OFF Integer Scale in Settings > Video > Scaling",["id"]="NOTE1",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * GBC: Turn OFF Core > Color Correction & Interframe Blending",["id"]="NOTE2",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * GBA: Turn ON Core > Color Correction & Interframe Blending",["id"]="NOTE3",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Soft / circular pixels = less grid distortion",["id"]="NOTE5",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Screen position ==",["id"]="SCREEN_POSITIONING",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Blend amount",["id"]="adjacent_texel_alpha_blending",["initial"]=0.1755,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Auto soften (reduce distortion)",["id"]="auto_soften",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Base pixel transparency",["id"]="baseline_alpha",["initial"]=0.1,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]=" โ†ณ Background texture smoothing",["id"]="bg_smoothing",["initial"]=0.75,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]="== Pixel blending mode == (0=Blend gaps, 1=Blend all)",["id"]="blending_mode",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Pixel transparency detection == (0=Simple, 1=Perceptual)",["id"]="brightness_mode",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Color mode == (0=Grayscale, 1=Color)",["id"]="color_toggle",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Contrast",["id"]="contrast",["initial"]=0.95,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Grey balance",["id"]="grey_balance",["initial"]=3,["maximum"]=4,["minimum"]=0,["step"]=0.1,},{["description"]="== Display mode == (0=Full, 1=Pixel Perfect, 2=Scale factor)",["id"]="integer_mode",["initial"]=0,["maximum"]=2,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Palette (0=IMG, 1/2=Pocket, 3=B&W, 4=DMG, 5/6=Light)",["id"]="palette",["initial"]=0,["maximum"]=6,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Pixel opacity",["id"]="pixel_opacity",["initial"]=1,["maximum"]=1,["minimum"]=0.01,["step"]=0.01,},{["description"]=" โ†ณ Pixel shape [Sharp mode] (Circle/Rectangle)",["id"]="pixel_shape",["initial"]=1,["maximum"]=1.3,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Pixel size",["id"]="pixel_size",["initial"]=0.8,["maximum"]=1.1,["minimum"]=0.2,["step"]=0.05,},{["description"]=" โ†ณ Pixel softness",["id"]="pixel_softness",["initial"]=1,["maximum"]=5,["minimum"]=0.2,["step"]=0.05,},{["description"]=" โ†ณ Latency",["id"]="response_time",["initial"]=0,["maximum"]=0.777,["minimum"]=0,["step"]=0.111,},{["description"]=" โ†ณ Screen light",["id"]="screen_light",["initial"]=1,["maximum"]=2,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Screen offset horizontal",["id"]="screen_offset_x",["initial"]=0,["maximum"]=5,["minimum"]=-5,["step"]=1,},{["description"]=" โ†ณ Screen offset vertical",["id"]="screen_offset_y",["initial"]=0,["maximum"]=5,["minimum"]=-5,["step"]=1,},{["description"]="== Drop shadows == (OFF/ON)",["id"]="shadow_enable",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Max offset",["id"]="shadow_max_offset",["initial"]=2.5,["maximum"]=30,["minimum"]=0,["step"]=0.5,},{["description"]=" โ†ณ Shadow motion (OFF/ON)",["id"]="shadow_motion",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Shadow offset horizontal",["id"]="shadow_offset_x",["initial"]=1,["maximum"]=5,["minimum"]=-5,["step"]=0.5,},{["description"]=" โ†ณ Shadow offset vertical",["id"]="shadow_offset_y",["initial"]=1,["maximum"]=5,["minimum"]=-5,["step"]=0.5,},{["description"]=" โ†ณ Shadow opacity",["id"]="shadow_opacity",["initial"]=0.55,["maximum"]=1,["minimum"]=0.01,["step"]=0.01,},{["description"]="== Fullscreen pixels == (0=Soft, 1=Sharp)",["id"]="sharp_mode",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Pixel edge sharpness",["id"]="sharpening_amount",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Scale factor",["id"]="video_scale",["initial"]=5,["maximum"]=15,["minimum"]=2,["step"]=1,},},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["size_uniforms"]={{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="unique",["name"]="blending_mode",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="adjacent_texel_alpha_blending",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 120\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ + float blending_mode;\ + float adjacent_texel_alpha_blending;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_4;\ +varying vec2 LIBRA_VARYING_5;\ +varying vec2 LIBRA_VARYING_6;\ +varying vec2 LIBRA_VARYING_7;\ +vec2 texel;\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord * 1.00010001659393310546875;\ + texel = LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ + LIBRA_VARYING_3 = LIBRA_VARYING_0 + vec2(0.0, texel.y);\ + LIBRA_VARYING_2 = LIBRA_VARYING_0 + vec2(0.0, -texel.y);\ + LIBRA_VARYING_4 = LIBRA_VARYING_0 + vec2(texel.x, 0.0);\ + LIBRA_VARYING_5 = LIBRA_VARYING_0 + vec2(-texel.x, 0.0);\ + LIBRA_VARYING_6 = vec2(0.0);\ + LIBRA_VARYING_7 = texel * (LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.xy - vec2(2.0));\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},{["alias"]="PASS2",["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 120\ +\ +const float _17[5] = float[](0.0, 1.0, 2.0, 3.0, 4.0);\ +const float _24[5] = float[](0.134658336639404296875, 0.1305153369903564453125, 0.118835575878620147705078125, 0.10164546966552734375, 0.08167444169521331787109375);\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_1;\ +\ +void main()\ +{\ + vec4 out_color = texture2D(LIBRA_TEXTURE_Source, clamp(LIBRA_VARYING_0, LIBRA_VARYING_2, LIBRA_VARYING_3)) * _24[0];\ + for (int i = 1; i < 5; i++)\ + {\ + out_color.w += (texture2D(LIBRA_TEXTURE_Source, clamp(LIBRA_VARYING_0 + vec2(_17[i] * LIBRA_VARYING_1.x, 0.0), LIBRA_VARYING_2, LIBRA_VARYING_3)).w * _24[i]);\ + out_color.w += (texture2D(LIBRA_TEXTURE_Source, clamp(LIBRA_VARYING_0 - vec2(_17[i] * LIBRA_VARYING_1.x, 0.0), LIBRA_VARYING_2, LIBRA_VARYING_3)).w * _24[i]);\ + }\ + gl_FragData[0] = out_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="2",["mipmap_input"]=false,["parameters"]={},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["size_uniforms"]={{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 120\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +varying vec2 LIBRA_VARYING_1;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord * 1.00010001659393310546875;\ + LIBRA_VARYING_1 = LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ + LIBRA_VARYING_2 = vec2(0.0);\ + LIBRA_VARYING_3 = LIBRA_VARYING_1 * (LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.xy - vec2(1.0));\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},{["alias"]="PASS3",["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 120\ +\ +const float _17[5] = float[](0.0, 1.0, 2.0, 3.0, 4.0);\ +const float _24[5] = float[](0.134658336639404296875, 0.1305153369903564453125, 0.118835575878620147705078125, 0.10164546966552734375, 0.08167444169521331787109375);\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_1;\ +\ +void main()\ +{\ + vec4 out_color = texture2D(LIBRA_TEXTURE_Source, clamp(LIBRA_VARYING_0, LIBRA_VARYING_2, LIBRA_VARYING_3)) * _24[0];\ + for (int i = 1; i < 5; i++)\ + {\ + out_color.w += (texture2D(LIBRA_TEXTURE_Source, clamp(LIBRA_VARYING_0 + vec2(0.0, _17[i] * LIBRA_VARYING_1.y), LIBRA_VARYING_2, LIBRA_VARYING_3)).w * _24[i]);\ + out_color.w += (texture2D(LIBRA_TEXTURE_Source, clamp(LIBRA_VARYING_0 - vec2(0.0, _17[i] * LIBRA_VARYING_1.y), LIBRA_VARYING_2, LIBRA_VARYING_3)).w * _24[i]);\ + }\ + gl_FragData[0] = out_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="3",["mipmap_input"]=false,["parameters"]={},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["size_uniforms"]={{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 120\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +varying vec2 LIBRA_VARYING_1;\ +varying vec2 LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord * 1.00010001659393310546875;\ + LIBRA_VARYING_1 = LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ + LIBRA_VARYING_2 = vec2(0.0);\ + LIBRA_VARYING_3 = LIBRA_VARYING_1 * (LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.xy - vec2(1.0));\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},{["alias"]="PASS4",["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 120\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ + vec3 Gyroscope;\ + vec3 Accelerometer;\ + vec3 AccelerometerRest;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ + vec4 PassOutputSize1;\ + float contrast;\ + float screen_light;\ + float pixel_opacity;\ + float bg_smoothing;\ + float shadow_opacity;\ + float shadow_offset_x;\ + float shadow_offset_y;\ + float screen_offset_x;\ + float screen_offset_y;\ + float shadow_enable;\ + float palette;\ + float integer_mode;\ + float shadow_motion;\ + float shadow_max_offset;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform sampler2D LIBRA_TEXTURE_COLOR_PALETTE;\ +uniform sampler2D LIBRA_TEXTURE_PassOutput1;\ +uniform sampler2D LIBRA_TEXTURE_BACKGROUND;\ +uniform sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_1;\ +\ +vec4 get_bg_color()\ +{\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 0.5)\ + {\ + return texture2D(LIBRA_TEXTURE_COLOR_PALETTE, vec2(0.25, 0.5));\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 1.5)\ + {\ + return vec4(0.65100002288818359375, 0.675000011920928955078125, 0.51800000667572021484375, 1.0);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 2.5)\ + {\ + return vec4(0.736999988555908203125, 0.736999988555908203125, 0.736999988555908203125, 1.0);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 3.5)\ + {\ + return vec4(1.0);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 4.5)\ + {\ + return vec4(0.62699997425079345703125, 0.666999995708465576171875, 0.0240000002086162567138671875, 1.0);\ + }\ + else\ + {\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.palette < 5.5)\ + {\ + return vec4(0.02700000070035457611083984375, 0.72899997234344482421875, 0.3689999878406524658203125, 1.0);\ + }\ + else\ + {\ + return vec4(0.02700000070035457611083984375, 0.72899997234344482421875, 0.607999980449676513671875, 1.0);\ + }\ + }\ + }\ + }\ + }\ + }\ +}\ +\ +void main()\ +{\ + vec2 tex = floor(LIBRA_PUSH_FRAGMENT_INSTANCE.PassOutputSize1.xy * LIBRA_VARYING_0);\ + tex = (tex + vec2(0.5)) * LIBRA_PUSH_FRAGMENT_INSTANCE.PassOutputSize1.zw;\ + vec4 bg_color_cached = get_bg_color();\ + vec2 shadow_sample = LIBRA_VARYING_0 - (LIBRA_VARYING_3 + vec2(LIBRA_PUSH_FRAGMENT_INSTANCE.screen_offset_x * LIBRA_VARYING_1.x, LIBRA_PUSH_FRAGMENT_INSTANCE.screen_offset_y * LIBRA_VARYING_1.y));\ + shadow_sample = clamp(shadow_sample, vec2(0.0), vec2(1.0) - LIBRA_VARYING_1);\ + vec4 foreground = texture2D(LIBRA_TEXTURE_PassOutput1, tex - vec2(LIBRA_PUSH_FRAGMENT_INSTANCE.screen_offset_x * LIBRA_VARYING_1.x, LIBRA_PUSH_FRAGMENT_INSTANCE.screen_offset_y * LIBRA_VARYING_1.y));\ + vec4 background = texture2D(LIBRA_TEXTURE_BACKGROUND, LIBRA_VARYING_0);\ + vec4 shadows = texture2D(LIBRA_TEXTURE_Source, shadow_sample);\ + foreground *= bg_color_cached;\ + float bg_test = 0.0;\ + if (foreground.w > 0.0)\ + {\ + bg_test = 1.0;\ + }\ + background -= (((background - vec4(0.5)) * LIBRA_PUSH_FRAGMENT_INSTANCE.bg_smoothing) * bg_test);\ + float _201 = background.x;\ + float _207 = background.y;\ + float _214 = background.z;\ + vec3 _221 = clamp(vec3(bg_color_cached.x + mix(-1.0, 1.0, _201), bg_color_cached.y + mix(-1.0, 1.0, _207), bg_color_cached.z + mix(-1.0, 1.0, _214)), vec3(0.0), vec3(1.0));\ + background.x = _221.x;\ + background.y = _221.y;\ + background.z = _221.z;\ + vec4 out_color = ((shadows * shadows.w) * ((LIBRA_PUSH_FRAGMENT_INSTANCE.contrast * LIBRA_PUSH_FRAGMENT_INSTANCE.shadow_opacity) * LIBRA_PUSH_FRAGMENT_INSTANCE.shadow_enable)) + (background * (1.0 - (shadows.w * ((LIBRA_PUSH_FRAGMENT_INSTANCE.contrast * LIBRA_PUSH_FRAGMENT_INSTANCE.shadow_opacity) * LIBRA_PUSH_FRAGMENT_INSTANCE.shadow_enable))));\ + out_color = ((foreground * foreground.w) * LIBRA_PUSH_FRAGMENT_INSTANCE.contrast) + (out_color * (LIBRA_PUSH_FRAGMENT_INSTANCE.screen_light - ((foreground.w * LIBRA_PUSH_FRAGMENT_INSTANCE.contrast) * LIBRA_PUSH_FRAGMENT_INSTANCE.pixel_opacity)));\ + gl_FragData[0] = out_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="4",["mipmap_input"]=false,["parameters"]={{["description"]="=== GAME BOY DOT MATRIX SHADER v1.2 ===",["id"]="GAMEBOY_SHADER",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== LCD effects ==",["id"]="LCD_EFFECTS",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Turn OFF Integer Scale in Settings > Video > Scaling",["id"]="NOTE1",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * GBC: Turn OFF Core > Color Correction & Interframe Blending",["id"]="NOTE2",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * GBA: Turn ON Core > Color Correction & Interframe Blending",["id"]="NOTE3",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Soft / circular pixels = less grid distortion",["id"]="NOTE5",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Screen position ==",["id"]="SCREEN_POSITIONING",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Blend amount",["id"]="adjacent_texel_alpha_blending",["initial"]=0.1755,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Auto soften (reduce distortion)",["id"]="auto_soften",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Base pixel transparency",["id"]="baseline_alpha",["initial"]=0.1,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]=" โ†ณ Background texture smoothing",["id"]="bg_smoothing",["initial"]=0.75,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]="== Pixel blending mode == (0=Blend gaps, 1=Blend all)",["id"]="blending_mode",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Pixel transparency detection == (0=Simple, 1=Perceptual)",["id"]="brightness_mode",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== Color mode == (0=Grayscale, 1=Color)",["id"]="color_toggle",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Contrast",["id"]="contrast",["initial"]=0.95,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Grey balance",["id"]="grey_balance",["initial"]=3,["maximum"]=4,["minimum"]=0,["step"]=0.1,},{["description"]="== Display mode == (0=Full, 1=Pixel Perfect, 2=Scale factor)",["id"]="integer_mode",["initial"]=0,["maximum"]=2,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Palette (0=IMG, 1/2=Pocket, 3=B&W, 4=DMG, 5/6=Light)",["id"]="palette",["initial"]=0,["maximum"]=6,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Pixel opacity",["id"]="pixel_opacity",["initial"]=1,["maximum"]=1,["minimum"]=0.01,["step"]=0.01,},{["description"]=" โ†ณ Pixel shape [Sharp mode] (Circle/Rectangle)",["id"]="pixel_shape",["initial"]=1,["maximum"]=1.3,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Pixel size",["id"]="pixel_size",["initial"]=0.8,["maximum"]=1.1,["minimum"]=0.2,["step"]=0.05,},{["description"]=" โ†ณ Pixel softness",["id"]="pixel_softness",["initial"]=1,["maximum"]=5,["minimum"]=0.2,["step"]=0.05,},{["description"]=" โ†ณ Latency",["id"]="response_time",["initial"]=0,["maximum"]=0.777,["minimum"]=0,["step"]=0.111,},{["description"]=" โ†ณ Screen light",["id"]="screen_light",["initial"]=1,["maximum"]=2,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Screen offset horizontal",["id"]="screen_offset_x",["initial"]=0,["maximum"]=5,["minimum"]=-5,["step"]=1,},{["description"]=" โ†ณ Screen offset vertical",["id"]="screen_offset_y",["initial"]=0,["maximum"]=5,["minimum"]=-5,["step"]=1,},{["description"]="== Drop shadows == (OFF/ON)",["id"]="shadow_enable",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Max offset",["id"]="shadow_max_offset",["initial"]=2.5,["maximum"]=30,["minimum"]=0,["step"]=0.5,},{["description"]=" โ†ณ Shadow motion (OFF/ON)",["id"]="shadow_motion",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Shadow offset horizontal",["id"]="shadow_offset_x",["initial"]=1,["maximum"]=5,["minimum"]=-5,["step"]=0.5,},{["description"]=" โ†ณ Shadow offset vertical",["id"]="shadow_offset_y",["initial"]=1,["maximum"]=5,["minimum"]=-5,["step"]=0.5,},{["description"]=" โ†ณ Shadow opacity",["id"]="shadow_opacity",["initial"]=0.55,["maximum"]=1,["minimum"]=0.01,["step"]=0.01,},{["description"]="== Fullscreen pixels == (0=Soft, 1=Sharp)",["id"]="sharp_mode",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Pixel edge sharpness",["id"]="sharpening_amount",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]=" โ†ณ Scale factor",["id"]="video_scale",["initial"]=5,["maximum"]=15,["minimum"]=2,["step"]=1,},},["samplers"]={{["index"]=0,["name"]="COLOR_PALETTE",["semantic"]="User",["user_name"]="COLOR_PALETTE",},{["index"]=1,["name"]="PassOutput1",["semantic"]="PassOutput",},{["index"]=1,["name"]="BACKGROUND",["semantic"]="User",["user_name"]="BACKGROUND",},{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["size_uniforms"]={{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=1,["kind"]="texture",["name"]="PassOutputSize1",["semantic"]="PassOutput",},{["index"]=0,["kind"]="unique",["name"]="contrast",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="screen_light",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="pixel_opacity",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="bg_smoothing",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="shadow_opacity",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="shadow_offset_x",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="shadow_offset_y",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="screen_offset_x",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="screen_offset_y",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="shadow_enable",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="palette",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="integer_mode",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="shadow_motion",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="shadow_max_offset",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},{["index"]=0,["kind"]="unique",["name"]="Gyroscope",["semantic"]="Gyroscope",},{["index"]=0,["kind"]="unique",["name"]="Accelerometer",["semantic"]="Accelerometer",},{["index"]=0,["kind"]="unique",["name"]="AccelerometerRest",["semantic"]="AccelerometerRest",},},["srgb_framebuffer"]=false,["vertex"]="#version 120\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ + vec3 Gyroscope;\ + vec3 Accelerometer;\ + vec3 AccelerometerRest;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 OutputSize;\ + vec4 OriginalSize;\ + vec4 SourceSize;\ + vec4 PassOutputSize1;\ + float contrast;\ + float screen_light;\ + float pixel_opacity;\ + float bg_smoothing;\ + float shadow_opacity;\ + float shadow_offset_x;\ + float shadow_offset_y;\ + float screen_offset_x;\ + float screen_offset_y;\ + float shadow_enable;\ + float palette;\ + float integer_mode;\ + float shadow_motion;\ + float shadow_max_offset;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +varying vec2 LIBRA_VARYING_3;\ +varying vec2 LIBRA_VARYING_1;\ +float resolution_scale;\ +\ +vec2 getOrientedTilt(vec3 accel)\ +{\ + float magnitude = length(accel);\ + if (magnitude < 0.00999999977648258209228515625)\ + {\ + return vec2(0.0);\ + }\ + vec3 dir = accel / vec3(magnitude);\ + return vec2(-dir.x, dir.y);\ +}\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord * 1.00010001659393310546875;\ + float scale_x = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.x / 640.0;\ + float scale_y = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.y / 480.0;\ + resolution_scale = sqrt(scale_x * scale_y);\ + vec2 gravity = vec2(0.0);\ + if (LIBRA_PUSH_VERTEX_INSTANCE.shadow_motion > 0.5)\ + {\ + vec3 accel = LIBRA_UBO_VERTEX_INSTANCE.Accelerometer;\ + bool has_sensors = length(accel) > 0.00999999977648258209228515625;\ + if (has_sensors)\ + {\ + vec3 param = accel;\ + gravity = getOrientedTilt(param);\ + }\ + }\ + if (LIBRA_PUSH_VERTEX_INSTANCE.shadow_motion > 0.5)\ + {\ + vec2 base = vec2(LIBRA_PUSH_VERTEX_INSTANCE.shadow_offset_x, LIBRA_PUSH_VERTEX_INSTANCE.shadow_offset_y);\ + LIBRA_VARYING_3 = ((base + (gravity * LIBRA_PUSH_VERTEX_INSTANCE.shadow_max_offset)) * resolution_scale) * LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ + }\ + else\ + {\ + LIBRA_VARYING_3 = (vec2(LIBRA_PUSH_VERTEX_INSTANCE.shadow_offset_x, LIBRA_PUSH_VERTEX_INSTANCE.shadow_offset_y) * resolution_scale) * LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ + }\ + LIBRA_VARYING_1 = LIBRA_PUSH_VERTEX_INSTANCE.SourceSize.zw;\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},},["textures"]={{["filter_mode"]="nearest",["mipmap"]=false,["name"]="COLOR_PALETTE",["path"]="/Library/Application Support/LOVE/pokemon-love2d/shaders/handheld/shaders/gameboy/resources/palette.png",["wrap_mode"]="clamp_to_border",},{["filter_mode"]="linear",["mipmap"]=false,["name"]="BACKGROUND",["path"]="/Library/Application Support/LOVE/pokemon-love2d/shaders/handheld/shaders/gameboy/resources/background.png",["wrap_mode"]="clamp_to_border",},},} diff --git a/tests/data/shaderfx/gl/gb-palette-dmg.lua b/tests/data/shaderfx/gl/gb-palette-dmg.lua new file mode 100644 index 00000000..8905ebe2 --- /dev/null +++ b/tests/data/shaderfx/gl/gb-palette-dmg.lua @@ -0,0 +1,64 @@ +return {["parameter_overrides"]={},["pass_count"]=1,["passes"]={{["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 120\ +#extension GL_EXT_gpu_shader4 : require\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + uint FrameCount;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform sampler2D LIBRA_TEXTURE_Source;\ +uniform sampler2D LIBRA_TEXTURE_COLOR_PALETTE;\ +\ +varying vec2 LIBRA_VARYING_0;\ +\ +void main()\ +{\ + vec4 out_color = texture2D(LIBRA_TEXTURE_Source, LIBRA_VARYING_0);\ + vec2 palette_coordinate = vec2(0.5, (abs(1.0 - out_color.x) * 0.75) + 0.125);\ + out_color = vec4(texture2D(LIBRA_TEXTURE_COLOR_PALETTE, palette_coordinate).xyz, ceil(abs(1.0 - out_color.x)));\ + gl_FragData[0] = out_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="0",["mipmap_input"]=false,["parameters"]={},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},{["index"]=0,["name"]="COLOR_PALETTE",["semantic"]="User",["user_name"]="COLOR_PALETTE",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="source",},["size_uniforms"]={{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="unique",["name"]="FrameCount",["semantic"]="FrameCount",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 120\ +#extension GL_EXT_gpu_shader4 : require\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + uint FrameCount;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord * 1.00010001659393310546875;\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},},["textures"]={{["filter_mode"]="nearest",["mipmap"]=false,["name"]="COLOR_PALETTE",["path"]="/Library/Application Support/LOVE/pokemon-love2d/shaders/handheld/shaders/gb-palette/resources/palette-dmg.png",["wrap_mode"]="clamp_to_border",},},} diff --git a/tests/data/shaderfx/gl/lcd3x.lua b/tests/data/shaderfx/gl/lcd3x.lua new file mode 100644 index 00000000..fbc4365b --- /dev/null +++ b/tests/data/shaderfx/gl/lcd3x.lua @@ -0,0 +1,73 @@ +return {["parameter_overrides"]={},["pass_count"]=1,["passes"]={{["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 120\ +#extension GL_EXT_gpu_shader4 : require\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + uint FrameCount;\ + float brighten_scanlines;\ + float brighten_lcd;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +vec2 omega;\ +\ +void main()\ +{\ + omega = vec2(6.283185482025146484375) * LIBRA_PUSH_FRAGMENT_INSTANCE.OriginalSize.xy;\ + vec3 res = texture2D(LIBRA_TEXTURE_Source, LIBRA_VARYING_0).xyz;\ + vec2 angle = LIBRA_VARYING_0 * omega;\ + float yfactor = (LIBRA_PUSH_FRAGMENT_INSTANCE.brighten_scanlines + sin(angle.y)) / (LIBRA_PUSH_FRAGMENT_INSTANCE.brighten_scanlines + 1.0);\ + vec3 xfactors = (vec3(LIBRA_PUSH_FRAGMENT_INSTANCE.brighten_lcd) + sin(vec3(angle.x) + vec3(1.57079637050628662109375, -0.52359879016876220703125, -2.617993831634521484375))) / vec3(LIBRA_PUSH_FRAGMENT_INSTANCE.brighten_lcd + 1.0);\ + vec3 color = (xfactors * yfactor) * res;\ + gl_FragData[0] = vec4(color.x, color.y, color.z, 1.0);\ +}\ +\ +",["frame_count_mod"]=0,["id"]="0",["mipmap_input"]=false,["parameters"]={{["description"]="Brighten LCD",["id"]="brighten_lcd",["initial"]=4,["maximum"]=12,["minimum"]=1,["step"]=0.1,},{["description"]="Brighten Scanlines",["id"]="brighten_scanlines",["initial"]=16,["maximum"]=32,["minimum"]=1,["step"]=0.5,},},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["size_uniforms"]={{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="unique",["name"]="FrameCount",["semantic"]="FrameCount",},{["index"]=0,["kind"]="unique",["name"]="brighten_scanlines",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="brighten_lcd",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 120\ +#extension GL_EXT_gpu_shader4 : require\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + uint FrameCount;\ + float brighten_scanlines;\ + float brighten_lcd;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +vec2 omega;\ +\ +void main()\ +{\ + omega = vec2(6.283185482025146484375) * LIBRA_PUSH_VERTEX_INSTANCE.OriginalSize.xy;\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord;\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},},["textures"]={},} diff --git a/tests/data/shaderfx/gl/pixel_transparency.lua b/tests/data/shaderfx/gl/pixel_transparency.lua new file mode 100644 index 00000000..d5f8a80a --- /dev/null +++ b/tests/data/shaderfx/gl/pixel_transparency.lua @@ -0,0 +1,501 @@ +return {["parameter_overrides"]={{["name"]="PT_PALETTE",["value"]=1,},{["name"]="PT_POLARIZER",["value"]=0,},{["name"]="PT_HIGHLIGHTS",["value"]=0,},},["pass_count"]=1,["passes"]={{["filter"]="nearest",["float_framebuffer"]=false,["fragment"]="#version 120\ +\ +const vec2 _356[5] = vec2[](vec2(0.0), vec2(-1.0, 0.0), vec2(1.0, 0.0), vec2(0.0, -1.0), vec2(0.0, 1.0));\ +const vec2 _414[9] = vec2[](vec2(0.0), vec2(-1.0, 0.0), vec2(1.0, 0.0), vec2(0.0, -1.0), vec2(0.0, 1.0), vec2(-1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0));\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ + vec3 Gyroscope;\ + vec3 Accelerometer;\ + vec3 AccelerometerRest;\ + float PT_HIDE_CONTENT;\ + float PT_PALETTE;\ + float PT_PALETTE_INTENSITY;\ + float PT_BACKING_BRIGHTNESS;\ + float PT_POLARIZER;\ + float PT_POLARIZER_TINT;\ + float PT_SATURATION;\ + float PT_HIGHLIGHTS;\ + float PT_WHITE_BOOST;\ + float PT_WHITE_TRANSPARENCY;\ + float PT_BRIGHTNESS_GRID;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 SourceSize;\ + vec4 OutputSize;\ + float PT_ACCEL_ENABLE;\ + float PT_ACCEL_SENSITIVITY;\ + float PT_SHADOW_MOTION;\ + float PT_SHADOW_MAX_OFFSET;\ + float PT_SHADOW_MOTION_X;\ + float PT_SHADOW_MOTION_Y;\ + float PT_SHADOW_OFFSET_X;\ + float PT_SHADOW_OFFSET_Y;\ + float PT_ENABLE;\ + float PT_BRIGHTNESS_MODE;\ + float PT_SHADOW_ENABLE;\ + float PT_SHADOW_BLUR;\ + float PT_SHADOW_OPACITY;\ + float PT_THRESHOLD;\ + float PT_SHADOW_FAST_BLUR;\ + float PT_PIXEL_MODE;\ + float PT_BASE_ALPHA;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform sampler2D LIBRA_TEXTURE_Source;\ +uniform sampler2D LIBRA_TEXTURE_Original;\ +\ +varying vec2 LIBRA_VARYING_0;\ +varying vec2 LIBRA_VARYING_3;\ +varying float LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_1;\ +\ +float getPerceptualBrightness(vec3 color)\ +{\ + return ((0.2125999927520751953125 * color.x) + (0.715200006961822509765625 * color.y)) + (0.072200000286102294921875 * color.z);\ +}\ +\ +bool isWhitePixel(vec3 color, float threshold)\ +{\ + vec3 param = color;\ + float brightness = getPerceptualBrightness(param);\ + float min_channel = min(min(color.x, color.y), color.z);\ + bool _111 = brightness > threshold;\ + bool _119;\ + if (_111)\ + {\ + _119 = min_channel > (threshold * 0.89999997615814208984375);\ + }\ + else\ + {\ + _119 = _111;\ + }\ + return _119;\ +}\ +\ +float hash(vec2 p)\ +{\ + vec3 p3 = fract(p.xyx * 0.103100001811981201171875);\ + p3 += vec3(dot(p3, p3.yzx + vec3(33.3300018310546875)));\ + return fract((p3.x + p3.y) * p3.z);\ +}\ +\ +float paperNoise(vec2 uv, float scale)\ +{\ + vec2 p = (uv * scale) * 512.0;\ + float n = 0.0;\ + float amplitude = 0.5;\ + float frequency = 1.0;\ + for (int i = 0; i < 3; i++)\ + {\ + vec2 param = p * frequency;\ + n += (hash(param) * amplitude);\ + amplitude *= 0.5;\ + frequency *= 2.0;\ + }\ + return n;\ +}\ +\ +vec3 generateProceduralBackground(vec2 uv)\ +{\ + vec3 baseColor = vec3(LIBRA_UBO_FRAGMENT_INSTANCE.PT_BACKING_BRIGHTNESS);\ + vec2 param = uv;\ + float param_1 = 0.25;\ + float grain = paperNoise(param, param_1);\ + float grainOffset = (grain - 0.4375) * 0.064999997615814208984375;\ + return baseColor + vec3(grainOffset);\ +}\ +\ +float getGameBoyRGBSum(vec3 color)\ +{\ + return ((color.x + color.y) + color.z) / 3.0;\ +}\ +\ +float getBrightness(vec3 color)\ +{\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_BRIGHTNESS_MODE < 0.5)\ + {\ + vec3 param = color;\ + return getGameBoyRGBSum(param);\ + }\ + else\ + {\ + vec3 param_1 = color;\ + return getPerceptualBrightness(param_1);\ + }\ +}\ +\ +void main()\ +{\ + vec4 lcd_color = texture2D(LIBRA_TEXTURE_Source, LIBRA_VARYING_0);\ + vec4 output_color = lcd_color;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_ENABLE < 0.5)\ + {\ + gl_FragData[0] = output_color;\ + return;\ + }\ + vec3 original_pixel = texture2D(LIBRA_TEXTURE_Original, LIBRA_VARYING_0).xyz;\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_HIDE_CONTENT > 0.5)\ + {\ + lcd_color = vec4(1.0);\ + original_pixel = vec3(1.0);\ + output_color = lcd_color;\ + }\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_HIGHLIGHTS < (-0.001000000047497451305389404296875))\ + {\ + vec3 param = lcd_color.xyz;\ + float luma = getPerceptualBrightness(param);\ + vec4 _274 = lcd_color;\ + vec3 _276 = _274.xyz * max(1.0 + (LIBRA_UBO_FRAGMENT_INSTANCE.PT_HIGHLIGHTS * luma), 0.0);\ + lcd_color.x = _276.x;\ + lcd_color.y = _276.y;\ + lcd_color.z = _276.z;\ + }\ + vec3 param_1 = original_pixel;\ + float param_2 = LIBRA_PUSH_FRAGMENT_INSTANCE.PT_THRESHOLD;\ + bool current_is_white = isWhitePixel(param_1, param_2);\ + vec2 param_3 = LIBRA_VARYING_0;\ + vec4 background = vec4(generateProceduralBackground(param_3), 1.0);\ + bool _303 = LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_ENABLE > 0.5;\ + bool _310;\ + if (_303)\ + {\ + _310 = LIBRA_UBO_FRAGMENT_INSTANCE.PT_HIDE_CONTENT < 1.5;\ + }\ + else\ + {\ + _310 = _303;\ + }\ + if (_310)\ + {\ + vec2 shadow_offset = ((-LIBRA_VARYING_3) * LIBRA_VARYING_2) * LIBRA_VARYING_1;\ + float shadow_strength = 0.0;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_BLUR > 0.100000001490116119384765625)\ + {\ + vec2 blur_dist = LIBRA_VARYING_1 * (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_BLUR * LIBRA_VARYING_2);\ + float blurred_shadow = 0.0;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_FAST_BLUR < 0.5)\ + {\ + for (int i = 0; i < 5; i++)\ + {\ + vec2 sample_pos = (LIBRA_VARYING_0 + shadow_offset) + (_356[i] * blur_dist);\ + vec3 blur_sample = texture2D(LIBRA_TEXTURE_Original, sample_pos).xyz;\ + float w = (i == 0) ? 4.0 : 2.0;\ + vec3 param_4 = blur_sample;\ + float blur_brightness = getBrightness(param_4);\ + blurred_shadow += ((1.0 - blur_brightness) * w);\ + }\ + shadow_strength = (blurred_shadow / 12.0) * LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_OPACITY;\ + }\ + else\ + {\ + float _442;\ + for (int i_1 = 0; i_1 < 9; i_1++)\ + {\ + vec2 sample_pos_1 = (LIBRA_VARYING_0 + shadow_offset) + (_414[i_1] * blur_dist);\ + vec3 blur_sample_1 = texture2D(LIBRA_TEXTURE_Original, sample_pos_1).xyz;\ + if (i_1 == 0)\ + {\ + _442 = 4.0;\ + }\ + else\ + {\ + _442 = (i_1 < 5) ? 2.0 : 1.0;\ + }\ + float w_1 = _442;\ + vec3 param_5 = blur_sample_1;\ + float blur_brightness_1 = getBrightness(param_5);\ + blurred_shadow += ((1.0 - blur_brightness_1) * w_1);\ + }\ + shadow_strength = (blurred_shadow / 16.0) * LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_OPACITY;\ + }\ + }\ + else\ + {\ + vec3 center_source = texture2D(LIBRA_TEXTURE_Original, LIBRA_VARYING_0 + shadow_offset).xyz;\ + vec3 param_6 = center_source;\ + float shadow_source_brightness = getBrightness(param_6);\ + shadow_strength = (1.0 - shadow_source_brightness) * LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_OPACITY;\ + }\ + float deadzone = (1.0 - LIBRA_PUSH_FRAGMENT_INSTANCE.PT_THRESHOLD) * LIBRA_PUSH_FRAGMENT_INSTANCE.PT_SHADOW_OPACITY;\ + if (deadzone > 0.001000000047497451305389404296875)\ + {\ + shadow_strength = smoothstep(0.0, deadzone, shadow_strength) * shadow_strength;\ + }\ + vec4 _502 = background;\ + vec4 _504 = background;\ + vec3 _510 = mix(_502.xyz, _504.xyz * 0.20000000298023223876953125, vec3(shadow_strength));\ + background.x = _510.x;\ + background.y = _510.y;\ + background.z = _510.z;\ + }\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_PALETTE > 0.5)\ + {\ + vec3 bg_palette_color;\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_PALETTE < 1.5)\ + {\ + bg_palette_color = vec3(0.65100002288818359375, 0.675000011920928955078125, 0.51800000667572021484375);\ + }\ + else\ + {\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_PALETTE < 2.5)\ + {\ + bg_palette_color = vec3(0.7200000286102294921875, 0.730000019073486328125, 0.660000026226043701171875);\ + }\ + else\ + {\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_PALETTE < 3.5)\ + {\ + bg_palette_color = vec3(0.765999972820281982421875, 0.730000019073486328125, 0.763000011444091796875);\ + }\ + else\ + {\ + bg_palette_color = vec3(0.7599999904632568359375);\ + }\ + }\ + }\ + float max_ch = max(bg_palette_color.x, max(bg_palette_color.y, bg_palette_color.z));\ + bg_palette_color *= (0.675000011920928955078125 / max_ch);\ + vec3 tinted_background = clamp(vec3(bg_palette_color.x + mix(-1.0, 1.0, background.x), bg_palette_color.y + mix(-1.0, 1.0, background.y), bg_palette_color.z + mix(-1.0, 1.0, background.z)), vec3(0.0), vec3(1.0));\ + vec4 _592 = background;\ + vec3 _599 = mix(_592.xyz, tinted_background, vec3(LIBRA_UBO_FRAGMENT_INSTANCE.PT_PALETTE_INTENSITY));\ + background.x = _599.x;\ + background.y = _599.y;\ + background.z = _599.z;\ + }\ + vec3 brightness_source = original_pixel;\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_BRIGHTNESS_GRID > 0.001000000047497451305389404296875)\ + {\ + brightness_source = lcd_color.xyz;\ + if (current_is_white)\ + {\ + float white_blend = smoothstep(0.0500000007450580596923828125, 1.0, LIBRA_UBO_FRAGMENT_INSTANCE.PT_BRIGHTNESS_GRID);\ + brightness_source = mix(original_pixel, lcd_color.xyz, vec3(white_blend));\ + }\ + }\ + bool _632 = LIBRA_PUSH_FRAGMENT_INSTANCE.PT_PIXEL_MODE > 0.5;\ + bool _638;\ + if (_632)\ + {\ + _638 = LIBRA_PUSH_FRAGMENT_INSTANCE.PT_PIXEL_MODE < 1.5;\ + }\ + else\ + {\ + _638 = _632;\ + }\ + if (_638)\ + {\ + vec3 param_7 = brightness_source;\ + float pixel_intensity = getBrightness(param_7);\ + float transparency = (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_BASE_ALPHA * pixel_intensity) * 2.66499996185302734375;\ + bool _659;\ + if (current_is_white)\ + {\ + _659 = LIBRA_UBO_FRAGMENT_INSTANCE.PT_WHITE_BOOST > 0.5;\ + }\ + else\ + {\ + _659 = current_is_white;\ + }\ + if (_659)\ + {\ + transparency = max(transparency, LIBRA_UBO_FRAGMENT_INSTANCE.PT_WHITE_TRANSPARENCY);\ + }\ + transparency = clamp(transparency, 0.0, 1.0);\ + vec3 _674 = mix(lcd_color.xyz, background.xyz, vec3(transparency));\ + output_color.x = _674.x;\ + output_color.y = _674.y;\ + output_color.z = _674.z;\ + }\ + else\ + {\ + bool should_apply = (LIBRA_PUSH_FRAGMENT_INSTANCE.PT_PIXEL_MODE < 0.5) ? current_is_white : true;\ + if (should_apply)\ + {\ + vec3 param_8 = brightness_source;\ + float pixel_intensity_1 = getBrightness(param_8);\ + float pixel_alpha = (pixel_intensity_1 / 3.0) + LIBRA_PUSH_FRAGMENT_INSTANCE.PT_BASE_ALPHA;\ + bool _708;\ + if (current_is_white)\ + {\ + _708 = LIBRA_UBO_FRAGMENT_INSTANCE.PT_WHITE_BOOST > 0.5;\ + }\ + else\ + {\ + _708 = current_is_white;\ + }\ + if (_708)\ + {\ + pixel_alpha = max(pixel_alpha, LIBRA_UBO_FRAGMENT_INSTANCE.PT_WHITE_TRANSPARENCY);\ + }\ + pixel_alpha = clamp(pixel_alpha, 0.0, 1.0);\ + vec3 _723 = mix(lcd_color.xyz, background.xyz, vec3(pixel_alpha));\ + output_color.x = _723.x;\ + output_color.y = _723.y;\ + output_color.z = _723.z;\ + }\ + }\ + bool _733 = LIBRA_UBO_FRAGMENT_INSTANCE.PT_POLARIZER > 0.5;\ + bool _739;\ + if (_733)\ + {\ + _739 = LIBRA_UBO_FRAGMENT_INSTANCE.PT_POLARIZER_TINT > 0.001000000047497451305389404296875;\ + }\ + else\ + {\ + _739 = _733;\ + }\ + if (_739)\ + {\ + vec3 polarizer = mix(vec3(1.0), vec3(0.939999997615814208984375, 1.0, 0.8650000095367431640625), vec3(LIBRA_UBO_FRAGMENT_INSTANCE.PT_POLARIZER_TINT));\ + vec4 _751 = output_color;\ + vec3 _753 = _751.xyz * polarizer;\ + output_color.x = _753.x;\ + output_color.y = _753.y;\ + output_color.z = _753.z;\ + }\ + if (LIBRA_UBO_FRAGMENT_INSTANCE.PT_HIGHLIGHTS > 0.001000000047497451305389404296875)\ + {\ + vec3 param_9 = output_color.xyz;\ + float luma_1 = getPerceptualBrightness(param_9);\ + vec4 _770 = output_color;\ + vec3 _780 = clamp(_770.xyz * (1.0 + (LIBRA_UBO_FRAGMENT_INSTANCE.PT_HIGHLIGHTS * luma_1)), vec3(0.0), vec3(1.0));\ + output_color.x = _780.x;\ + output_color.y = _780.y;\ + output_color.z = _780.z;\ + }\ + if (abs(LIBRA_UBO_FRAGMENT_INSTANCE.PT_SATURATION - 1.0) > 0.001000000047497451305389404296875)\ + {\ + vec3 param_10 = output_color.xyz;\ + float luma_2 = getPerceptualBrightness(param_10);\ + vec4 _801 = output_color;\ + vec3 _809 = clamp(mix(vec3(luma_2), _801.xyz, vec3(LIBRA_UBO_FRAGMENT_INSTANCE.PT_SATURATION)), vec3(0.0), vec3(1.0));\ + output_color.x = _809.x;\ + output_color.y = _809.y;\ + output_color.z = _809.z;\ + }\ + vec2 param_11 = gl_FragCoord.xy;\ + vec4 _825 = output_color;\ + vec3 _828 = _825.xyz + vec3((hash(param_11) - 0.5) / 255.0);\ + output_color.x = _828.x;\ + output_color.y = _828.y;\ + output_color.z = _828.z;\ + gl_FragData[0] = output_color;\ +}\ +\ +",["frame_count_mod"]=0,["id"]="0",["mipmap_input"]=false,["parameters"]={{["description"]=" * Turn ON Core > Color Correction & Interframe Blending",["id"]="NOTE2",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Motion enabled on latest RetroArch nightly build.",["id"]="NOTE3",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== MOTION == (OFF/ON)",["id"]="PT_ACCEL_ENABLE",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Tilt sensitivity",["id"]="PT_ACCEL_SENSITIVITY",["initial"]=1,["maximum"]=5,["minimum"]=0.1,["step"]=0.05,},{["description"]=" โ†ณ Brightness",["id"]="PT_BACKING_BRIGHTNESS",["initial"]=0.48,["maximum"]=0.65,["minimum"]=0.2,["step"]=0.01,},{["description"]=" โ†ณ Transparency amount",["id"]="PT_BASE_ALPHA",["initial"]=0.2,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]="--- Gridline intensity (0=Less, 1=More)",["id"]="PT_BRIGHTNESS_GRID",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]="--- Brightness mode (0=Simple, 1=Natural)",["id"]="PT_BRIGHTNESS_MODE",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="=== Pixel Transparency v2.2 === (OFF/ON)",["id"]="PT_ENABLE",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" * Append to any LCD shader as last pass. Try with GBC & GBA.",["id"]="PT_HIDE_CONTENT",["initial"]=0,["maximum"]=2,["minimum"]=0,["step"]=1,},{["description"]="--- Pixel brightness (0=OFF, -Dim / +Bright)",["id"]="PT_HIGHLIGHTS",["initial"]=0.05,["maximum"]=1,["minimum"]=-1,["step"]=0.01,},{["description"]="--- Backing tint (0=OFF, 1/2=Warm, 3/4=Neutral)",["id"]="PT_PALETTE",["initial"]=3,["maximum"]=4,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Intensity",["id"]="PT_PALETTE_INTENSITY",["initial"]=1,["maximum"]=2,["minimum"]=0,["step"]=0.05,},{["description"]="--- Transparent pixels (0=White only, 1=Bright, 2=All)",["id"]="PT_PIXEL_MODE",["initial"]=1,["maximum"]=2,["minimum"]=0,["step"]=1,},{["description"]="--- Front polarizer tint (OFF/ON)",["id"]="PT_POLARIZER",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Intensity",["id"]="PT_POLARIZER_TINT",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=0.05,},{["description"]="--- Color saturation",["id"]="PT_SATURATION",["initial"]=1,["maximum"]=1.5,["minimum"]=0.5,["step"]=0.05,},{["description"]=" โ†ณ Blur amount",["id"]="PT_SHADOW_BLUR",["initial"]=1,["maximum"]=5,["minimum"]=0,["step"]=0.1,},{["description"]="== SHADOWS == (OFF/ON)",["id"]="PT_SHADOW_ENABLE",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Shadow quality (0=Low, 1=High)",["id"]="PT_SHADOW_FAST_BLUR",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Max offset",["id"]="PT_SHADOW_MAX_OFFSET",["initial"]=2.5,["maximum"]=30,["minimum"]=0,["step"]=0.5,},{["description"]=" โ†ณ Shadow motion (OFF/ON) ",["id"]="PT_SHADOW_MOTION",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Neutral X",["id"]="PT_SHADOW_MOTION_X",["initial"]=2,["maximum"]=30,["minimum"]=-30,["step"]=0.5,},{["description"]=" โ†ณ Neutral Y",["id"]="PT_SHADOW_MOTION_Y",["initial"]=2,["maximum"]=30,["minimum"]=-30,["step"]=0.5,},{["description"]=" โ†ณ X offset",["id"]="PT_SHADOW_OFFSET_X",["initial"]=3,["maximum"]=30,["minimum"]=-30,["step"]=0.5,},{["description"]=" โ†ณ Y offset",["id"]="PT_SHADOW_OFFSET_Y",["initial"]=3,["maximum"]=30,["minimum"]=-30,["step"]=0.5,},{["description"]=" โ†ณ Opacity",["id"]="PT_SHADOW_OPACITY",["initial"]=0.5,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]="--- White pixel detection threshold",["id"]="PT_THRESHOLD",["initial"]=0.9,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]="--- Boost white pixel transparency (OFF/ON)",["id"]="PT_WHITE_BOOST",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]=" โ†ณ Boost amount",["id"]="PT_WHITE_TRANSPARENCY",["initial"]=0.5,["maximum"]=1,["minimum"]=0,["step"]=0.01,},{["description"]="== COLOR ==",["id"]="SECTION1",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== TRANSPARENCY ==",["id"]="SECTION2",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},{["description"]="== PERFORMANCE ==",["id"]="SECTION_PERF",["initial"]=0,["maximum"]=1,["minimum"]=0,["step"]=1,},},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},{["index"]=0,["name"]="Original",["semantic"]="Original",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["size_uniforms"]={{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="unique",["name"]="PT_ACCEL_ENABLE",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_ACCEL_SENSITIVITY",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_MOTION",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_MAX_OFFSET",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_MOTION_X",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_MOTION_Y",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_OFFSET_X",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_OFFSET_Y",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_ENABLE",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_BRIGHTNESS_MODE",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_ENABLE",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_BLUR",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_OPACITY",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_THRESHOLD",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SHADOW_FAST_BLUR",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_PIXEL_MODE",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_BASE_ALPHA",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},{["index"]=0,["kind"]="unique",["name"]="Gyroscope",["semantic"]="Gyroscope",},{["index"]=0,["kind"]="unique",["name"]="Accelerometer",["semantic"]="Accelerometer",},{["index"]=0,["kind"]="unique",["name"]="AccelerometerRest",["semantic"]="AccelerometerRest",},{["index"]=0,["kind"]="unique",["name"]="PT_HIDE_CONTENT",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_PALETTE",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_PALETTE_INTENSITY",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_BACKING_BRIGHTNESS",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_POLARIZER",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_POLARIZER_TINT",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_SATURATION",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_HIGHLIGHTS",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_WHITE_BOOST",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_WHITE_TRANSPARENCY",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="PT_BRIGHTNESS_GRID",["semantic"]="FloatParameter",},},["srgb_framebuffer"]=false,["vertex"]="#version 120\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ + vec3 Gyroscope;\ + vec3 Accelerometer;\ + vec3 AccelerometerRest;\ + float PT_HIDE_CONTENT;\ + float PT_PALETTE;\ + float PT_PALETTE_INTENSITY;\ + float PT_BACKING_BRIGHTNESS;\ + float PT_POLARIZER;\ + float PT_POLARIZER_TINT;\ + float PT_SATURATION;\ + float PT_HIGHLIGHTS;\ + float PT_WHITE_BOOST;\ + float PT_WHITE_TRANSPARENCY;\ + float PT_BRIGHTNESS_GRID;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 SourceSize;\ + vec4 OutputSize;\ + float PT_ACCEL_ENABLE;\ + float PT_ACCEL_SENSITIVITY;\ + float PT_SHADOW_MOTION;\ + float PT_SHADOW_MAX_OFFSET;\ + float PT_SHADOW_MOTION_X;\ + float PT_SHADOW_MOTION_Y;\ + float PT_SHADOW_OFFSET_X;\ + float PT_SHADOW_OFFSET_Y;\ + float PT_ENABLE;\ + float PT_BRIGHTNESS_MODE;\ + float PT_SHADOW_ENABLE;\ + float PT_SHADOW_BLUR;\ + float PT_SHADOW_OPACITY;\ + float PT_THRESHOLD;\ + float PT_SHADOW_FAST_BLUR;\ + float PT_PIXEL_MODE;\ + float PT_BASE_ALPHA;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +varying vec2 LIBRA_VARYING_1;\ +varying float LIBRA_VARYING_2;\ +varying vec2 LIBRA_VARYING_3;\ +\ +vec2 getOrientedTilt(vec3 accel)\ +{\ + float magnitude = length(accel);\ + if (magnitude < 0.00999999977648258209228515625)\ + {\ + return vec2(0.0);\ + }\ + vec3 dir = accel / vec3(magnitude);\ + return vec2(-dir.x, dir.y);\ +}\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord;\ + LIBRA_VARYING_1 = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.zw;\ + float scale_x = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.x / 640.0;\ + float scale_y = LIBRA_PUSH_VERTEX_INSTANCE.OutputSize.y / 480.0;\ + LIBRA_VARYING_2 = sqrt(scale_x * scale_y);\ + vec2 gravity = vec2(0.0);\ + bool has_sensors = false;\ + if (LIBRA_PUSH_VERTEX_INSTANCE.PT_ACCEL_ENABLE > 0.5)\ + {\ + vec3 accel = LIBRA_UBO_VERTEX_INSTANCE.Accelerometer;\ + has_sensors = length(accel) > 0.00999999977648258209228515625;\ + if (has_sensors)\ + {\ + vec3 param = accel;\ + gravity = getOrientedTilt(param);\ + gravity = clamp(gravity * LIBRA_PUSH_VERTEX_INSTANCE.PT_ACCEL_SENSITIVITY, vec2(-1.0), vec2(1.0));\ + }\ + }\ + bool _132 = LIBRA_PUSH_VERTEX_INSTANCE.PT_SHADOW_MOTION > 0.5;\ + bool _138;\ + if (_132)\ + {\ + _138 = LIBRA_PUSH_VERTEX_INSTANCE.PT_ACCEL_ENABLE > 0.5;\ + }\ + else\ + {\ + _138 = _132;\ + }\ + if (_138 && has_sensors)\ + {\ + vec2 base = vec2(LIBRA_PUSH_VERTEX_INSTANCE.PT_SHADOW_MOTION_X, LIBRA_PUSH_VERTEX_INSTANCE.PT_SHADOW_MOTION_Y);\ + LIBRA_VARYING_3 = base + (gravity * LIBRA_PUSH_VERTEX_INSTANCE.PT_SHADOW_MAX_OFFSET);\ + }\ + else\ + {\ + LIBRA_VARYING_3 = vec2(LIBRA_PUSH_VERTEX_INSTANCE.PT_SHADOW_OFFSET_X, LIBRA_PUSH_VERTEX_INSTANCE.PT_SHADOW_OFFSET_Y);\ + }\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},},["textures"]={},} diff --git a/tests/data/shaderfx/gl/zfast-lcd.lua b/tests/data/shaderfx/gl/zfast-lcd.lua new file mode 100644 index 00000000..8fab703e --- /dev/null +++ b/tests/data/shaderfx/gl/zfast-lcd.lua @@ -0,0 +1,78 @@ +return {["parameter_overrides"]={},["pass_count"]=1,["passes"]={{["filter"]="linear",["float_framebuffer"]=false,["fragment"]="#version 120\ +#extension GL_EXT_gpu_shader4 : require\ +\ +struct LIBRA_UBO_FRAGMENT\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_FRAGMENT LIBRA_UBO_FRAGMENT_INSTANCE;\ +\ +struct LIBRA_PUSH_FRAGMENT\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + uint FrameCount;\ + float BORDERMULT;\ + float GBAGAMMA;\ +};\ +\ +uniform LIBRA_PUSH_FRAGMENT LIBRA_PUSH_FRAGMENT_INSTANCE;\ +\ +uniform sampler2D LIBRA_TEXTURE_Source;\ +\ +varying vec2 LIBRA_VARYING_0;\ +\ +void main()\ +{\ + vec2 texcoordInPixels = LIBRA_VARYING_0 * LIBRA_PUSH_FRAGMENT_INSTANCE.SourceSize.xy;\ + vec2 centerCoord = floor(texcoordInPixels) + vec2(0.5);\ + vec2 distFromCenter = abs(centerCoord - texcoordInPixels);\ + float Y = max(distFromCenter.x, distFromCenter.y);\ + Y *= Y;\ + float YY = Y * Y;\ + float YYY = YY * Y;\ + float LineWeight = YY - (2.7000000476837158203125 * YYY);\ + LineWeight = 1.0 - (LIBRA_PUSH_FRAGMENT_INSTANCE.BORDERMULT * LineWeight);\ + vec3 colour = texture2D(LIBRA_TEXTURE_Source, LIBRA_PUSH_FRAGMENT_INSTANCE.SourceSize.zw * centerCoord).xyz * LineWeight;\ + if (LIBRA_PUSH_FRAGMENT_INSTANCE.GBAGAMMA > 0.5)\ + {\ + colour *= (vec3(0.60000002384185791015625) + (colour * 0.4000000059604644775390625));\ + }\ + gl_FragData[0] = vec4(colour, 1.0);\ +}\ +\ +",["frame_count_mod"]=0,["id"]="0",["mipmap_input"]=false,["parameters"]={{["description"]="Border Multiplier",["id"]="BORDERMULT",["initial"]=14,["maximum"]=40,["minimum"]=-40,["step"]=1,},{["description"]="GBA Gamma Hack",["id"]="GBAGAMMA",["initial"]=1,["maximum"]=1,["minimum"]=0,["step"]=1,},},["samplers"]={{["index"]=0,["name"]="Source",["semantic"]="Source",},},["scale_x"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["scale_y"]={["factor"]=1,["factor_kind"]="float",["scale_type"]="viewport",},["size_uniforms"]={{["index"]=0,["kind"]="texture",["name"]="SourceSize",["semantic"]="Source",},{["index"]=0,["kind"]="texture",["name"]="OriginalSize",["semantic"]="Original",},{["index"]=0,["kind"]="unique",["name"]="OutputSize",["semantic"]="Output",},{["index"]=0,["kind"]="unique",["name"]="FrameCount",["semantic"]="FrameCount",},{["index"]=0,["kind"]="unique",["name"]="BORDERMULT",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="GBAGAMMA",["semantic"]="FloatParameter",},{["index"]=0,["kind"]="unique",["name"]="MVP",["semantic"]="MVP",},},["srgb_framebuffer"]=false,["vertex"]="#version 120\ +#extension GL_EXT_gpu_shader4 : require\ +\ +struct LIBRA_UBO_VERTEX\ +{\ + mat4 MVP;\ +};\ +\ +uniform LIBRA_UBO_VERTEX LIBRA_UBO_VERTEX_INSTANCE;\ +\ +struct LIBRA_PUSH_VERTEX\ +{\ + vec4 SourceSize;\ + vec4 OriginalSize;\ + vec4 OutputSize;\ + uint FrameCount;\ + float BORDERMULT;\ + float GBAGAMMA;\ +};\ +\ +uniform LIBRA_PUSH_VERTEX LIBRA_PUSH_VERTEX_INSTANCE;\ +\ +attribute vec4 Position;\ +varying vec2 LIBRA_VARYING_0;\ +attribute vec2 TexCoord;\ +\ +void main()\ +{\ + gl_Position = LIBRA_UBO_VERTEX_INSTANCE.MVP * Position;\ + LIBRA_VARYING_0 = TexCoord;\ +}\ +\ +",["wrap_mode"]="clamp_to_border",},},["textures"]={},} diff --git a/tests/drivers/battle_substitute_bug2030_test.lua b/tests/drivers/battle_substitute_bug2030_test.lua new file mode 100644 index 00000000..9c91e577 --- /dev/null +++ b/tests/drivers/battle_substitute_bug2030_test.lua @@ -0,0 +1,101 @@ +-- ../pokered/engine/battle/move_effects/substitute.asm:2-3, :47-55 +-- ../pokered/engine/battle/animations.asm:1936-1973 +-- POKEPORT_DRIVER=tests/drivers/battle_substitute_bug2030_test.lua \ +-- POKEPORT_IDENTITY=red-aug28 POKEPORT_VERSION=red POKEPORT_TOUCH=0 \ +-- SHOT_DIR=/tmp/shots love . +return function(game) + local U = dofile("tests/drivers/util.lua") + local DIR = os.getenv("SHOT_DIR") or "/tmp/shots" + local animsOff = os.getenv("BUG2030_ANIMS_OFF") == "1" + local Pokemon = require("src.pokemon.Pokemon") + local BattleState = require("src.battle.BattleState") + + local tag = animsOff and "animoff" or "animon" + local function check(label, ok) + U.log(ok and "PASS" or "FAIL", label) + return ok + end + + game.save.options = game.save.options or {} + game.save.options.animations = not animsOff + U.log("SUBSTITUTE doll timing, animations", animsOff and "OFF" or "ON") + + game.save.party = { Pokemon.new(game.data, "PARASECT", 40) } + U.teleport(game, "PALLET_TOWN", 5, 6, "down") + U.wait(20) + local ow = game.overworld + check("the overworld is up", ow ~= nil) + + local battle = BattleState.newWild(game, "RATTATA", 5) + battle.onFinish = function() end + ow:pushBattle(battle) + for _ = 1, 600 do + if game.stack:top() == battle and (battle.introSlide or 0) == 0 then break end + U.wait(1) + end + check("the battle reached the screen", game.stack:top() == battle) + + battle.player.curMoves = { { id = "SUBSTITUTE", pp = 10 } } + battle.enemy.curMoves = { { id = "TAIL_WHIP", pp = 30 } } + + for _ = 1, 400 do + if battle.phase == "menu" then break end + U.tap(game, "a") + U.wait(6) + end + check("the battle reached its FIGHT/PKMN/ITEM/RUN menu", battle.phase == "menu") + + U.tap(game, "a") + U.wait(20) + U.tap(game, "a") + + local function textNow() + local c = battle.current + return c and c.text or nil + end + local function waitForText(needle, frames) + for _ = 1, frames do + local t = textNow() + if t and t:find(needle, 1, true) then return true end + if battle.phase == "menu" then U.tap(game, "a") end + U.wait(1) + end + return false + end + + local sawUsed = waitForText("SUBSTITUTE!", 900) + check("the 'used SUBSTITUTE!' page is on screen", sawUsed) + U.shot(game, DIR .. "/sub2030_" .. tag .. "_1_announce.png") + check("the substitute is up in RAM", battle.player.substituteHP ~= nil) + check("but the doll is not on screen yet (#2030)", + battle.player.substitutePending == true) + check("so the pic slot still reports the mon's own pic", + battle:faintPicKind(battle.player) == "pic") + + U.wait(40) + U.shot(game, DIR .. "/sub2030_" .. tag .. "_2_hold.png") + + local sawDoll = false + for _ = 1, 900 do + if not battle.player.substitutePending then sawDoll = true break end + U.wait(1) + end + check("the doll lands with the animation", sawDoll) + U.wait(2) + U.shot(game, DIR .. "/sub2030_" .. tag .. "_3_doll.png") + check("and the pic slot reports the doll", + battle:faintPicKind(battle.player) == "doll") + + local sawCreated = waitForText("created", 900) + check("'It created a SUBSTITUTE!' prints after the doll", sawCreated) + U.shot(game, DIR .. "/sub2030_" .. tag .. "_4_created.png") + + U.log("Shots: " .. DIR .. "/sub2030_" .. tag .. "_*.png") + U.log("Shot 1 (the announcement) must still be the PARASECT back pic.") + U.log("Shot 2 is 40 frames later: with animations ON that is the 50-frame") + U.log("entry hold or the slide-off, never the doll. Shot 3 is the doll,") + U.log("shot 4 the doll under 'It created a SUBSTITUTE!'. Broken looked") + U.log("like the doll in ALL FOUR shots, with no slide-off at all.") + + while true do coroutine.yield() end +end diff --git a/tests/drivers/battle_transform_bug2030_test.lua b/tests/drivers/battle_transform_bug2030_test.lua new file mode 100644 index 00000000..a37e7088 --- /dev/null +++ b/tests/drivers/battle_transform_bug2030_test.lua @@ -0,0 +1,102 @@ +-- animation ahead of the cart. transform.asm:37-45 plays the animation +-- data/battle_anims/special_effect_pointers.asm:30), then copies the data +-- and prints _TransformedText (transform.asm:57-134). +-- POKEPORT_DRIVER=tests/drivers/battle_transform_bug2030_test.lua \ +-- POKEPORT_IDENTITY=bug2030 POKEPORT_TOUCH=0 SHOT_DIR=/tmp/shots love . +-- BUG2030_ANIMS_OFF=1 POKEPORT_DRIVER=tests/drivers/battle_transform_bug2030_test.lua \ +-- POKEPORT_IDENTITY=bug2030 POKEPORT_TOUCH=0 SHOT_DIR=/tmp/shots love . +return function(game) + local U = dofile("tests/drivers/util.lua") + local DIR = os.getenv("SHOT_DIR") or "/tmp/shots" + local animsOff = os.getenv("BUG2030_ANIMS_OFF") == "1" + local Pokemon = require("src.pokemon.Pokemon") + local BattleState = require("src.battle.BattleState") + + local tag = animsOff and "animoff" or "animon" + local function check(label, ok) + U.log(ok and "PASS" or "FAIL", label) + return ok + end + + game.save.options = game.save.options or {} + game.save.options.animations = not animsOff + U.log("issue #2030 -- TRANSFORM pic timing, animations", + animsOff and "OFF" or "ON") + + game.save.party = { Pokemon.new(game.data, "PARASECT", 24) } + U.teleport(game, "PALLET_TOWN", 5, 6, "down") + U.wait(20) + local ow = game.overworld + check("the overworld is up", ow ~= nil) + + local battle = BattleState.newWild(game, "DITTO", 26) + battle.onFinish = function() end + ow:pushBattle(battle) + for _ = 1, 600 do + if game.stack:top() == battle and (battle.introSlide or 0) == 0 then break end + U.wait(1) + end + check("the battle reached the screen", game.stack:top() == battle) + + battle.enemy.curMoves = { { id = "TRANSFORM", pp = 10 } } + battle.player.curMoves = { { id = "GROWL", pp = 40 } } + local dittoPic = battle.enemy.sprite + local copiedPic = battle:speciesSprite(battle.player.mon.species, false) + check("the enemy starts on the DITTO pic", dittoPic ~= nil) + check("the PARASECT front pic is loadable", copiedPic ~= nil) + check("they are different images, so the swap is visible", + dittoPic ~= copiedPic) + + for _ = 1, 400 do + if battle.phase == "menu" then break end + U.tap(game, "a") + U.wait(6) + end + check("the battle reached its FIGHT/PKMN/ITEM/RUN menu", battle.phase == "menu") + + U.tap(game, "a") + U.wait(20) + U.tap(game, "a") + + local function textNow() + local c = battle.current + return c and c.text or nil + end + local function waitForText(needle, frames) + for _ = 1, frames do + local t = textNow() + if t and t:find(needle, 1, true) then return true end + if battle.phase == "menu" then U.tap(game, "a") end + U.wait(1) + end + return false + end + + local sawUsed = waitForText("TRANSFORM!", 900) + check("the 'used TRANSFORM!' page is on screen", sawUsed) + local picAtAnnounce = battle.enemy.sprite + U.shot(game, DIR .. "/bug2030_" .. tag .. "_1_announce.png") + check("the enemy is STILL the DITTO blob while the move is announced (#2030)", + picAtAnnounce == dittoPic) + + local sawTransformed = waitForText("transformed", 900) + check("the '...transformed into...' page is on screen", sawTransformed) + U.wait(2) + U.shot(game, DIR .. "/bug2030_" .. tag .. "_2_transformed.png") + check("the enemy is the copied species by then", + battle.enemy.sprite == copiedPic) + + U.log("Shots: " .. DIR .. "/bug2030_" .. tag .. "_*.png") + U.log("Shot 1 (the announcement) must still be the pink DITTO blob; the") + U.log("black blocks of the animation only start after it. Shot 2 must be") + U.log("PARASECT. Broken looked like PARASECT in BOTH shots. The near miss") + U.log("to watch for is the swap landing mid-animation instead of on its") + U.log("last row -- the blob should still be a blob under the green ring.") + U.log("Run again with BUG2030_ANIMS_OFF=1 for the OPTION -> ANIMATION OFF") + U.log("path: there is no animation to watch, but the pic must still hold") + U.log("the blob through the announcement and swap before 'transformed'.") + U.log("The pad is yours -- GROWL keeps the DITTO alive, so you can watch") + U.log("it again on the next turn.") + + while true do coroutine.yield() end +end diff --git a/tests/drivers/crystal_battle_text.lua b/tests/drivers/crystal_battle_text.lua new file mode 100644 index 00000000..2fb7c981 --- /dev/null +++ b/tests/drivers/crystal_battle_text.lua @@ -0,0 +1,138 @@ +-- POKEPORT_IDENTITY=crystal-sep01 POKEPORT_VERSION=crystal \ +-- POKEPORT_DRIVER=tests/drivers/crystal_battle_text.lua \ +-- POKEPORT_SHOT_DIR=/tmp/crystal-battle-text love . +-- time (PrintLetterDelay, ../pokecrystal/home/print_text.asm:1). A +-- (../pokecrystal/engine/battle/core.asm:9104-9130). +local U = require("tests.drivers.util") + +local Mon = require("src.battle.gen2.Mon") + +return function(game) + local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/crystal-battle-text" + local fails = 0 + local function ok(cond, line) + if not cond then fails = fails + 1 end + print("[battle-text] " .. (cond and "PASS " or "FAIL ") .. line) + end + + U.wait(45) + local world = game.world + assert(world and world.map, "the crystal world did not boot") + + local lead = Mon.new(game.data, "CYNDAQUIL", 12) + assert(lead and #lead.moves > 0, "could not build a CYNDAQUIL") + game.save.party = { lead } + game.save.inventory = { POKE_BALL = 5 } + + local function fight(opts) + local wild = Mon.new(game.data, opts.species, opts.level or 6) + assert(wild, "could not build a wild " .. tostring(opts.species)) + if opts.status then + wild.status = opts.status + wild.statusTurns = 7 + end + assert(world:startBattle({ wild = wild, battleType = opts.battleType }), + "startBattle failed") + local screen + for _ = 1, 600 do + local top = game.stack:top() + if top and top.battle then screen = top break end + U.wait(1) + end + assert(screen and screen.battle, "battle screen never came up") + return screen, wild + end + + local function introEvent(screen) + for _, event in ipairs(screen.queue) do + if event.intro then return event end + end + return nil + end + + local function endBattle(screen) + for _ = 1, 900 do + if screen.phase == "done" or game.stack:top() ~= screen then break end + screen.battle.enemy.hp = 0 + U.tap(game, "a") + U.wait(2) + end + for _ = 1, 300 do + if game.stack:top() ~= screen then break end + U.tap(game, "a") + U.wait(2) + end + end + + local screen = fight({ species = "PIDGEY" }) + local intro = introEvent(screen) + ok(intro ~= nil and intro.text == "Wild PIDGEY appeared!", + "the wild arm is WildPokemanAppearedText: " + .. tostring(intro and intro.text)) + + for _ = 1, 400 do + if screen.message then break end + U.wait(1) + end + local grew, shots = {}, 0 + for i = 1, 3 do + shots = shots + 1 + grew[i] = screen.typer and screen.typer.shown or -1 + U.shot(game, out .. ("/%02d-typing.png"):format(i)) + end + print(("[battle-text] glyphs up at the three shots: %d, %d, %d") + :format(grew[1], grew[2], grew[3])) + ok(grew[1] >= 0, "the intro line went through the typewriter") + ok(grew[1] < (screen.typer and screen.typer.total or 0), + "and was not whole on the first shot") + ok(grew[3] > grew[1], "the box filled in as the frames went by") + + local before = screen.typer.shown + U.tap(game, "a") + U.wait(1) + ok(screen.typer.shown <= before + 2, + "a tap mid-print does not dump the rest of the line") + ok(screen.messageTimer > 0, "and does not page the box away either") + + for _ = 1, 400 do + if screen.typer:done() then break end + U.wait(1) + end + ok(screen.typer:done(), "the line finishes on its own") + U.shot(game, out .. "/04-printed.png") + endBattle(screen) + + local treeScreen, treeMon = fight({ species = "EXEGGCUTE", level = 10, + battleType = "tree", status = "sleep" }) + local treeIntro = introEvent(treeScreen) + ok(treeIntro ~= nil + and treeIntro.text == "EXEGGCUTE fell out of the tree!", + "PokemonFellFromTreeText: " .. tostring(treeIntro and treeIntro.text)) + ok(treeIntro ~= nil and treeIntro.cry == nil, + "CheckSleepingTreeMon takes the cry with it") + ok(treeMon.status == "sleep", "and the mon is still asleep in the battle") + for _ = 1, 400 do + if treeScreen.typer and treeScreen.typer:done() then break end + U.wait(1) + end + U.shot(game, out .. "/05-tree.png") + endBattle(treeScreen) + + local fishScreen = fight({ species = "MAGIKARP", level = 10, + battleType = "fish" }) + local fishIntro = introEvent(fishScreen) + ok(fishIntro ~= nil + and fishIntro.text == "The hooked MAGIKARP attacked!", + "HookedPokemonAttackedText: " .. tostring(fishIntro and fishIntro.text)) + ok(fishIntro ~= nil and fishIntro.cry ~= nil, + "and an awake hooked mon still cries") + for _ = 1, 400 do + if fishScreen.typer and fishScreen.typer:done() then break end + U.wait(1) + end + U.shot(game, out .. "/06-fish.png") + + print("[battle-text] " .. (fails == 0 and "PASS all claims" or + (fails .. " claims failed")) .. " -- shots in " .. out) + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/drivers/crystal_evolution_anim_shots.lua b/tests/drivers/crystal_evolution_anim_shots.lua new file mode 100644 index 00000000..e1e944cc --- /dev/null +++ b/tests/drivers/crystal_evolution_anim_shots.lua @@ -0,0 +1,90 @@ +-- POKEPORT_IDENTITY=crystal-sep01 POKEPORT_VERSION=crystal \ +-- POKEPORT_DRIVER=tests/drivers/crystal_evolution_anim_shots.lua \ +-- POKEPORT_SHOT_DIR=/tmp/crystal-evo love . +-- ../pokecrystal/engine/movie/evolution_animation.asm:82-92, :113-130 +-- ../pokecrystal/engine/gfx/pic_animation.asm:73 +local U = require("tests.drivers.util") + +local Evolution = require("src.core.gen2.Evolution") +local EvolutionAnim = require("src.ui.gen2.EvolutionAnim") +local Mon = require("src.battle.gen2.Mon") +local Music = require("src.core.Music") +local Palettes = require("src.world.gen2.Palettes") + +local FRAME_LIMIT = 1800 + +return function(game) + local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/crystal-evo" + local species = os.getenv("POKEPORT_EVO_SPECIES") or "CYNDAQUIL" + local level = tonumber(os.getenv("POKEPORT_EVO_LEVEL") or "16") + local fails = 0 + local function ok(cond, line) + if not cond then fails = fails + 1 end + print("[evoanim] " .. (cond and "PASS " or "FAIL ") .. line) + end + + U.wait(45) + assert(game.world and game.world.map, "crystal world did not boot") + + local probe = Mon.new(game.data, species, level) + local entry = Evolution.checkMon(game.data, probe, + { timeOfDay = Palettes.clockDaytime() }) + assert(entry, species .. " at " .. level .. " has no evolution") + local def = game.data.pokemon[entry.into] + ok(def and def.anim ~= nil, + entry.into .. " has an anim row in this cache") + + game.save.party = { Mon.new(game.data, species, level) } + local mapSong = Music.current() + local finished = nil + local screen = EvolutionAnim.new(game, { + mon = game.save.party[1], entry = entry, index = 1, + party = game.save.party, save = game.save, + onDone = function(result) finished = result end, + }) + game.stack:push(screen) + + local first, last, anim = {}, {}, 0 + local musicAt = nil + local steps = 0 + while not finished and steps < FRAME_LIMIT do + local phase = screen.phase or "?" + local now = U.frame() + first[phase] = first[phase] or now + last[phase] = now + local song = Music.current() + if not musicAt and song and song ~= mapSong then musicAt = now end + if phase == "picAnim" then + anim = anim + 1 + U.shot(game, ("%s/anim-%04d.png"):format(out, anim)) + elseif phase ~= "cry" and phase ~= "flash" and steps % 8 == 0 then + U.shot(game, ("%s/evo-%04d-%s.png"):format(out, steps, phase)) + else + U.wait(1) + end + steps = steps + 1 + end + U.shot(game, ("%s/evo-end.png"):format(out)) + assert(finished, "the evolution screen never finished") + + -- ../pokecrystal/engine/movie/evolution_animation.asm:85-89 + ok(first.cry ~= nil and musicAt ~= nil and musicAt > first.cry, + "MUSIC_EVOLUTION starts after the old cry (cry at " + .. tostring(first.cry) .. ", music at " .. tostring(musicAt) .. ")") + ok(first.flash ~= nil and musicAt ~= nil + and first.flash - musicAt >= Evolution.MUSIC_FRAMES - 2, + "and the 80 DelayFrames are spent after it, not under the cry") + + -- ../pokecrystal/engine/gfx/pic_animation.asm:73 + ok(anim > 1, "ANIM_MON_EVOLVE ran (" .. anim .. " iterations)") + ok(first.congrats ~= nil and first.picAnim ~= nil + and first.congrats > last.picAnim, + "the congratulations page waits for the scene to end") + ok(game.save.party[1].species == entry.into, + "the party slot took " .. entry.into) + + print("[evoanim] shots in " .. out) + print("[evoanim] " .. (fails == 0 and "all claims passed" + or (fails .. " claims failed"))) + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/drivers/crystal_fly_bird_bug2015_test.lua b/tests/drivers/crystal_fly_bird_bug2015_test.lua index de1eea5c..f8e04681 100644 --- a/tests/drivers/crystal_fly_bird_bug2015_test.lua +++ b/tests/drivers/crystal_fly_bird_bug2015_test.lua @@ -29,6 +29,14 @@ return function(game) local function tap(btn) U.tap(game, btn) U.wait(3) end local function top() return game.stack:top() end + local function waitFor(id, limit) + for _ = 1, limit or 120 do + local state = top() + if (state and state.screenId or nil) == id then return true end + U.wait(1) + end + return false + end U.wait(45) local world = game.world @@ -84,6 +92,7 @@ return function(game) end tap("a") + waitFor("Gen2PartyMenu", 90) local party = top() if not ok(party and party.screenId == "Gen2PartyMenu", "POKeMON opened the party list") then @@ -107,6 +116,7 @@ return function(game) end tap("a") U.wait(10) + waitFor("Gen2Pokegear", 120) local picker = top() if not ok(picker and picker.screenId == "Gen2Pokegear" and picker.fly, diff --git a/tests/drivers/crystal_fly_bug1960_test.lua b/tests/drivers/crystal_fly_bug1960_test.lua index 165ddb1b..ff016805 100644 --- a/tests/drivers/crystal_fly_bug1960_test.lua +++ b/tests/drivers/crystal_fly_bug1960_test.lua @@ -28,6 +28,14 @@ return function(game) local function tap(btn) U.tap(game, btn) U.wait(3) end local function top() return game.stack:top() end + local function waitFor(id, limit) + for _ = 1, limit or 120 do + local state = top() + if (state and state.screenId or nil) == id then return true end + U.wait(1) + end + return false + end U.wait(45) local world = game.world @@ -80,6 +88,7 @@ return function(game) end tap("a") + waitFor("Gen2PartyMenu", 90) local party = top() if not ok(party and party.screenId == "Gen2PartyMenu", "POKeMON opened the party list") then @@ -103,6 +112,7 @@ return function(game) end tap("a") U.wait(10) + waitFor("Gen2Pokegear", 120) local picker = top() if not ok(picker and picker.screenId == "Gen2Pokegear" and picker.fly, diff --git a/tests/drivers/crystal_fly_flash_bug2020_test.lua b/tests/drivers/crystal_fly_flash_bug2020_test.lua index 690fa42a..f1e8c004 100644 --- a/tests/drivers/crystal_fly_flash_bug2020_test.lua +++ b/tests/drivers/crystal_fly_flash_bug2020_test.lua @@ -64,7 +64,16 @@ return function(game) U.wait(2) end + local function waitFor(id, limit) + for _ = 1, limit or 120 do + if topId() == id then return true end + U.wait(1) + end + return topId() == id + end + local function openFlyRow() + waitFor(nil, 240) local menu for _ = 1, 10 do tap("start") @@ -81,6 +90,7 @@ return function(game) tap("down") end tap("a") + waitFor("Gen2PartyMenu", 90) local party = top() if not ok(party and party.screenId == "Gen2PartyMenu", "POKeMON opened the party list") then @@ -143,14 +153,21 @@ return function(game) U.shot(game, out .. "/02-fly-map.png") tap("b") - U.wait(6) + -- engine/pokegear/pokegear.asm:2062-2078 + for _ = 1, 120 do + if topId() == "Gen2PartyMenu" then break end + U.wait(1) + end ok(topId() == "Gen2PartyMenu", ("B returned to the party list (top is %s)") :format(tostring(topId()))) ok(world.queuedFieldMove == nil, "with no fly queued behind it") U.shot(game, out .. "/03-b-back-to-party.png") - tap("b") - tap("b") + for _ = 1, 6 do + if topId() == nil then break end + tap("b") + waitFor(nil, 60) + end U.wait(10) party = openFlyRow() U.tap(game, "a") diff --git a/tests/drivers/crystal_fly_hold_bug2051_test.lua b/tests/drivers/crystal_fly_hold_bug2051_test.lua new file mode 100644 index 00000000..97d5812c --- /dev/null +++ b/tests/drivers/crystal_fly_hold_bug2051_test.lua @@ -0,0 +1,248 @@ +-- ../pokecrystal/engine/pokegear/pokegear.asm:2026-2046 +-- ../pokecrystal/engine/pokegear/pokegear.asm:2062-2087 +-- ../pokecrystal/home/map.asm:1927-1940 +-- ../pokecrystal/engine/events/field_moves.asm:305-311 +-- POKEPORT_IDENTITY=crystal-sep01 POKEPORT_VERSION=crystal POKEPORT_TOUCH=0 \ +-- POKEPORT_SPEED=1 POKEPORT_DRIVER=tests/drivers/crystal_fly_hold_bug2051_test.lua \ +-- POKEPORT_SHOT_DIR=/tmp/crystal-fly2051 +local U = require("tests.drivers.util") + +local FieldMoves = require("src.world.gen2.FieldMoves") +local Mon = require("src.battle.gen2.Mon") +local World = require("src.world.gen2.World") + +local FLY_SPECIES = "PIDGEOTTO" + +return function(game) + local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/crystal-fly2051" + local fails = 0 + + local function ok(cond, msg) + if cond then + print("[fly2051] ok " .. msg) + else + fails = fails + 1 + print("[fly2051] FAIL " .. msg) + end + return cond + end + + local function tap(btn) U.tap(game, btn) U.wait(3) end + local function top() return game.stack:top() end + local function topId() + local state = top() + return state and state.screenId or nil + end + + local counts = { blank = 0, white = 0, preroll = 0, cancel = 0 } + local counting = nil + local shotAt = {} + local realDraw = love.draw + love.draw = function(...) + realDraw(...) + if not counting then return end + local world = game.world + local id = topId() + if counting == "in" or counting == "cancel" then + if id == "Gen2BlankScreen" then + counts[counting == "in" and "blank" or "cancel"] = + counts[counting == "in" and "blank" or "cancel"] + 1 + end + elseif counting == "out" then + if id == nil and world.fade == "white" and (world.fadeLevel or 0) >= 1 then + counts.white = counts.white + 1 + end + if id == nil and world.fade == nil and world.mapSetup == nil + and (not world.flyAnim or (world.flyAnim.preroll or 0) > 0) then + counts.preroll = counts.preroll + 1 + end + local _, hidden = world:flyHides() + if id == nil and not hidden then counts.player = (counts.player or 0) + 1 end + end + end + + U.wait(45) + local world = game.world + local save, data = game.save, game.data + if not ok(world and world.map, "booted into the overworld") then + error("fly2051: no world") + end + + save.player = save.player or {} + if type(save.player.badges) ~= "table" then save.player.badges = {} end + save.player.badges.STORM = true + save.engineFlags = save.engineFlags or {} + for _, row in ipairs(FieldMoves.FLYPOINTS) do + save.engineFlags[row.flag] = true + end + + local flyer = Mon.new(data, FLY_SPECIES, 24) + table.remove(flyer.moves, 1) + Mon.learnMove(flyer, "FLY", data) + save.party = { flyer } + + world.mapScenes = world.mapScenes or {} + world.mapScenes.NEW_BARK_TOWN = 1 + world:warpToMapId("NEW_BARK_TOWN", 9, 8, "down") + U.wait(15) + ok(world.map.id == "NEW_BARK_TOWN", "standing in New Bark Town") + + for _ = 1, 240 do + if not world:busy() then break end + U.wait(2) + end + + local function openFlyRow() + for _ = 1, 120 do + if topId() == nil then break end + U.wait(1) + end + local menu + for _ = 1, 10 do + tap("start") + menu = top() + if menu and menu.screenId == "Gen2StartMenu" then break end + U.wait(6) + end + if not ok(menu and menu.screenId == "Gen2StartMenu", + "START opened the menu") then + error("fly2051: no start menu") + end + for _ = 1, 10 do + if menu.list:current().value == "pokemon" then break end + tap("down") + end + tap("a") + for _ = 1, 60 do + if topId() == "Gen2PartyMenu" then break end + U.wait(1) + end + local party = top() + if not ok(party and party.screenId == "Gen2PartyMenu", + "POKeMON opened the party list") then + error("fly2051: no party list") + end + tap("a") + local sub = party.submenu + if not ok(sub ~= nil, "A opened the action submenu") then + error("fly2051: no submenu") + end + local flyRow + for index, item in ipairs(sub.items) do + if item.id == "FLY" then flyRow = index end + end + if not ok(flyRow ~= nil, "which lists FLY") then + error("fly2051: the submenu has no FLY row") + end + for _ = 1, #sub.items do + if sub.index == flyRow then break end + tap("down") + end + return party + end + + openFlyRow() + counting = "in" + U.tap(game, "a") + for _ = 1, 120 do + U.wait(1) + if counts.blank == 14 and not shotAt.blank then + shotAt.blank = true + U.shot(game, out .. "/01-blank-in.png") + end + if topId() == "Gen2Pokegear" then break end + end + counting = nil + ok(counts.blank == World.FLY_MAP_BUILD_FRAMES, + ("_FlyMap held white for %d drawn frames (want %d)") + :format(counts.blank, World.FLY_MAP_BUILD_FRAMES)) + if not ok(topId() == "Gen2Pokegear", "then the town map took over") then + error("fly2051: no picker") + end + U.wait(6) + U.shot(game, out .. "/02-fly-map.png") + + counting = "cancel" + U.tap(game, "b") + for _ = 1, 120 do + U.wait(1) + if topId() == "Gen2PartyMenu" then break end + end + counting = nil + local wantCancel = World.flyCancelBlankFrames(#save.party) + ok(counts.cancel == wantCancel, + ("B whited out for %d drawn frames (want %d)"):format(counts.cancel, wantCancel)) + ok(topId() == "Gen2PartyMenu", ("B returned to the party list (top is %s)") + :format(tostring(topId()))) + U.shot(game, out .. "/03-b-back-to-party.png") + + tap("b") + tap("b") + U.wait(10) + openFlyRow() + U.tap(game, "a") + for _ = 1, 120 do + U.wait(1) + if topId() == "Gen2Pokegear" then break end + end + if not ok(topId() == "Gen2Pokegear", "the picker is back up") then + error("fly2051: no picker on the second pass") + end + tap("up") + U.wait(6) + + counting = "out" + U.tap(game, "a") + local sawBird = false + for _ = 1, 300 do + U.wait(1) + if counts.white == 14 and not shotAt.white then + shotAt.white = true + U.shot(game, out .. "/04-exit-white.png") + end + local ms = world.mapSetup + if ms and ms.phase == "in" and (ms.step or 9) <= 2 and not shotAt.ramp then + shotAt.ramp = true + U.shot(game, out .. "/05-fade-in.png") + end + if world.flyAnim and (world.flyAnim.preroll or 0) == 0 then + sawBird = true + break + end + end + counting = nil + ok(counts.white == World.FLY_EXIT_WHITE_FRAMES, + ("the exit held full white for %d drawn frames (want %d)") + :format(counts.white, World.FLY_EXIT_WHITE_FRAMES)) + ok(counts.preroll == World.FLY_FROM_PREROLL, + ("the map stood clear for %d drawn frames before the bird (want %d)") + :format(counts.preroll, World.FLY_FROM_PREROLL)) + ok((counts.player or 0) == 0, + ("HideSprites: the player never showed on the way out (%d frame(s))") + :format(counts.player or 0)) + ok(sawBird, "then the take-off sprite appeared") + U.wait(4) + U.shot(game, out .. "/06-takeoff.png") + + for _ = 1, 1800 do + U.wait(1) + if world.flyAnim == nil and world.map.id ~= "NEW_BARK_TOWN" then break end + end + U.wait(20) + ok(world.map.id ~= "NEW_BARK_TOWN", + ("landed on %s"):format(tostring(world.map.id))) + U.shot(game, out .. "/07-landed.png") + + love.draw = realDraw + U.log("fly hold frames (#2051): blank in " .. counts.blank .. ", cancel " + .. counts.cancel .. ", exit white " .. counts.white .. ", preroll " + .. counts.preroll .. ".") + U.log("01-blank-in.png / 04-exit-white.png are the two holds, edge to edge.") + U.log("shots are in " .. out .. ".") + if fails > 0 then + U.log(("%d assertion(s) failed above."):format(fails)) + love.event.quit(1) + return + end + love.event.quit() +end diff --git a/tests/drivers/crystal_headbutt_probe.lua b/tests/drivers/crystal_headbutt_probe.lua new file mode 100644 index 00000000..f4f1ca99 --- /dev/null +++ b/tests/drivers/crystal_headbutt_probe.lua @@ -0,0 +1,109 @@ +-- ../pokecrystal/engine/events/treemons.asm:1 TreeMonEncounter +-- ../pokecrystal/engine/battle/core.asm:6422 CheckSleepingTreeMon +-- POKEPORT_IDENTITY=crystal-aug31 POKEPORT_VERSION=crystal \ +-- POKEPORT_DRIVER=tests/drivers/crystal_headbutt_probe.lua love . +local U = require("tests.drivers.util") + +local Encounter = require("src.battle.gen2.Encounter") +local GameVersion = require("src.core.GameVersion") + +-- data/wild/treemon_maps.asm: Route 29 is TREEMON_SET_ROUTE, New Bark is +local TREE_MAP = "ROUTE_29" +local DEAD_MAP = "NEW_BARK_TOWN" + +return function(game) + local fails = 0 + local function say(line) print("[headbutt] " .. line) end + local function ok(cond, line) + if not cond then fails = fails + 1 end + say((cond and "PASS " or "FAIL ") .. line) + end + + U.wait(60) + local world = game.world + if not (world and world.map and world.encounters) then + say("FAIL the crystal world did not boot") + love.event.quit(1) + return + end + local engine = GameVersion.engine() + ok(engine == "crystal", "engine lineage is crystal (got " .. tostring(engine) .. ")") + + local save = game.save + save.player = save.player or {} + save.player.id = save.player.id or 0 + local otId = save.player.id + say("wPlayerID = " .. tostring(otId)) + + local fought + local realStart = world.startBattle + world.startBattle = function(self, opts) + fought = opts + return true + end + + world:warpToMapId(TREE_MAP, 10, 10, "up") + U.wait(45) + ok(world.map and world.map.def and world.map.def.id == TREE_MAP, + "standing on " .. TREE_MAP) + say("tree set = " .. tostring(Encounter.treeSet(world.encounters, TREE_MAP))) + + local cx, cy = 10, 9 + local score = Encounter.treeScore(cx, cy, otId) + local gate = (score == Encounter.TREEMON_SCORE_RARE and 8) + or (score == Encounter.TREEMON_SCORE_GOOD and 5) or 1 + say(("tree (%d,%d) scores %d -> %d in 10"):format(cx, cy, score, gate)) + + world.treemonRandom = function() return gate - 1 end + fought = nil + ok(world:tryHeadbutt(cx, cy) == "battle", + ("a roll of %d is inside the gate"):format(gate - 1)) + ok(fought and fought.battleType == "tree", + "and the battle carries BATTLETYPE_TREE") + if fought and fought.wild then + say(("caught %s status=%s turns=%s"):format(tostring(fought.wild.species), + tostring(fought.wild.status), tostring(fought.wild.statusTurns))) + end + + world.treemonRandom = function() return gate end + ok(world:tryHeadbutt(cx, cy) == "nothing", + ("a roll of %d is outside it"):format(gate)) + + world.tod = "NITE" + world.treemonRandom = function() return 0 end + local slept, tries = false, 0 + while tries < 200 and not slept do + tries = tries + 1 + fought = nil + world.treemonRandom = function(n) + if n == 10 then return 0 end + return (tries * 7) % 100 + end + if world:tryHeadbutt(cx, cy) == "battle" and fought and fought.wild then + if Encounter.treeMonAsleep(fought.wild.species, "NITE", engine) then + slept = fought.wild.status == "sleep" + and fought.wild.statusTurns == Encounter.TREEMON_SLEEP_TURNS + if slept then + say("asleep at night: " .. tostring(fought.wild.species)) + end + break + end + end + end + ok(slept or tries >= 200, + "a Nite-list tree mon comes out asleep for 7 turns") + + world:warpToMapId(DEAD_MAP, 9, 8, "down") + U.wait(45) + say("dead-map set = " .. tostring(Encounter.treeSet(world.encounters, DEAD_MAP))) + local battles = 0 + for i = 1, 200 do + world.treemonRandom = function() return i % 10 end + if world:tryHeadbutt(5, 5) == "battle" then battles = battles + 1 end + end + ok(battles == 0, ("TREEMON_SET_NONE gives up nothing (%d battles)"):format(battles)) + + world.startBattle = realStart + say(fails == 0 and "all claims passed" or (fails .. " claims failed")) + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/drivers/crystal_hp_bar_cadence.lua b/tests/drivers/crystal_hp_bar_cadence.lua new file mode 100644 index 00000000..9358cd50 --- /dev/null +++ b/tests/drivers/crystal_hp_bar_cadence.lua @@ -0,0 +1,103 @@ +-- POKEPORT_IDENTITY=crystal-sep01 POKEPORT_VERSION=crystal \ +-- POKEPORT_DRIVER=tests/drivers/crystal_hp_bar_cadence.lua \ +-- POKEPORT_SHOT_DIR=/tmp/crystal-hp-bar-cadence love . +-- ../pokecrystal/engine/battle/anim_hp_bar.asm:286 +local U = require("tests.drivers.util") + +local Mon = require("src.battle.gen2.Mon") +local HpBar = require("src.battle.gen2.HpBar") + +return function(game) + local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/crystal-hp-bar-cadence" + os.execute('mkdir -p "' .. out .. '" 2>/dev/null') + local fails = 0 + local function ok(cond, line) + if not cond then fails = fails + 1 end + print("[hp-bar-cadence] " .. (cond and "PASS " or "FAIL ") .. line) + end + + U.wait(45) + local world = game.world + assert(world and world.map, "the crystal world did not boot") + + local lead = Mon.new(game.data, "CYNDAQUIL", 12) + assert(lead, "could not build a CYNDAQUIL") + lead.moves = { { id = "EMBER", pp = 25, maxPp = 25 } } + game.save.party = { lead } + game.save.inventory = { POKE_BALL = 5 } + + local wild = Mon.new(game.data, "PIDGEY", 3) + assert(wild, "could not build a wild PIDGEY") + wild.maxHp = 15 + wild.hp = 15 + if wild.stats then wild.stats.hp = 15 end + wild.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } } + + assert(world:startBattle({ wild = wild }), "startBattle failed") + local screen + for _ = 1, 600 do + local top = game.stack:top() + if top and top.battle then screen = top break end + U.wait(1) + end + assert(screen and screen.battle, "battle screen never came up") + + for _ = 1, 900 do + if screen.phase == "menu" then break end + U.tap(game, "a") + U.wait(2) + end + ok(screen.phase == "menu", "reached the battle menu") + U.tap(game, "a") + U.wait(4) + U.tap(game, "a") + + local enemy = screen.battle.enemy + local maxHp = enemy.maxHp or (enemy.stats and enemy.stats.hp) or 0 + local start = screen.shownHp.enemy + print(("[hp-bar-cadence] enemy %d/%d before EMBER"):format(start, maxHp)) + + local tick, firstChange, lastChange, changes = 0, nil, nil, 0 + local startPx = HpBar.pixels(start, maxHp) + local lastHp, lastPx = start, startPx + local shooting = false + local gaps = {} + for _ = 1, 900 do + local hp = screen.shownHp.enemy + local px = screen.hudHpPixels and screen:hudHpPixels(enemy, "enemy") + or HpBar.pixels(hp, maxHp) + if hp ~= lastHp or px ~= lastPx then + changes = changes + 1 + if not firstChange then firstChange = tick end + if lastChange then gaps[#gaps + 1] = tick - lastChange end + lastChange = tick + shooting = true + end + if shooting then + print(("[hp-bar-cadence] tick %3d shownHp %2d px %2d hpAnim %s") + :format(tick, hp, px, tostring(screen.hpAnim ~= nil))) + game.capturePath = ("%s/t%03d.png"):format(out, tick) + end + lastHp, lastPx = hp, px + if shooting and hp == 0 and not screen.hpAnim then break end + U.wait(1) + tick = tick + 1 + end + + local span = (firstChange and lastChange) and (lastChange - firstChange) or -1 + print(("[hp-bar-cadence] %d changes, first at tick %s, last at tick %s, span %d") + :format(changes, tostring(firstChange), tostring(lastChange), span)) + local allTwo = #gaps > 0 + for _, g in ipairs(gaps) do if g ~= 2 then allTwo = false end end + ok(lastHp == 0, "the bar drained to zero") + ok(changes == startPx, + ("one change per pixel: %d of %d"):format(changes, startPx)) + ok(allTwo, "every change two ticks after the last") + ok(span == 2 * (startPx - 1), + ("first to last change spans %d ticks (expect %d)") + :format(span, 2 * (startPx - 1))) + + print("[hp-bar-cadence] " .. (fails == 0 and "PASS all claims" or + (fails .. " claims failed")) .. " -- shots in " .. out) + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/drivers/crystal_map_name_sign_speed_bug1994.lua b/tests/drivers/crystal_map_name_sign_speed_bug1994.lua index 2c08f2f4..52363b52 100644 --- a/tests/drivers/crystal_map_name_sign_speed_bug1994.lua +++ b/tests/drivers/crystal_map_name_sign_speed_bug1994.lua @@ -45,37 +45,48 @@ return function(game) end say(("%dX: game:logicSpeed() reports %s") :format(speed, tostring(game:logicSpeed()))) + -- ../pokecrystal/data/maps/setup_scripts.asm:32-56 + for _ = 1, 120 do + if not world.mapSetup then break end + U.wait(1) + end local frames, t0 = 0, love.timer.getTime() + local shownAt for _ = 1, 60 * 60 do if not world.mapSign then break end + if world.mapSign.shown and not shownAt then shownAt = frames end U.wait(1) frames = frames + 1 end local elapsed = love.timer.getTime() - t0 - say(("%dX: %d rendered frames (%d logic steps), %.3f real seconds") - :format(speed, frames, frames * speed, elapsed)) + say(("%dX: %d rendered frames (shown from %s), %.3f real seconds") + :format(speed, frames, tostring(shownAt), elapsed)) ok(world.mapSign == nil, speed .. "X: the sign expired") - return frames, elapsed + return frames, elapsed, shownAt end - for _, speed in ipairs({ 1, 4, 10 }) do - local frames, elapsed = measure(speed) + -- ../pokecrystal/engine/overworld/events.asm:177-191 + for _, speed in ipairs({ 1, 4, 10, 20, 200 }) do + local frames, elapsed, shownAt = measure(speed) if frames then - ok(math.abs(frames - 60) <= 15, - ("%dX: %d rendered frames is within 15 of 60"):format(speed, frames)) - ok(elapsed >= 0.75 and elapsed <= 1.5, - ("%dX: %.3fs of wall clock is the one-second hold"):format(speed, elapsed)) + ok(math.abs(frames - 122) <= 6, + ("%dX: %d rendered frames is within 6 of 122"):format(speed, frames)) + ok(elapsed >= 1.8 and elapsed <= 2.4, + ("%dX: %.3fs of wall clock is the two-second hold"):format(speed, elapsed)) + ok(shownAt ~= nil and shownAt <= 6, + ("%dX: the sign came on within the first HandleMap calls (frame %s)") + :format(speed, tostring(shownAt))) end end - game.speedOverride = 10 + game.speedOverride = 200 if raise() then - U.wait(30) - ok(world.mapSign ~= nil, - "10X: the banner is still up half a real second in") - U.shot(game, SHOT_DIR .. "/mapsign1994_10x_halfsecond.png") + U.wait(60) + ok(world.mapSign ~= nil and world.mapSign.shown, + "200X: the banner is still up one real second in") + U.shot(game, SHOT_DIR .. "/mapsign1994_200x_onesecond.png") else - ok(false, "10X: no sign came up for the screenshot") + ok(false, "200X: no sign came up for the screenshot") end game.speedOverride = nil diff --git a/tests/drivers/crystal_menu_whites_s6.lua b/tests/drivers/crystal_menu_whites_s6.lua new file mode 100644 index 00000000..a505d71b --- /dev/null +++ b/tests/drivers/crystal_menu_whites_s6.lua @@ -0,0 +1,175 @@ +-- ../pokecrystal/home/map.asm:1910-1940 +-- ../pokecrystal/engine/menus/start_menu.asm:444-518 +-- POKEPORT_IDENTITY=crystal-sep01b POKEPORT_VERSION=crystal POKEPORT_TOUCH=0 \ +-- POKEPORT_SPEED=1 POKEPORT_DRIVER=tests/drivers/crystal_menu_whites_s6.lua \ +-- POKEPORT_SHOT_DIR=/tmp/crystal-menu-whites +local U = require("tests.drivers.util") + +local Mon = require("src.battle.gen2.Mon") +local MenuFade = require("src.ui.gen2.MenuFade") + +return function(game) + local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/crystal-menu-whites" + os.execute('mkdir -p "' .. out .. '" 2>/dev/null') + local fails = 0 + + local function ok(cond, msg) + if cond then + print("[menuwhite] ok " .. msg) + else + fails = fails + 1 + print("[menuwhite] FAIL " .. msg) + end + return cond + end + + local function top() return game.stack:top() end + local function topId() + local state = top() + return state and state.screenId or nil + end + + local counts, shooting, shotIndex = nil, nil, 0 + local realDraw = love.draw + love.draw = function(...) + realDraw(...) + if not counts then return end + local state = top() + counts.frames = counts.frames + 1 + if state and state.screenId == "Gen2MenuFade" then + counts.fade = counts.fade + 1 + if state:level() >= 1 then + counts.white = counts.white + 1 + else + counts.ramp = counts.ramp + 1 + end + end + if shooting then + shotIndex = shotIndex + 1 + local path = ("%s/%s-%02d.png"):format(out, shooting, shotIndex) + love.graphics.captureScreenshot(function(imagedata) + local f = io.open(path, "wb") + if f then + f:write(imagedata:encode("png"):getString()) + f:close() + end + end) + end + end + + local function startCount(label) + counts = { frames = 0, fade = 0, white = 0, ramp = 0 } + shooting, shotIndex = label, 0 + end + local function stopCount() + local c = counts + counts, shooting = nil, nil + return c + end + + local function settle() + for _ = 1, 400 do + if topId() == nil and not game.world:busy() then return true end + U.wait(1) + end + return false + end + + local function openStart() + for _ = 1, 10 do + U.tap(game, "start") + for _ = 1, 10 do + if topId() == "Gen2StartMenu" then return top() end + U.wait(1) + end + end + return nil + end + + local function cursorTo(menu, id) + for _ = 1, 12 do + if menu.list:current().value == id then return true end + U.tap(game, "down") + U.wait(2) + end + return false + end + + local function waitTop(id, limit) + for _ = 1, limit or 120 do + if topId() == id then return true end + U.wait(1) + end + return false + end + + U.wait(45) + local world = game.world + local save, data = game.save, game.data + if not ok(world and world.map, "booted into the overworld") then + error("menuwhite: no world") + end + save.party = { Mon.new(data, "CYNDAQUIL", 12), Mon.new(data, "PIDGEY", 8) } + save.engineFlags = save.engineFlags or {} + save.engineFlags[4] = true + save.engineFlags[11] = true + world.mapScenes = world.mapScenes or {} + world.mapScenes.NEW_BARK_TOWN = 1 + world:warpToMapId("NEW_BARK_TOWN", 9, 8, "down") + U.wait(15) + settle() + + local function openPage(id, pageId, label, shots) + local menu = openStart() + if not ok(menu ~= nil, label .. ": START opened the menu") then return end + if not ok(cursorTo(menu, id), label .. ": cursor on the row") then return end + startCount(shots and (label .. "-open") or nil) + if not shots then shooting = nil end + U.tap(game, "a") + local landed = waitTop(pageId, 160) + local c = stopCount() + local wantWhite = MenuFade.openWhite(id, #save.party) + ok(landed, label .. ": the page came up") + ok(c.fade == MenuFade.framesOut(wantWhite), + ("%s: the fade ran %d drawn frames (want %d = 8 + %d)") + :format(label, c.fade, MenuFade.framesOut(wantWhite), wantWhite)) + ok(c.white == MenuFade.OUT_WHITE_FRAMES + wantWhite, + ("%s: %d of them white (want %d = 2 + %d)") + :format(label, c.white, MenuFade.OUT_WHITE_FRAMES + wantWhite, wantWhite)) + ok(c.ramp == MenuFade.OUT_FRAMES - MenuFade.OUT_WHITE_FRAMES, + ("%s: %d ramp frames (want 6)"):format(label, c.ramp)) + U.wait(12) + U.shot(game, ("%s/%s-page.png"):format(out, label)) + startCount(shots and (label .. "-close") or nil) + if not shots then shooting = nil end + U.tap(game, "b") + local back = waitTop("Gen2StartMenu", 160) + local d = stopCount() + local wantClose = MenuFade.closeWhite(id) + ok(back, label .. ": B came back to the START menu") + ok(d.fade == MenuFade.framesIn(wantClose), + ("%s: the close fade ran %d drawn frames (want %d = %d + 6)") + :format(label, d.fade, MenuFade.framesIn(wantClose), wantClose)) + ok(d.white == wantClose, + ("%s: %d of them white (want %d)"):format(label, d.white, wantClose)) + ok(d.ramp == MenuFade.IN_RAMP_FRAMES, + ("%s: %d ramp frames (want 6)"):format(label, d.ramp)) + U.wait(6) + U.tap(game, "start") + ok(settle(), label .. ": START closed the menu") + end + + openPage("pack", "Gen2PackMenu", "pack", true) + openPage("pokemon", "Gen2PartyMenu", "pokemon", true) + openPage("pokegear", "Gen2Pokegear", "pokegear", false) + openPage("status", "Gen2TrainerCard", "card", false) + + love.draw = realDraw + U.log("START page whites (S6): shots are in " .. out .. ".") + if fails > 0 then + U.log(("%d assertion(s) failed above."):format(fails)) + love.event.quit(1) + return + end + love.event.quit() +end diff --git a/tests/drivers/crystal_music_worker_bug2002.lua b/tests/drivers/crystal_music_worker_bug2002.lua new file mode 100644 index 00000000..eaccfbb0 --- /dev/null +++ b/tests/drivers/crystal_music_worker_bug2002.lua @@ -0,0 +1,81 @@ +-- ../pokecrystal/audio/engine.asm:84 +local U = require("tests.drivers.util") + +local ChipAudio = require("src.core.ChipAudio") +local Mon = require("src.battle.gen2.Mon") +local Music = require("src.core.Music") + +local SETTLE = 300 + +return function(game) + local fails = 0 + local logPath = os.getenv("POKEPORT_DRIVER_LOG") + local function say(line) + print("[2002] " .. line) + if logPath then + local file = io.open(logPath, "a") + if file then file:write(line, "\n") file:close() end + end + end + local function ok(cond, line) + if not cond then fails = fails + 1 end + say((cond and "PASS " or "FAIL ") .. line) + end + + say("jit.status=" .. tostring(jit and jit.status())) + + U.wait(60) + local world = game.world + if not (world and world.map) then + say("FAIL the crystal world did not boot") + love.event.quit(1) + return + end + + local save = game.save + save.party = { Mon.new(game.data, "CYNDAQUIL", 15) } + + local function scene(label) + U.wait(SETTLE) + local stats = ChipAudio.stats() + say(label .. ": song=" .. tostring(Music.current()) .. " " .. stats.line) + return stats + end + + world:warpToMapId("NEW_BARK_TOWN", 4, 6, "down") + local outdoor = scene("overworld") + ok(outdoor.worker ~= "none", "the chip worker is running") + + world:warpToMapId("PLAYERS_HOUSE_1F", 7, 5, "down") + scene("interior") + + local wild = Mon.new(game.data, "RATTATA", 4) + world:startBattle({ wild = wild }) + U.wait(10) + scene("battle") + for _ = 1, 240 do + local top = game.stack:top() + if top and top.battle then + game.stack:pop() + U.wait(2) + break + end + U.wait(1) + end + + local back = scene("overworld after battle") + + ok(back.underruns == 0, + "no queue underrun across every scene (" .. back.underruns .. ")") + ok(back.restarts == 0, + "and no restart (" .. back.restarts .. ")") + ok(back.depthMin and back.depthMin >= ChipAudio.MUSIC_PREROLL, + "the queue never fell below the pre-roll (min " + .. tostring(back.depthMin) .. ")") + ok(type(back.xrtAvg) == "number" and back.xrtAvg < 0.5, + "the worker's mean cost per buffer stays under half realtime (" + .. tostring(back.xrtAvg) .. ", worst " .. tostring(back.xrtMax) .. ")") + + say(fails == 0 and "ALL PASS" or (fails .. " FAILURES")) + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/drivers/crystal_rock_smash_menu_s5.lua b/tests/drivers/crystal_rock_smash_menu_s5.lua new file mode 100644 index 00000000..42061ca8 --- /dev/null +++ b/tests/drivers/crystal_rock_smash_menu_s5.lua @@ -0,0 +1,94 @@ +-- ../pokecrystal/engine/events/overworld.asm:1317 TryRockSmashFromMenu +-- POKEPORT_IDENTITY=crystal-sep01 POKEPORT_VERSION=crystal \ +-- POKEPORT_DRIVER=tests/drivers/crystal_rock_smash_menu_s5.lua \ +-- POKEPORT_SHOT_DIR=/tmp/shots love . + +local U = require("tests.drivers.util") + +local Mon = require("src.battle.gen2.Mon") +local PartyMenu = require("src.ui.gen2.PartyMenu") + +-- data/wild/treemon_maps.asm RockMonMaps +local ROCK_MAP = "DARK_CAVE_VIOLET_ENTRANCE" +local ROCK_X, ROCK_Y = 16, 14 +local STAND_X, STAND_Y = 15, 14 +local ROCK_SPECIES = { [98] = "KRABBY", [213] = "SHUCKLE" } + +return function(game) + local DIR = os.getenv("POKEPORT_SHOT_DIR") or os.getenv("SHOT_DIR") + or "/tmp/shots" + local fails = 0 + local function say(line) print("[rocksmashmenu] " .. line) end + local function ok(cond, line) + if not cond then fails = fails + 1 end + say((cond and "PASS " or "FAIL ") .. line) + end + + U.wait(60) + local world = game.world + if not (world and world.map) then + say("FAIL the world did not boot") + love.event.quit(1) + return + end + + world:setMap(ROCK_MAP, STAND_X, STAND_Y, "right") + U.wait(60) + ok(world.map and world.map.def and world.map.def.id == ROCK_MAP, + "arrived at " .. ROCK_MAP) + + local save = game.save + save.party = save.party or {} + if not save.party[1] then + save.party[1] = Mon.new(game.data, "GEODUDE", 20) + end + local lead = save.party[1] + lead.moves = lead.moves or {} + lead.moves[#lead.moves + 1] = { id = "ROCK_SMASH", pp = 15, maxPp = 15 } + + world.rockmonRandom = function() return 0 end + world.noWildEncounters = true + + local fought + local realScripted = world.startScriptedBattle + world.startScriptedBattle = function(self, record, wild, onDone) + if wild and wild.species then fought = wild.species end + return realScripted(self, record, wild, onDone) + end + + local rock + for _, e in ipairs(world.entities or {}) do + if e.cellX == ROCK_X and e.cellY == ROCK_Y then rock = e end + end + ok(rock ~= nil and rock.def ~= nil and rock.def.movement == 0x18, + "the object faced is SPRITEMOVEDATA_SMASHABLE_ROCK") + U.shot(game, DIR .. "/rocksmash_menu_before.png") + + local menu = PartyMenu.new(game, { save = save, submenu = true }) + menu:useFieldMove("ROCK_SMASH", lead) + ok(world.queuedScript ~= nil, "the menu queued RockSmashFromMenuScript") + ok(world.vm and world.vm.lastTalked == (rock and rock.def.index or -1) + 1, + "with hLastTalked pointing at the rock") + + U.wait(90) + U.shot(game, DIR .. "/rocksmash_menu_used.png") + for _ = 1, 60 do + if fought then break end + U.tap(game, "a") + U.wait(15) + end + U.shot(game, DIR .. "/rocksmash_menu_after.png") + ok(fought ~= nil, "the queued script reached startbattle with no yes/no") + ok(fought ~= nil and ROCK_SPECIES[fought] ~= nil, + "and the wild mon came out of TREEMON_SET_ROCK (got " + .. tostring(fought and (ROCK_SPECIES[fought] or fought)) .. ")") + + local gone = true + for _, e in ipairs(world.entities or {}) do + if e == rock and not e.hidden then gone = false end + end + ok(gone, "disappear LAST_TALKED took the rock away") + + say(fails == 0 and "all claims passed" or (fails .. " claims failed")) + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/drivers/crystal_sendout_pic.lua b/tests/drivers/crystal_sendout_pic.lua new file mode 100644 index 00000000..cc26f502 --- /dev/null +++ b/tests/drivers/crystal_sendout_pic.lua @@ -0,0 +1,119 @@ +-- POKEPORT_IDENTITY=crystal-sep01b POKEPORT_VERSION=crystal \ +-- POKEPORT_DRIVER=tests/drivers/crystal_sendout_pic.lua \ +-- POKEPORT_SHOT_DIR=/tmp/crystal-sendout-pic love . +-- ../pokecrystal/engine/battle/core.asm:82-93, :4027-4056 +-- ../pokecrystal/engine/battle_anims/bg_effects.asm:647-671 +local U = require("tests.drivers.util") + +local Mon = require("src.battle.gen2.Mon") + +return function(game) + local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/crystal-sendout-pic" + local fails = 0 + local function ok(cond, line) + if not cond then fails = fails + 1 end + print("[sendout-pic] " .. (cond and "PASS " or "FAIL ") .. line) + end + + U.wait(45) + local world = game.world + assert(world and world.map, "the crystal world did not boot") + + local lead = Mon.new(game.data, "CYNDAQUIL", 12) + assert(lead and #lead.moves > 0, "could not build a CYNDAQUIL") + game.save.party = { lead } + game.save.inventory = { POKE_BALL = 5 } + + local wild = Mon.new(game.data, "PIDGEY", 6) + assert(world:startBattle({ wild = wild }), "startBattle failed") + local screen + for _ = 1, 600 do + local top = game.stack:top() + if top and top.battle then screen = top break end + U.wait(1) + end + assert(screen and screen.battle, "battle screen never came up") + + for _ = 1, 600 do + if screen.message == "Wild PIDGEY appeared!" and screen.typer + and screen.typer:done() then + break + end + U.wait(1) + end + ok(screen.message == "Wild PIDGEY appeared!", "the appeared line is up") + U.shot(game, out .. "/000-appeared.png") + + U.tap(game, "a") + + local function state() + local anim = screen.anim + local size = anim and anim.bg and anim.bg.picSize.player + return { + slide = screen.backpicSlide, + trainer = screen.showPlayerTrainer, + cleared = screen:picBoxCleared("player"), + message = screen.message, + typed = screen.typer and screen.typer:done(), + animFrames = anim and anim.frames or nil, + size = size, + pending = screen.afterSendOut ~= nil, + hud = screen.showPlayerHud, + phase = screen.phase, + } + end + + local log = {} + local slideStart, slideEnd, goFirst, revealAt, hudAt, menuAt + local clearedDuringGo, trainerDuringSlide = true, true + local hudBeforeReveal, revealSize, revealTyped = false, nil, nil + for frame = 1, 360 do + local s = state() + log[#log + 1] = ("%03d t=%d slide=%s trainer=%s cleared=%s msg=%s typed=%s " + .. "anim=%s size=%s pending=%s hud=%s phase=%s"):format(frame, U.frame(), + tostring(s.slide), tostring(s.trainer), tostring(s.cleared), + tostring(s.message), tostring(s.typed), tostring(s.animFrames), + tostring(s.size), tostring(s.pending), tostring(s.hud), s.phase) + if s.slide then + slideStart = slideStart or U.frame() - s.slide + trainerDuringSlide = trainerDuringSlide and s.trainer + elseif slideStart and not slideEnd then + slideEnd = U.frame() + end + if s.message == "Go! CYNDAQUIL!" and not goFirst then goFirst = frame end + if goFirst and not revealAt then + if not s.cleared then + revealAt = frame + revealSize = s.size + revealTyped = s.typed + if not s.typed then clearedDuringGo = false end + end + end + if s.hud and not hudAt then hudAt = frame end + if s.hud and not revealAt then hudBeforeReveal = true end + if s.phase == "menu" and not menuAt then menuAt = frame break end + U.shot(game, out .. ("/%03d.png"):format(frame)) + end + local f = io.open(out .. "/frames.log", "w") + if f then f:write(table.concat(log, "\n"), "\n") f:close() end + + local slideFrames = (slideStart and slideEnd) and (slideEnd - slideStart) or -1 + ok(slideFrames >= 18 and slideFrames <= 20, + "the back pic slides out over 18 frames: " .. slideFrames) + ok(trainerDuringSlide, "and it is the trainer sliding, not the mon") + ok(goFirst ~= nil, "Go! CYNDAQUIL! comes up at shot " .. tostring(goFirst)) + ok(goFirst ~= nil and slideEnd ~= nil, "after the slide") + ok(clearedDuringGo, "the box stays empty while the line types") + ok(revealAt ~= nil, "the mon appears at frame " .. tostring(revealAt)) + ok(revealTyped, "after the line finished typing") + ok(revealSize == 2, "as EnterMon's 2x2 square: size " .. tostring(revealSize)) + ok(not hudBeforeReveal, "the HUD is not up before the mon") + ok(hudAt ~= nil and revealAt ~= nil and hudAt > revealAt, + "the HUD comes up at frame " .. tostring(hudAt) .. ", after the anim") + ok(menuAt ~= nil, "the menu follows with no A press at frame " + .. tostring(menuAt)) + + print("[sendout-pic] " .. (fails == 0 and "PASS all claims" or + (fails .. " claims failed")) .. " -- shots in " .. out) + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/drivers/crystal_softboiled_field_bug2041.lua b/tests/drivers/crystal_softboiled_field_bug2041.lua new file mode 100644 index 00000000..a46a0559 --- /dev/null +++ b/tests/drivers/crystal_softboiled_field_bug2041.lua @@ -0,0 +1,121 @@ +-- ../pokecrystal/engine/pokemon/mon_menu.asm:724 MonMenu_Softboiled_MilkDrink +-- ../pokecrystal/engine/items/item_effects.asm:1986 Softboiled_MilkDrinkFunction +-- POKEPORT_IDENTITY=crystal-aug31 POKEPORT_VERSION=crystal POKEPORT_TOUCH=0 \ +-- POKEPORT_DRIVER=tests/drivers/crystal_softboiled_field_bug2041.lua \ +-- POKEPORT_SHOT_DIR=/tmp/sb2041 +local U = require("tests.drivers.util") + +local Mon = require("src.battle.gen2.Mon") + +return function(game) + local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/sb2041" + local fails = 0 + local function ok(cond, msg) + if not cond then fails = fails + 1 end + print("[2041] " .. (cond and "ok " or "FAIL ") .. msg) + return cond + end + local function tap(btn) U.tap(game, btn) U.wait(3) end + local function top() return game.stack:top() end + local function topId() + local state = top() + return state and state.screenId or nil + end + local function waitFor(id, limit) + for _ = 1, limit or 120 do + if topId() == id then return true end + U.wait(1) + end + return topId() == id + end + + U.wait(45) + local world, save, data = game.world, game.save, game.data + if not ok(world and world.map, "booted into the overworld") then + love.event.quit(1) + return + end + + local healer = Mon.new(data, "CHANSEY", 30) + Mon.learnMove(healer, "SOFTBOILED", data) + local patient = Mon.new(data, "GEODUDE", 20) + patient.hp = 1 + save.party = { healer, patient } + + world:warpToMapId("NEW_BARK_TOWN", 9, 8, "down") + U.wait(30) + for _ = 1, 240 do + if not world:busy() then break end + U.wait(2) + end + + waitFor(nil, 240) + local menu + for _ = 1, 10 do + tap("start") + menu = top() + if menu and menu.screenId == "Gen2StartMenu" then break end + U.wait(6) + end + if not ok(menu and menu.screenId == "Gen2StartMenu", "START opened the menu") then + love.event.quit(1) + return + end + for _ = 1, 10 do + if menu.list:current().value == "pokemon" then break end + tap("down") + end + tap("a") + waitFor("Gen2PartyMenu", 90) + local party = top() + if not ok(party and party.screenId == "Gen2PartyMenu", + "POKeMON opened the party list") then + love.event.quit(1) + return + end + tap("a") + local sub = party.submenu + if not ok(sub ~= nil, "A opened the action submenu") then + love.event.quit(1) + return + end + local row + for index, item in ipairs(sub.items) do + if item.id == "SOFTBOILED" then row = index end + end + if not ok(row ~= nil, "which lists SOFTBOILED") then + love.event.quit(1) + return + end + for _ = 1, #sub.items do + if sub.index == row then break end + tap("down") + end + tap("a") + U.wait(10) + ok(party.softboiledFrom == 1, "the list stayed up asking for a recipient") + ok(top() == party, "with no refusal box over it") + U.shot(game, out .. "/2041_pick_recipient.png") + + tap("down") + U.tap(game, "a") + -- ../pokecrystal/engine/items/item_effects.asm:1999 HealHP_SFX_GFX + local order = {} + for _ = 1, 90 do + local r = party.itemResult + if r and order[#order] ~= r.slot then order[#order + 1] = r.slot end + U.wait(1) + end + ok(order[1] == 1 and order[2] == 2, + "the user's bar drops before the recipient's climbs") + U.shot(game, out .. "/2041_recovered.png") + ok((save.party[2].hp or 0) > 1, "GEODUDE gained HP") + ok(save.party[1].hp < (save.party[1].stats and save.party[1].stats.hp or 0), + "and CHANSEY paid for it") + + print("[2041] eyeball 2041_pick_recipient.png (\"Use on which #MON?\") " + .. "and 2041_recovered.png (the HP bar climb + \"recovered NHP!\")") + print("[2041] " .. (fails == 0 and "all claims passed" + or (fails .. " claims failed"))) + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/drivers/crystal_trade_anim_shots.lua b/tests/drivers/crystal_trade_anim_shots.lua new file mode 100644 index 00000000..add80e28 --- /dev/null +++ b/tests/drivers/crystal_trade_anim_shots.lua @@ -0,0 +1,79 @@ +-- POKEPORT_IDENTITY=crystal-sep01 POKEPORT_VERSION=crystal \ +-- POKEPORT_DRIVER=tests/drivers/crystal_trade_anim_shots.lua \ +-- POKEPORT_SHOT_DIR=/tmp/crystal-trade love . +-- ../pokecrystal/engine/movie/trade_animation.asm:64-65, :772-800 +-- ../pokecrystal/engine/gfx/pic_animation.asm:72 +local U = require("tests.drivers.util") + +local Anim = require("src.core.gen2.TradeAnim") +local Mon = require("src.battle.gen2.Mon") +local Screens = require("src.ui.Screens") + +local FRAME_LIMIT = 900 + +return function(game) + local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/crystal-trade" + local give = os.getenv("POKEPORT_TRADE_GIVE") or "ABRA" + local get = os.getenv("POKEPORT_TRADE_GET") or "MACHOP" + local fails = 0 + local function ok(cond, line) + if not cond then fails = fails + 1 end + print("[tradeanim] " .. (cond and "PASS " or "FAIL ") .. line) + end + + U.wait(45) + assert(game.world and game.world.map, "crystal world did not boot") + + local given = Mon.new(game.data, give, 12) + local received = Mon.new(game.data, get, 12) + ok(game.data.pokemon[get] and game.data.pokemon[get].anim ~= nil, + get .. " has an anim row in this cache") + + local done = false + Screens.push(game, "Gen2TradeAnim", { + row = { otName = "MIKE", otId = 1234, get = get }, + given = given, received = received, save = game.save, + eventTables = game.data.events, + onDone = function() done = true end, + }) + local screen = game.stack:top() + assert(screen and screen.beat, "the trade screen did not open") + + -- ../pokecrystal/engine/movie/trade_animation.asm:61-63 + screen.frame = Anim.startOf("getmon_poof") - 1 + + local shots, animSteps, beatWhileAnim = 0, 0, {} + local sawAnim = false + for _ = 1, FRAME_LIMIT do + if done then break end + if screen.picAnim then + sawAnim = true + animSteps = animSteps + 1 + beatWhileAnim[screen.beat and screen.beat.id or "?"] = true + if animSteps % 3 == 1 then + shots = shots + 1 + U.shot(game, ("%s/anim-%03d.png"):format(out, shots)) + else + U.wait(1) + end + else + local id = screen.beat and screen.beat.id or "?" + U.shot(game, ("%s/beat-%s-%04d.png"):format(out, id, screen.frame)) + U.wait(3) + end + end + + ok(sawAnim, "AnimateFrontpic ran at getmon_hold") + ok(animSteps > 1, "and for more than one iteration (" .. animSteps .. ")") + local ids = {} + for id in pairs(beatWhileAnim) do ids[#ids + 1] = id end + ok(#ids == 1 and ids[1] == "getmon_hold", + "the script is held while the scene runs (" .. table.concat(ids, ",") + .. ")") + ok(done, "and the animation still reaches its end") + + print("[tradeanim] shots in " .. out) + print("[tradeanim] " .. (fails == 0 and "all claims passed" + or (fails .. " claims failed"))) + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/drivers/crystal_trainer_arrow_bug2045.lua b/tests/drivers/crystal_trainer_arrow_bug2045.lua new file mode 100644 index 00000000..912bd026 --- /dev/null +++ b/tests/drivers/crystal_trainer_arrow_bug2045.lua @@ -0,0 +1,51 @@ +local U = require("tests.drivers.util") + +-- ../pokecrystal/maps/Route30.asm:426 TrainerYoungsterMikey +local MIKEY_MAP, MIKEY_X, MIKEY_Y = "ROUTE_30", 5, 24 +local CYNDAQUIL = 155 + +local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/pokeport-shots" + +return function(game) + local fails = 0 + local function say(line) print("[2045] " .. line) end + local function ok(cond, line) + if not cond then fails = fails + 1 end + say((cond and "PASS " or "FAIL ") .. line) + end + + U.wait(60) + local world = game.world + if not (world and world.map and world.vm) then + say("FAIL the crystal world did not boot") + love.event.quit(1) + return + end + + game.save.party = {} + world.vm.givePokeFn(CYNDAQUIL, 10, 0, nil) + U.wait(5) + ok(game.save.party[1] ~= nil, "the player has a mon to battle with") + + world:warpToMapId(MIKEY_MAP, MIKEY_X, MIKEY_Y, "up") + U.wait(60) + ok(world.map and world.map.def and world.map.def.id == MIKEY_MAP, + "standing on Route 30 in front of Youngster Mikey") + + U.tap(game, "a") + U.wait(90) + + local top = game.stack and game.stack:top() + local isBox = top and top.isTextBox + ok(isBox, "the trainer's seen text is up") + if isBox then + ok(top.waitButton == true, "the box carries WaitButton semantics") + ok(not top:arrowVisible(), "and prints no blinking cursor") + end + U.shot(game, SHOT_DIR .. "/2045_trainer_seen.png") + say("eyeball 2045_trainer_seen.png: no arrow in the bottom-right cell, " + .. "and A starts the battle with no beep") + + say(fails == 0 and "all claims passed" or (fails .. " claims failed")) + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/drivers/evolution_black_bug279_test.lua b/tests/drivers/evolution_black_bug279_test.lua index ffb7eb9c..15d6865c 100644 --- a/tests/drivers/evolution_black_bug279_test.lua +++ b/tests/drivers/evolution_black_bug279_test.lua @@ -72,8 +72,16 @@ return function(game) return z and z[1] and z[1].colors, z and z[1] end + -- ../pokered/engine/movie/evolution.asm:25-26 + local graceCols = colorsFor({ + done = false, canceled = false, t = 0, + mon = { species = "MAGIKARP" }, newSpecies = "GYARADOS", + }) + check(samePal(graceCols, PaletteFX.monPal(game.data, "MAGIKARP")), + "during the 80-frame grace the OLD form wears its own palette (MAGIKARP/REDMON)") + -- ../pokered/engine/movie/evolution.asm:49-50 local flashCols, flashZone = colorsFor({ - done = false, canceled = false, + done = false, canceled = false, t = 100, mon = { species = "MAGIKARP" }, newSpecies = "GYARADOS", }) check(samePal(flashCols, BLACK), @@ -100,7 +108,7 @@ return function(game) -- other COLORS modes honest. A hardcoded {0,0,0} passes everything above. PaletteFX.setMode("ogred") local ogFlash = colorsFor({ - done = false, canceled = false, + done = false, canceled = false, t = 100, mon = { species = "WEEDLE" }, newSpecies = "KAKUNA", }) check(samePal(ogFlash, PaletteFX.ogBg()), @@ -199,6 +207,21 @@ return function(game) if st then st.t = 362 end end + -- evolution.asm:20-40 keeps the pic off screen until the load window ends + local function waitForPic() + return waitFor(function() + local st = evoTop() + return st ~= nil and st.loading == nil + end, 400) + end + -- evolution.asm:47-48: the cancel poll only opens after the 80-frame delay + local function waitPastGrace() + return waitFor(function() + local st = evoTop() + return st ~= nil and (st.t or 0) > 90 + end, 700) + end + local function startEvo(species, into) local mon = Pokemon.new(game.data, species, 7) table.insert(game.save.party, 1, mon) @@ -212,7 +235,10 @@ return function(game) -- ---- case 1: the reported case, SGB, mid-flash -------------------------- local weedle = startEvo("WEEDLE", "KAKUNA") - U.wait(24) -- into the flash, far short of FLASH_FRAMES = 368 + waitForPic() + -- ../pokered/engine/movie/evolution.asm:49-50 + waitFor(function() local e = evoTop() return e and (e.t or 0) > 80 end, 300) + U.wait(24) U.shot(game, DIR .. "/evo279_1_flash_sgb.png") local c1 = probe("flash/SGB", { monYellow1 = YELLOW[2], monYellow2 = YELLOW[3], @@ -248,7 +274,7 @@ return function(game) -- ---- case 2: B-cancel settles on the OLD form's colours ----------------- local karp = startEvo("MAGIKARP", "GYARADOS") - U.wait(100) -- past the 80-frame pre-animLoop delay before the poll starts + waitPastGrace() U.hold(game, "b", 20) -- evolution.asm Evolution_CheckForCancel if not waitFor(function() return findText("stopped evolving") ~= nil end, 240) then check(false, "holding B prints \"stopped evolving\"") @@ -276,6 +302,8 @@ return function(game) PaletteFX.setMode("ogred") U.wait(20) startEvo("WEEDLE", "KAKUNA") + waitForPic() + waitFor(function() local e = evoTop() return e and (e.t or 0) > 80 end, 300) U.wait(24) U.shot(game, DIR .. "/evo279_4_flash_ogred.png") local c4 = probe("flash/OG RED", { diff --git a/tests/drivers/evolution_cancel_bug213_test.lua b/tests/drivers/evolution_cancel_bug213_test.lua index db71c43a..1f57216c 100644 --- a/tests/drivers/evolution_cancel_bug213_test.lua +++ b/tests/drivers/evolution_cancel_bug213_test.lua @@ -38,6 +38,11 @@ return function(game) end return false end + -- evolution.asm:20-48: the pic load and the old cry both run before the poll + local function pastGrace() + local t = top() + return t and t.screenId == "EvolutionState" and (t.t or 0) > 90 + end -- flatten a TextBox's paginated pages (list of line lists) to one string local function pagesText(st) if not st.pages then return nil end @@ -81,7 +86,9 @@ return function(game) Evolution.evolve(game, mon, "METAPOD", function() done1 = true end) if not waitFor(evoTop, 300) then error("EvolutionState never opened (case1)") end - U.wait(100) -- past the 80-frame pre-animLoop delay, still under 368 + if not waitFor(pastGrace, 600) then + error("the flash never reached the cancel poll (case1)") + end U.log("case1 flash", "t=", top().t, "species=", mon.species) U.shot(game, DIR .. "/evo213_1_evolving.png") @@ -119,7 +126,9 @@ return function(game) if not waitFor(evoTop, 300) then error("EvolutionState never opened after level-up re-offer") end - U.wait(100) -- past the same 80-frame pre-animLoop delay + if not waitFor(pastGrace, 600) then + error("the re-offered flash never reached the cancel poll") + end U.hold(game, "b", 20) -- cancel so case 2 stays independent if not waitFor(function() return not evoTop() end, 240) then error("level-up re-offer did not abort on B") @@ -133,8 +142,7 @@ return function(game) local done2 = false Evolution.evolve(game, mon2, "METAPOD", function() done2 = true end) if not waitFor(evoTop, 300) then error("EvolutionState never opened (case2)") end - -- let the full flash run (FLASH_FRAMES=368) without pressing B - waitFor(function() return not evoTop() end, 500) + waitFor(function() return not evoTop() end, 900) if not waitFor(function() return findText("evolved into") ~= nil end, 120) then error("Congratulations text not shown (case2)") end diff --git a/tests/drivers/evolution_flip_bug1412_test.lua b/tests/drivers/evolution_flip_bug1412_test.lua index dfa978c4..4e0d2f15 100644 --- a/tests/drivers/evolution_flip_bug1412_test.lua +++ b/tests/drivers/evolution_flip_bug1412_test.lua @@ -32,9 +32,14 @@ return function(game) end check("the evolution screen opened", top and top.screenId == "EvolutionState") + -- engine/movie/evolution.asm:20-40 holds the pic off screen first + for _ = 1, 400 do + if top.loading == nil then break end + U.wait(1) + end U.shot(game, DIR .. "/bug1412_evo_old_pikachu.png") - for _ = 1, 600 do + for _ = 1, 900 do if top.done then break end U.wait(1) end diff --git a/tests/drivers/evolution_move_bug12_test.lua b/tests/drivers/evolution_move_bug12_test.lua index 731690a1..30239644 100644 --- a/tests/drivers/evolution_move_bug12_test.lua +++ b/tests/drivers/evolution_move_bug12_test.lua @@ -89,8 +89,7 @@ return function(game) Evolution.evolve(game, mon, "GYARADOS", function() done1 = true end) if not waitFor(evoTop, 300) then error("EvolutionState never opened (case1)") end - -- let the full flash run (FLASH_FRAMES=368) with no input, then apply - waitFor(function() return not evoTop() end, 500) + waitFor(function() return not evoTop() end, 900) if not waitFor(function() return findText("evolved into") ~= nil end, 120) then error("Congratulations text not shown (case1)") end @@ -124,7 +123,7 @@ return function(game) local done2 = false Evolution.evolve(game, mon2, "GYARADOS", function() done2 = true end) if not waitFor(evoTop, 300) then error("EvolutionState never opened (case2)") end - waitFor(function() return not evoTop() end, 500) + waitFor(function() return not evoTop() end, 900) if not waitFor(function() return findText("evolved into") ~= nil end, 120) then error("Congratulations text not shown (case2)") end diff --git a/tests/drivers/evolution_timing_bug2033.lua b/tests/drivers/evolution_timing_bug2033.lua new file mode 100644 index 00000000..e6717acb --- /dev/null +++ b/tests/drivers/evolution_timing_bug2033.lua @@ -0,0 +1,103 @@ +-- pokered engine/movie/evolution.asm:12-19 stops the music and plays +-- and PlayCry blocks (home/pokemon.asm:145-149), so EvolvedText +-- (engine/pokemon/evos_moves.asm:136) cannot print until the new cry ends. +-- SHOT_DIR=/tmp/evo2033 POKEPORT_DRIVER=tests/drivers/evolution_timing_bug2033.lua \ +-- POKEPORT_IDENTITY=bug2033 POKEPORT_TOUCH=0 love . +return function(game) + local U = dofile("tests/drivers/util.lua") + local DIR = os.getenv("SHOT_DIR") or os.getenv("POKEPORT_SHOT_DIR") + or "/tmp/evo2033" + os.execute("mkdir -p " .. DIR) + + local Pokemon = require("src.pokemon.Pokemon") + local Evolution = require("src.pokemon.Evolution") + + game.save.options = game.save.options or {} + game.save.options.textSpeed = 1 + + local function top() return game.stack:top() end + local function evoState() + for _, st in ipairs(game.stack.states or {}) do + if st.screenId == "EvolutionState" then return st end + end + return nil + end + local function waitFor(cond, max) + for _ = 1, max or 600 do + if cond() then return true end + U.wait(1) + end + return false + end + local function pagesText(st) + if not st or not st.pages then return nil end + local parts = {} + for _, page in ipairs(st.pages) do + if type(page) == "table" then + for _, line in ipairs(page) do + if type(line) == "string" then parts[#parts + 1] = line end + end + elseif type(page) == "string" then + parts[#parts + 1] = page + end + end + return table.concat(parts, " ") + end + local function findText(needle) + for _, st in ipairs(game.stack.states or {}) do + local blob = pagesText(st) + if blob and blob:find(needle, 1, true) then return st end + end + return nil + end + + U.teleport(game, "ROUTE_1", 5, 5, "down") + U.wait(10) + + local mon = Pokemon.new(game.data, "PARAS", 23) + table.insert(game.save.party, 1, mon) + Evolution.evolve(game, mon, "PARASECT", nil, "LEVEL") + + if not waitFor(function() return evoState() ~= nil end, 300) then + error("EvolutionState never opened") + end + local evo = evoState() + + U.shot(game, DIR .. "/evo2033_1_clear.png") + U.log("head", "loading=", evo.loading, "t=", evo.t) + waitFor(function() return (evo.loading or 0) <= 42 end, 200) + U.shot(game, DIR .. "/evo2033_2_blank42.png") + waitFor(function() return (evo.loading or 0) <= 2 end, 200) + U.shot(game, DIR .. "/evo2033_3_blank_last.png") + U.log("one frame short of the pic", "loading=", evo.loading) + waitFor(function() return evo.loading == nil end, 200) + U.shot(game, DIR .. "/evo2033_4_pic_and_cry.png") + U.log("pic up", "loading=", evo.loading, "cryWait=", evo.cryWait) + + if not waitFor(function() return evo.done end, 900) then + error("the flash never finished") + end + U.shot(game, DIR .. "/evo2033_5_settled.png") + U.log("flash end", "species=", mon.species, "endCryWait=", evo.endCryWait) + U.wait(50) + U.shot(game, DIR .. "/evo2033_6_cry_alone.png") + U.log("50 frames into the new cry", "topIsEvo=", top() == evo, + "evolvedText=", findText("evolved") ~= nil) + + if not waitFor(function() return findText("evolved") ~= nil end, 400) then + error("the evolved-into text never printed") + end + U.wait(70) + U.shot(game, DIR .. "/evo2033_7_evolved_text.png") + + U.log("shots in", DIR) + U.log("Right: shots 1-3 show a blank white field with the \"is evolving!\"") + U.log("box still up (a Tink sounds on shot 1); shot 4 has the PARAS pic and") + U.log("its cry together; shot 6 still has the settled PARASECT under the OLD") + U.log("box, silent but for its cry; shot 7 finally types \"evolved into\".") + U.log("Wrong: the pic is up on shot 1, or the text is typing on shot 6.") + + while true do + coroutine.yield() + end +end diff --git a/tests/drivers/fossil_gift_bug2034_test.lua b/tests/drivers/fossil_gift_bug2034_test.lua new file mode 100644 index 00000000..77907540 --- /dev/null +++ b/tests/drivers/fossil_gift_bug2034_test.lua @@ -0,0 +1,90 @@ +-- ../pokered/scripts/CinnabarLabFossilRoom.asm:74-83 +-- ../pokered/engine/events/give_pokemon.asm:52-71 +-- POKEPORT_DRIVER=tests/drivers/fossil_gift_bug2034_test.lua \ +-- POKEPORT_IDENTITY=red-aug28 POKEPORT_VERSION=red POKEPORT_TOUCH=0 \ +-- SHOT_DIR=/tmp/shots love . +return function(game) + local U = dofile("tests/drivers/util.lua") + local DIR = os.getenv("SHOT_DIR") or "/tmp/shots" + + local function check(label, ok) + U.log(ok and "PASS" or "FAIL", label) + return ok + end + + local function boxText() + local t = game.stack:top() + if not (t and t.pages) then return nil end + local out = {} + for _, page in ipairs(t.pages) do + for _, line in ipairs(page) do out[#out + 1] = line end + end + return table.concat(out, " ") + end + + local function mashUntil(cond, tries) + for _ = 1, tries or 200 do + if cond() then return true end + U.tap(game, "a") + U.wait(4) + end + return false + end + + U.teleport(game, "CINNABAR_LAB_FOSSIL_ROOM", 5, 3, "up") + local ow = game.overworld + for _, npc in ipairs(ow.npcs) do + if npc.def and npc.def.text == "TEXT_CINNABARLABFOSSILROOM_SCIENTIST1" then + npc.wanders, npc.moving = false, false + npc.cellX, npc.cellY = 5, 2 + npc.px, npc.py = npc.cellX * 16, npc.cellY * 16 + npc.facing = "down" + end + end + + game.save.party = {} + game.save.labFossilMon = "AERODACTYL" + game.save.flags.EVENT_GAVE_FOSSIL_TO_LAB = true + game.save.flags.EVENT_LAB_STILL_REVIVING_FOSSIL = nil + game.save.flags.EVENT_LAB_HANDING_OVER_FOSSIL_MON = nil + U.wait(5) + + U.tap(game, "a") + U.wait(20) + U.shot(game, DIR .. "/fossil2034_1_backtolife.png") + + local sawGot = mashUntil(function() + local t = boxText() + return t and t:find("AERODACTYL", 1, true) and t:find("got", 1, true) + end) + check("_GotMonText prints for the revived fossil", sawGot) + U.wait(40) + U.shot(game, DIR .. "/fossil2034_2_gotmon.png") + + local sawNick = mashUntil(function() + local t = boxText() + return t and t:find("nickname", 1, true) ~= nil + end) + check("and AddPartyMon asks for a nickname", sawNick) + U.wait(60) + U.shot(game, DIR .. "/fossil2034_3_nickname.png") + + U.tap(game, "b") + mashUntil(function() return game.stack:top() == ow end) + U.shot(game, DIR .. "/fossil2034_4_after.png") + + check("AERODACTYL is in the party", + game.save.party[1] and game.save.party[1].species == "AERODACTYL") + check("at level 30 (`ld c, 30`)", + game.save.party[1] and game.save.party[1].level == 30) + check("the quest reset", not game.save.flags.EVENT_GAVE_FOSSIL_TO_LAB + and game.save.labFossilMon == nil) + check("and nothing is left over the overworld", game.stack:top() == ow) + + U.log("Shots: " .. DIR .. "/fossil2034_*.png") + U.log("Shot 2 must read 'RED got / AERODACTYL!' and shot 3 the nickname") + U.log("prompt. Broken showed neither: the mon appeared in the party with") + U.log("no line at all, straight from the back-to-life text.") + U.log("DONE") + love.event.quit() +end diff --git a/tests/drivers/gen2_pc_save_overlay_bug2049.lua b/tests/drivers/gen2_pc_save_overlay_bug2049.lua new file mode 100644 index 00000000..da80f442 --- /dev/null +++ b/tests/drivers/gen2_pc_save_overlay_bug2049.lua @@ -0,0 +1,120 @@ +-- ../pokecrystal/engine/events/pokecenter_pc.asm:15 (#2049) +-- ../pokecrystal/engine/menus/save.asm:1 (#2053) +local U = require("tests.drivers.util") + +local POKECENTER = os.getenv("POKEPORT_PC_MAP") or "CHERRYGROVE_POKECENTER_1F" +local COLL_PC = 0x93 + +return function(game) + local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gen2-pc-overlay" + local failed = 0 + + local function pass(ok, line) + if not ok then failed = failed + 1 end + U.log((ok and "PASS " or "FAIL ") .. line) + end + + -- ../pokecrystal/home/print_text.asm:59 + local function settle() + for _ = 1, 600 do + local top = game.stack:top() + local typer = top and top.typer + if not typer or typer:done() then break end + U.wait(1) + end + end + + local function tap(btn, frames) + settle() + U.tap(game, btn) + U.wait(frames or 6) + settle() + end + + local function topId() + local top = game.stack:top() + return top and top.screenId or nil + end + + U.wait(45) + local world = game.world + assert(world and world.map, "gen2 world did not boot") + + local Mon = require("src.battle.gen2.Mon") + game.save.party = { Mon.new(game.data, "CYNDAQUIL", 10) } + + world:warpToMapId(POKECENTER, 4, 4, "up") + U.wait(20) + + local pcX, pcY + for cy = 0, world.map.heightCells - 1 do + for cx = 0, world.map.widthCells - 1 do + if world.map:cellCollision(cx, cy) == COLL_PC then pcX, pcY = cx, cy end + end + end + assert(pcX, "no COLL_PC tile on " .. POKECENTER) + world:warpToMapId(POKECENTER, pcX, pcY + 1, "up") + U.wait(20) + + tap("a", 10) + pass(topId() == "Gen2CenterPcMenu", + "A on the PC tile opens the whose-PC screen (top: " + .. tostring(topId()) .. ")") + local pc = game.stack:top() + pass(pc ~= nil and pc.isOpaque == false, + "PokemonCenterPC is a window, not an opaque page") + + U.log("01-turn-on: ' turned on the PC.' in the bottom box, with the") + U.log("Pokecenter floor, the counter and the player sprite still drawn") + U.log("above it. A pure white field above the box is the bug.") + U.shot(game, out .. "/01-turn-on.png") + + tap("a", 10) + U.log("02-whose-pc: the whose-PC list over 'Access whose PC?', with the map") + U.log("still showing in the four tile columns right of the menu box.") + U.shot(game, out .. "/02-whose-pc.png") + + tap("a", 10) + U.log("03-bills-accessed: \"BILL's PC accessed.\" prints WITH the whose-PC") + U.log("menu box still standing above it, and the map beside it.") + U.shot(game, out .. "/03-bills-accessed.png") + + for _ = 1, 8 do + if topId() == nil then break end + tap("b", 12) + end + pass(topId() == nil, "B backs all the way out of the PC (top: " + .. tostring(topId()) .. ")") + + world:warpToMapId(POKECENTER, pcX, pcY + 2, "down") + U.wait(20) + tap("start", 12) + U.log("04-start-menu: the START menu over the Pokecenter.") + U.shot(game, out .. "/04-start-menu.png") + + local Screens = require("src.ui.Screens") + Screens.push(game, "Gen2SaveMenu", { + save = game.save, + existed = false, + writer = function() return true end, + onDone = function() game.stack:pop() end, + }) + U.wait(8) + local saveMenu = game.stack:top() + pass(saveMenu ~= nil and saveMenu.isOpaque == false, + "SaveMenu is a window over the tilemap, not an opaque page") + settle() + U.log("05-save-prompt: the continue panel, the YES/NO box and 'Would you") + U.log("like to save the game?' over the LIVE map, with the START menu's own") + U.log("rows still visible in the gap between them.") + U.shot(game, out .. "/05-save-prompt.png") + + tap("a", 10) + U.log("06-saving: 'SAVING... DON'T TURN OFF THE POWER.' up, map") + U.log("still behind it.") + U.shot(game, out .. "/06-saving.png") + + U.log(("%d check(s) failed"):format(failed)) + + while true do coroutine.yield() end +end diff --git a/tests/drivers/gen2_save_order_s5.lua b/tests/drivers/gen2_save_order_s5.lua new file mode 100644 index 00000000..06c88366 --- /dev/null +++ b/tests/drivers/gen2_save_order_s5.lua @@ -0,0 +1,102 @@ +-- ../pokecrystal/engine/menus/save.asm:242 SavedTheGame +-- POKEPORT_IDENTITY=crystal-sep01 POKEPORT_VERSION=crystal POKEPORT_TOUCH=0 \ +-- POKEPORT_DRIVER=tests/drivers/gen2_save_order_s5.lua \ +-- POKEPORT_SHOT_DIR=/tmp/save-order love . + +local U = require("tests.drivers.util") + +local POKECENTER = os.getenv("POKEPORT_PC_MAP") or "CHERRYGROVE_POKECENTER_1F" + +return function(game) + local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/save-order" + local failed = 0 + local function pass(ok, line) + if not ok then failed = failed + 1 end + U.log((ok and "PASS " or "FAIL ") .. line) + end + + U.wait(45) + local world = game.world + if not (world and world.map) then + U.log("FAIL the world did not boot") + love.event.quit(1) + return + end + world:warpToMapId(POKECENTER, 5, 5, "down") + U.wait(30) + + local Sound = require("src.core.Sound") + local Screens = require("src.ui.Screens") + local realPlay = Sound.play + local frames, wrote, rang = 0, nil, nil + Sound.play = function(data, name) + if name and tostring(name):find("Save") then rang = frames end + return realPlay(data, name) + end + + local closed = nil + Screens.push(game, "Gen2SaveMenu", { + save = game.save, + existed = false, + writer = function() wrote = frames return true end, + onDone = function() closed = frames game.stack:pop() end, + }) + U.wait(8) + local menu = game.stack:top() + + local function step() + frames = frames + 1 + U.wait(1) + end + local function settle() + for _ = 1, 600 do + if not menu.typer or menu.typer:done() then break end + step() + end + end + + settle() + U.tap(game, "a") + U.wait(4) + settle() + pass(menu.phase == "saving", "YES opens SavingDontTurnOffThePower") + U.shot(game, out .. "/01-saving-line.png") + + local typedAt = frames + for _ = 1, 200 do + if wrote then break end + step() + end + pass(wrote ~= nil, "the write landed") + U.log(("write landed %d frames after the SAVING line"):format( + (wrote or 0) - typedAt)) + pass(menu.phase == "saving", "and the SAVING page is still up at the write") + U.shot(game, out .. "/02-write-frame.png") + + for _ = 1, 200 do + if menu.phase ~= "saving" then break end + step() + end + U.log(("the saved line came up %d frames after the write"):format( + frames - (wrote or 0))) + pass(rang == nil, "no chime before the line") + U.shot(game, out .. "/03-saved-line-starts.png") + + settle() + step() + U.shot(game, out .. "/04-saved-line-and-chime.png") + pass(rang ~= nil, "SFX_SAVE rings under the saved line") + + for _ = 1, 400 do + if closed then break end + step() + end + pass(closed ~= nil, "the box closes") + U.log(("and closes %d frames after the chime"):format( + (closed or 0) - (rang or 0))) + U.shot(game, out .. "/05-closed.png") + + Sound.play = realPlay + U.log(("%d check(s) failed"):format(failed)) + love.event.quit(failed == 0 and 0 or 1) +end diff --git a/tests/drivers/gen2_text_speed_bug2046.lua b/tests/drivers/gen2_text_speed_bug2046.lua new file mode 100644 index 00000000..fd74a3b9 --- /dev/null +++ b/tests/drivers/gen2_text_speed_bug2046.lua @@ -0,0 +1,93 @@ +-- ../pokecrystal/home/print_text.asm:1 PrintLetterDelay (#2046) +local U = require("tests.drivers.util") + +local POKECENTER = os.getenv("POKEPORT_PC_MAP") or "CHERRYGROVE_POKECENTER_1F" +local COLL_PC = 0x93 + +return function(game) + local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gen2-text-speed" + local failed = 0 + + local function pass(ok, line) + if not ok then failed = failed + 1 end + U.log((ok and "PASS " or "FAIL ") .. line) + end + + local function tap(btn, frames) + U.tap(game, btn) + U.wait(frames or 6) + end + + local function shown() + local top = game.stack:top() + local typer = top and top.typer + if not typer then return nil end + return typer.shown, typer.total + end + + U.wait(45) + local world = game.world + assert(world and world.map, "gen2 world did not boot") + + game.save.options = game.save.options or {} + game.save.options.textSpeed = "SLOW" + + local Mon = require("src.battle.gen2.Mon") + game.save.party = { Mon.new(game.data, "CYNDAQUIL", 10) } + + world:warpToMapId(POKECENTER, 4, 4, "up") + U.wait(20) + local pcX, pcY + for cy = 0, world.map.heightCells - 1 do + for cx = 0, world.map.widthCells - 1 do + if world.map:cellCollision(cx, cy) == COLL_PC then pcX, pcY = cx, cy end + end + end + assert(pcX, "no COLL_PC tile on " .. POKECENTER) + world:warpToMapId(POKECENTER, pcX, pcY + 1, "up") + U.wait(20) + + U.tap(game, "a") + U.wait(2) + local top = game.stack:top() + pass(top ~= nil and top.screenId == "Gen2CenterPcMenu", + "the PC opened (top: " .. tostring(top and top.screenId) .. ")") + + U.wait(10) + local a = shown() + U.shot(game, out .. "/01-typing-early.png") + U.wait(30) + local b = shown() + U.shot(game, out .. "/02-typing-later.png") + pass(a ~= nil and b ~= nil and b > a, + ("the turn-on line is still filling in (%s -> %s glyphs)") + :format(tostring(a), tostring(b))) + + -- ../pokecrystal/home/print_text.asm:59 + U.tap(game, "a") + U.wait(2) + local p, ptotal = shown() + pass(p ~= nil and ptotal ~= nil and p < ptotal, + ("A mid-line does not dump the page (%s of %s glyphs)") + :format(tostring(p), tostring(ptotal))) + U.shot(game, out .. "/02b-press-mid-line.png") + + U.wait(120) + local c, total = shown() + pass(c ~= nil and total ~= nil and c == total, + "and it finishes on its own at SLOW") + U.shot(game, out .. "/03-typed.png") + + U.log("01/02: the same page part-printed at two different lengths -- proof") + U.log("the OPTION text speed reaches the PC box. 03: the whole line, with") + U.log("the Pokecenter still drawn behind it.") + + tap("a", 10) + U.log("04-whose-pc: 'Access whose PC?' is PC_DisplayTextWaitMenu, which sets") + U.log("NO_TEXT_SCROLL -- this one is meant to be instant.") + U.shot(game, out .. "/04-whose-pc.png") + + U.log(("%d check(s) failed"):format(failed)) + + while true do coroutine.yield() end +end diff --git a/tests/drivers/gold_center_pc.lua b/tests/drivers/gold_center_pc.lua index 7a089570..a752f6bf 100644 --- a/tests/drivers/gold_center_pc.lua +++ b/tests/drivers/gold_center_pc.lua @@ -25,12 +25,24 @@ return function(game) return cond end + -- ../pokecrystal/home/print_text.asm:59 + local function settle() + for _ = 1, 600 do + local top = game.stack:top() + local typer = top and top.typer + if not typer or typer:done() then break end + U.wait(1) + end + end + local function tap(button, frames) + settle() game.input.pressQueue[#game.input.pressQueue + 1] = button game.input.state[button] = true U.wait(2) game.input.state[button] = false U.wait(frames or 4) + settle() end U.wait(45) diff --git a/tests/drivers/gold_dig_warp.lua b/tests/drivers/gold_dig_warp.lua index 0ac4c3c2..a77ada33 100644 --- a/tests/drivers/gold_dig_warp.lua +++ b/tests/drivers/gold_dig_warp.lua @@ -72,7 +72,12 @@ return function(game) end U.tap(game, "a") U.wait(3) - local pack = game.stack:top() + local pack + for _ = 1, 120 do + pack = game.stack:top() + if pack and pack.rows then break end + U.wait(1) + end assert(pack and pack.rows, "PACK did not open") return pack end @@ -136,7 +141,8 @@ return function(game) end) local screen = game.stack:top() assert(screen and screen.battle, "the rod's bite did not push a battle") - assert(screen.battle.battleType == "fish", + assert(screen.battle.battleType + == require("src.battle.gen2.Battle").BATTLETYPE_FISH, "the rod's battle carries battleType " .. tostring(screen.battle.battleType)) U.log("PASS fishing: the rod's encounter carries BATTLETYPE_FISH") diff --git a/tests/drivers/gold_evolution_shots.lua b/tests/drivers/gold_evolution_shots.lua index 35de3e5d..f9f94d8a 100644 --- a/tests/drivers/gold_evolution_shots.lua +++ b/tests/drivers/gold_evolution_shots.lua @@ -43,10 +43,8 @@ return function(game) .. " has no evolution to show -- pick another with POKEPORT_EVO_SPECIES") print(("[driver] %s -> %s"):format(species, entry.into)) - -- One run of the screen, shooting every `interval` frames. `cancelAt` is - -- the frame to tap B on, which .WaitFrames_CheckPressedB only honours during -- a hold; nil runs it through to the end. - local function run(prefix, cancelAt) + local function run(prefix, cancelPhase) game.save.party = { Mon.new(game.data, species, level) } local finished = nil local screen = EvolutionAnim.new(game, { @@ -59,13 +57,14 @@ return function(game) }) game.stack:push(screen) - local frame = 0 + local frame, tapped = 0, false while not finished and frame < FRAME_LIMIT do if frame % interval == 0 then U.shot(game, ("%s/%s-%04d-%s.png"):format(out, prefix, frame, screen.phase or "?")) end - if cancelAt and frame == cancelAt then + if cancelPhase and not tapped and screen.phase == cancelPhase then + tapped = true U.tap(game, "b") else U.wait(1) @@ -85,11 +84,8 @@ return function(game) assert(game.save.party[1].species == entry.into, "the party slot did not take the new species") - -- B during the very first hold, which is the 16 frames after the 50 + 80 of - -- text and music: .cancel_evo leaves the OLD pic on screen and prints - -- StoppedEvolvingText. - local canceled = run("cancel", - Evolution.EVOLVING_FRAMES + Evolution.MUSIC_FRAMES + 4) + -- ../pokecrystal/engine/movie/evolution_animation.asm:234-251 + local canceled = run("cancel", "flash") assert(canceled.canceled, "the B press did not cancel the evolution") assert(game.save.party[1].species == species, "a cancelled evolution changed the species anyway") diff --git a/tests/drivers/gold_exp_bar.lua b/tests/drivers/gold_exp_bar.lua index b3f15e3e..d0d4ff8c 100644 --- a/tests/drivers/gold_exp_bar.lua +++ b/tests/drivers/gold_exp_bar.lua @@ -32,11 +32,9 @@ return function(game) local player = Mon.new(game.data, "CYNDAQUIL", 9) assert(player and #player.moves > 0, "could not build a CYNDAQUIL") - -- One point short of the next level, so the single kill below crosses it and - -- the bar has to fill, restart at zero and finish the second segment. local def = game.data.pokemon[player.species] local growth = game.data.pokemon.growthRates[def.growthRate] - player.experience = Mon.experienceForLevel(growth, player.level + 1) - 1 + player.experience = Mon.experienceForLevel(growth, player.level + 1) - 40 -- Red bar on the way in, so the siren is up before the faint. player.hp = 3 game.save.party = { player } @@ -85,52 +83,87 @@ return function(game) print("[driver] crawl armed at level " .. tostring(screen.shownLevel) .. ", bar at " .. tostring(screen.shownExp) .. "/64") - -- Four stills across the crawl. A bar that is already full in the first is - -- the bug this driver exists for. - local seen, burst, spread = {}, nil, false - local function watchBurst() - local frame = screen.expBurst and screen.expBurst.frame - if not frame then return end - if not burst then - burst = { screen.shownExp, screen.shownLevel, screen.message } - U.shot(game, out .. "/05b-end-of-bar-burst.png") - elseif not spread and frame >= 5 then - spread = true - U.shot(game, out .. "/05c-burst-spread.png") + -- ../pokecrystal/engine/battle/core.asm:7121-7128 + local function printed() + for _ = 1, 400 do + if not screen:syncTyper() then return true end + U.wait(1) end + return false end - while screen.expAnim and shots < 4 do + assert(printed(), "the gained-EXP line never finished printing") + U.shot(game, out .. "/01b-gained-prompt.png") + U.tap(game, "a") + U.wait(1) + assert((screen.messageTimer or 0) == 0, + "the A on the gained-EXP line did not page it") + + local seen = {} + while screen.expAnim and shots < 4 and (screen.shownExp or 0) < 62 do shots = shots + 1 seen[shots] = { screen.shownExp, screen.shownLevel } U.shot(game, out .. ("/%02d-crawl.png"):format(shots + 1)) - for _ = 1, 18 do - watchBurst() + for _ = 1, 10 do + if (screen.shownExp or 0) >= 62 then break end U.wait(1) end end - for _ = 1, 900 do - if burst or not screen.expAnim then break end - watchBurst() - U.wait(1) - end for i = 1, shots do print(("[driver] shot %d: bar %s/64, :L%s") :format(i + 1, tostring(seen[i][1]), tostring(seen[i][2]))) end assert(shots >= 2, "the crawl was over before two frames could be shot") assert(seen[1][1] < 64, "the bar was already full on the first crawl frame") - assert(burst, "the end-of-bar burst never armed on the level crossing") - print(("[driver] burst: bar %s/64, :L%s, message %q") - :format(tostring(burst[1]), tostring(burst[2]), tostring(burst[3]))) - assert(not tostring(burst[3] or ""):find("grew to level"), - "the grew-to-level line was already up while the sparks were on screen") - -- The rest of the queue: the grew-to-level line and the way out. + local logOnly = os.getenv("POKEPORT_EXP_BAR_LOG_ONLY") ~= nil + local function snap(path) + if logOnly then U.wait(1) return end + game.capturePath = path + for _ = 1, 120 do + U.wait(1) + if not game.capturePath then return end + end + end + local n, sparks, sparkLine, lineAt, zeroAt = 0, {}, nil, nil, nil + for _ = 1, 3000 do + if screen.phase == "stats-box" or screen.phase == "done" then break end + if (screen.shownExp or 0) >= 62 or n > 0 then + n = n + 1 + local frame = screen.expBurst and screen.expBurst.frame + local msg = tostring(screen.message or "") + print(("[driver] f%03d bar %s/64 :L%s burst %s %q"):format(n, + tostring(screen.shownExp), tostring(screen.shownLevel), + tostring(frame), msg)) + snap(out .. ("/f%03d.png"):format(n)) + if frame and frame >= 1 and frame <= 8 then + sparks[frame] = true + if msg:find("grew to level") then sparkLine = msg end + end + if msg:find("grew to level") then + lineAt = lineAt or n + if not zeroAt and screen.shownExp == 0 then zeroAt = n end + end + else + U.wait(1) + end + end + local count = 0 + for _ in pairs(sparks) do count = count + 1 end + print(("[driver] burst frames photographed: %d of 8"):format(count)) + assert(count > 0, "the end-of-bar burst never armed on the level crossing") + assert(not sparkLine, + "the grew-to-level line was already up while the sparks were on screen") + assert(lineAt, "the grew-to-level line never printed inside the crawl") + assert(zeroAt and zeroAt > lineAt, + "the bar did not restart at 0 after the level line") + assert(screen.phase == "stats-box", "no stats box after segment 2 (phase " + .. tostring(screen.phase) .. ")") + print(("[driver] level line at f%03d, bar zero at f%03d, stats box at f%03d") + :format(lineAt, zeroAt, n)) + U.shot(game, out .. "/07-stats-box.png") + for _ = 1, 900 do if screen.phase == "done" then break end - if (screen.message or ""):find("grew to level") then - U.shot(game, out .. "/06-grew-to-level.png") - end U.tap(game, "a") U.wait(2) end diff --git a/tests/drivers/gold_field_items.lua b/tests/drivers/gold_field_items.lua index fae59875..f40674ef 100644 --- a/tests/drivers/gold_field_items.lua +++ b/tests/drivers/gold_field_items.lua @@ -56,11 +56,26 @@ return function(game) end U.tap(game, "a") U.wait(3) - local pack = game.stack:top() + local pack + for _ = 1, 120 do + pack = game.stack:top() + if pack and pack.rows then break end + U.wait(1) + end assert(pack and pack.rows, "PACK did not open") return pack end + local function unwind(msg) + for _ = 1, 20 do + local state = game.stack:top() + if state == nil then break end + if state.screenId ~= "Gen2MenuFade" then U.tap(game, "b") end + U.wait(4) + end + assert(game.stack:top() == nil, msg) + end + -- ---- 1. the escape rope -------------------------------------------------- -- Bank the triple the way play does: up the Cherrygrove stairs, whose @@ -133,11 +148,7 @@ return function(game) "the heal message did not return to the PACK") assert(mon.hp == 25, "POTION healed to " .. tostring(mon.hp) .. ", want 25") assert(save.inventory.POTION == nil, "the POTION was not consumed") - U.tap(game, "b") - U.wait(2) - U.tap(game, "b") - U.wait(2) - assert(game.stack:top() == nil, "the menus did not unwind after the heal") + unwind("the menus did not unwind after the heal") U.log("PASS potion: healed a party mon from the PACK, consumed") -- ---- 3. the .Oak refusal ------------------------------------------------- @@ -155,11 +166,7 @@ return function(game) U.tap(game, "b") U.wait(2) assert(save.inventory.X_ATTACK == 1, "backing out must not spend the item") - U.tap(game, "b") - U.wait(2) - U.tap(game, "b") - U.wait(2) - assert(game.stack:top() == nil, "the menus did not unwind after the submenu") + unwind("the menus did not unwind after the submenu") -- ...and an untossable one still gets USE, because .ItemBallsKey_LoadSubmenu's -- untossable arm never looks at the menu nibble -- so THAT is where @@ -182,11 +189,7 @@ return function(game) "the refusal must not spend the item") U.tap(game, "a") U.wait(2) - U.tap(game, "b") - U.wait(2) - U.tap(game, "b") - U.wait(2) - assert(game.stack:top() == nil, "the menus did not unwind after the refusal") + unwind("the menus did not unwind after the refusal") U.log("PASS oak: field-NOUSE key item refused inside the PACK") -- ---- 4. the SELECT box --------------------------------------------------- @@ -228,7 +231,12 @@ return function(game) end U.tap(game, "a") U.wait(3) - local list = game.stack:top() + local list + for _ = 1, 120 do + list = game.stack:top() + if list and list.wantsSubmenu then break end + U.wait(1) + end assert(list and list.wantsSubmenu, "the field party list did not open") U.tap(game, "a") U.wait(2) diff --git a/tests/drivers/gold_fishing_rod_bug1708_test.lua b/tests/drivers/gold_fishing_rod_bug1708_test.lua index d3d877d7..c85e9649 100644 --- a/tests/drivers/gold_fishing_rod_bug1708_test.lua +++ b/tests/drivers/gold_fishing_rod_bug1708_test.lua @@ -235,7 +235,12 @@ return function(game) tap("down", 4) end tap("a", 10) - local pack = game.stack:top() + local pack + for _ = 1, 120 do + pack = game.stack:top() + if getmetatable(pack) == PackMenu then break end + U.wait(1) + end if not check(("%s: and the PACK is open"):format(facing), getmetatable(pack) == PackMenu) then return false diff --git a/tests/drivers/gold_fly_anim_bug1711_test.lua b/tests/drivers/gold_fly_anim_bug1711_test.lua index 0d103a08..4a132653 100644 --- a/tests/drivers/gold_fly_anim_bug1711_test.lua +++ b/tests/drivers/gold_fly_anim_bug1711_test.lua @@ -36,6 +36,17 @@ return function(game) local function tap(btn) U.tap(game, btn) U.wait(3) end local function top() return game.stack:top() end + local function topId() + local state = top() + return state and state.screenId or nil + end + local function waitFor(id, limit) + for _ = 1, limit or 120 do + if topId() == id then return true end + U.wait(1) + end + return topId() == id + end U.wait(45) local world = game.world @@ -172,6 +183,7 @@ return function(game) ok(menu.list:current().value == "pokemon", "the cursor found POKeMON") tap("a") + waitFor("Gen2PartyMenu", 90) local party = top() if not ok(party and party.screenId == "Gen2PartyMenu", "POKeMON opened the party list") then @@ -195,6 +207,7 @@ return function(game) end tap("a") U.wait(10) + waitFor("Gen2Pokegear", 120) local picker = top() if not ok(picker and picker.screenId == "Gen2Pokegear" and picker.fly, diff --git a/tests/drivers/gold_flymap_cursor_bug1712_test.lua b/tests/drivers/gold_flymap_cursor_bug1712_test.lua index 393642f6..ad09ee69 100644 --- a/tests/drivers/gold_flymap_cursor_bug1712_test.lua +++ b/tests/drivers/gold_flymap_cursor_bug1712_test.lua @@ -35,6 +35,17 @@ return function(game) local function tap(btn) U.tap(game, btn) U.wait(3) end local function top() return game.stack:top() end + local function topId() + local state = top() + return state and state.screenId or nil + end + local function waitFor(id, limit) + for _ = 1, limit or 120 do + if topId() == id then return true end + U.wait(1) + end + return topId() == id + end U.wait(45) local world = game.world @@ -146,6 +157,7 @@ return function(game) ok(menu.list:current().value == "pokemon", "the cursor found POKeMON") tap("a") + waitFor("Gen2PartyMenu", 90) local party = top() if not ok(party and party.screenId == "Gen2PartyMenu", "POKeMON opened the party list") then @@ -172,6 +184,7 @@ return function(game) ok(sub.index == flyRow, "the cursor found FLY") tap("a") U.wait(10) + waitFor("Gen2Pokegear", 120) local picker = top() if not ok(picker and picker.screenId == "Gen2Pokegear", diff --git a/tests/drivers/gold_halloffame_shots.lua b/tests/drivers/gold_halloffame_shots.lua index bc94d881..7dcc5796 100644 --- a/tests/drivers/gold_halloffame_shots.lua +++ b/tests/drivers/gold_halloffame_shots.lua @@ -27,6 +27,8 @@ local Mon = require("src.battle.gen2.Mon") local INDUCT_INTERVAL = 20 local CREDITS_INTERVAL = 60 local LIMIT = 8000 +-- ../pokecrystal/engine/gfx/pic_animation.asm:75 +local PC_INTERVAL, PC_SAMPLES = 8, 8 return function(game) local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-hof" @@ -138,10 +140,20 @@ return function(game) }) game.stack:push(viewer) for index = 1, #entry.mons do - U.wait(10) - U.shot(game, ("%s/pc-%02d-%s.png") - :format(out, index, tostring((viewer:currentMon() or {}).species))) + local species = tostring((viewer:currentMon() or {}).species) + for sample = 0, PC_SAMPLES - 1 do + U.shot(game, ("%s/pc-%02d-%s-%03d.png") + :format(out, index, species, sample * PC_INTERVAL)) + U.wait(PC_INTERVAL) + end + -- ../pokecrystal/engine/events/halloffame.asm:313-317 + local guard = 0 + while viewer.picAnim and guard < 240 do + U.wait(1) + guard = guard + 1 + end U.tap(game, "a") + U.wait(10) end U.log("roster viewer shot for " .. #entry.mons .. " mon(s) in " .. out) end diff --git a/tests/drivers/gold_menu_shots.lua b/tests/drivers/gold_menu_shots.lua index 58b9cc76..74bff4f9 100644 --- a/tests/drivers/gold_menu_shots.lua +++ b/tests/drivers/gold_menu_shots.lua @@ -107,6 +107,13 @@ return function(game) hasSave = true, save = save, clock = { hour = 20, minute = 42, weekday = 6 }, })) + -- ../pokecrystal/engine/menus/intro_menu.asm:479 DisplaySaveInfoOnContinue. + local contPanel = MainMenu.new(game, { + hasSave = true, save = save, + clock = { hour = 5, minute = 9, weekday = 2 }, + }) + contPanel.phase, contPanel.confirmDelay = "confirm", 0 + show("02b-mainmenu-continue-panel", contPanel) local sprites = game.data.gen2Sprites local chris = sprites and sprites.SPRITE_CHRIS diff --git a/tests/drivers/gold_naming_and_trade_item.lua b/tests/drivers/gold_naming_and_trade_item.lua index 8df56646..cb683573 100644 --- a/tests/drivers/gold_naming_and_trade_item.lua +++ b/tests/drivers/gold_naming_and_trade_item.lua @@ -50,6 +50,14 @@ return function(game) local function tap(btn) U.tap(game, btn) U.wait(3) end local function top() return game.stack:top() end + local function waitFor(id, limit) + for _ = 1, limit or 120 do + local state = top() + if (state and state.screenId or nil) == id then return true end + U.wait(1) + end + return false + end U.wait(45) ok(game.world and game.world.map, "gold world booted") @@ -124,6 +132,7 @@ return function(game) tap("down") end tap("a") + waitFor("Gen2PartyMenu", 90) local party = top() if not ok(party and party.screenId == "Gen2PartyMenu", "the party list opened") then diff --git a/tests/drivers/gold_party_held_item.lua b/tests/drivers/gold_party_held_item.lua index b1a033b8..2248782f 100644 --- a/tests/drivers/gold_party_held_item.lua +++ b/tests/drivers/gold_party_held_item.lua @@ -27,6 +27,14 @@ return function(game) local function tap(btn) U.tap(game, btn) U.wait(3) end local function top() return game.stack:top() end + local function waitFor(id, limit) + for _ = 1, limit or 120 do + local state = top() + if (state and state.screenId or nil) == id then return true end + U.wait(1) + end + return false + end U.wait(45) ok(game.world and game.world.map, "gold world booted") @@ -72,6 +80,7 @@ return function(game) end tap("a") + waitFor("Gen2PartyMenu", 90) local party = top() if not ok(party and party.screenId == "Gen2PartyMenu", "POKรฉMON opened the list") then diff --git a/tests/drivers/gold_party_submenu.lua b/tests/drivers/gold_party_submenu.lua index b13410af..6f56ab56 100644 --- a/tests/drivers/gold_party_submenu.lua +++ b/tests/drivers/gold_party_submenu.lua @@ -20,6 +20,14 @@ return function(game) local function tap(btn) U.tap(game, btn) U.wait(3) end local function top() return game.stack:top() end + local function waitFor(id, limit) + for _ = 1, limit or 120 do + local state = top() + if (state and state.screenId or nil) == id then return true end + U.wait(1) + end + return false + end U.wait(45) ok(game.world and game.world.map, "gold world booted") @@ -52,6 +60,7 @@ return function(game) ok(menu.list:current().value == "pokemon", "the cursor found POKรฉMON") tap("a") + waitFor("Gen2PartyMenu", 90) local party = top() if not ok(party and party.screenId == "Gen2PartyMenu", "POKรฉMON opened the list") then diff --git a/tests/drivers/gold_radio_persist.lua b/tests/drivers/gold_radio_persist.lua index 78758e92..80c3c0e5 100644 --- a/tests/drivers/gold_radio_persist.lua +++ b/tests/drivers/gold_radio_persist.lua @@ -71,10 +71,11 @@ return function(game) assert(gear, "the POKeGEAR row did not open the gear") assert(#gear.cards == 4, "expected all four cards, got " .. #gear.cards) - -- Strip: CLOCK, MAP, RADIO, PHONE. Two rights and A is the radio card. - U.tap(game, "right") U.wait(2) - U.tap(game, "right") U.wait(2) - U.tap(game, "a") U.wait(2) + for _ = 1, 8 do + if gear:card().id == "radio" then break end + U.tap(game, "right") U.wait(3) + end + if gear.mode == "strip" then U.tap(game, "a") U.wait(3) end assert(gear.mode == "card" and gear:card().id == "radio", "did not land on the radio card") @@ -96,9 +97,9 @@ return function(game) U.tap(game, "b") U.wait(3) U.tap(game, "b") U.wait(3) for _ = 1, 60 do - if not topIs("Gen2StartMenu") then break end - U.tap(game, "b") - U.wait(2) + if top() == nil then break end + if topIs("Gen2StartMenu") then U.tap(game, "b") end + U.wait(4) end U.wait(5) assert(Music.current() == "Music_PokeFluteChannel", diff --git a/tests/drivers/gold_stats_page_tint_bug1693_test.lua b/tests/drivers/gold_stats_page_tint_bug1693_test.lua index 327d5e99..acaefcb2 100644 --- a/tests/drivers/gold_stats_page_tint_bug1693_test.lua +++ b/tests/drivers/gold_stats_page_tint_bug1693_test.lua @@ -36,6 +36,14 @@ return function(game) end local function tap(btn) U.tap(game, btn) U.wait(4) end local function top() return game.stack:top() end + local function waitFor(id, limit) + for _ = 1, limit or 120 do + local state = top() + if (state and state.screenId or nil) == id then return true end + U.wait(1) + end + return false + end local function shot(name) return ok(U.shot(game, ("%s/%s.png"):format(out, name)), name .. " reached disk") @@ -190,6 +198,7 @@ return function(game) end ok(menu.list:current().value == "pokemon", "the cursor found the party row") tap("a") + waitFor("Gen2PartyMenu", 90) local party = top() if not ok(party and party.screenId == "Gen2PartyMenu", "that opened the party list") then diff --git a/tests/drivers/gold_stats_text_paper_bug1858_test.lua b/tests/drivers/gold_stats_text_paper_bug1858_test.lua index 5f9e7e51..f4a6dffb 100644 --- a/tests/drivers/gold_stats_text_paper_bug1858_test.lua +++ b/tests/drivers/gold_stats_text_paper_bug1858_test.lua @@ -43,6 +43,14 @@ return function(game) U.wait(4) end local function top() return game.stack:top() end + local function waitFor(id, limit) + for _ = 1, limit or 120 do + local state = top() + if (state and state.screenId or nil) == id then return true end + U.wait(1) + end + return false + end U.wait(45) if not (game.world and game.world.map) then @@ -104,6 +112,7 @@ return function(game) tap("down") end tap("a") + waitFor("Gen2PartyMenu", 90) local party = top() if not claim(party and party.screenId == "Gen2PartyMenu", "that opened the party list") then diff --git a/tests/drivers/gold_transition_shots.lua b/tests/drivers/gold_transition_shots.lua index 04ff7d94..e669ecb3 100644 --- a/tests/drivers/gold_transition_shots.lua +++ b/tests/drivers/gold_transition_shots.lua @@ -6,7 +6,6 @@ -- Ball overlay a trainer battle stamps over the map first. POKEPORT_SHOT_INTERVAL -- picks the sampling; the default walks the whole thing at 6 frames. -- --- Shots land in /tmp/gold-transition/