DramaticShapeVoxelMod archival mirror · read-only
tests/letsgo_trainer_exp.lua

letsgo_trainer_exp.lua

tests/letsgo_trainer_exp.lua6.3 KB100644f67c222656
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
-- Driver: does a TRAINER knockout pay the whole party under LET'S GO FULL?
--
-- The catch payout is easy to see (one throw, one award). A knockout is
-- not: it goes through the engine's own faint -> awardExp path, which is
-- the one this mode replaces wholesale. So fight a real trainer with a
-- deliberately uneven party and read the deltas -- every member should
-- gain, and the low ones should gain multiples of the high one.
--
--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/letsgo_trainer_exp.lua \
--   SHOT_DIR=.scratchpad/letsgo "/c/Program Files/LOVE/lovec.exe" .
--
-- DS_LETSGO_MODE=off runs the same fight on the engine's own rules, which
-- is the control: there, only the Pokemon that fought should gain.
return function(game)
  local U = dofile("tests/drivers/util.lua")
  local MODE = os.getenv("DS_LETSGO_MODE") or "full"
  local Pokemon = require("src.pokemon.Pokemon")
  local BattleState = require("src.battle.BattleState")

  local exports = game.mods and game.mods.exports
  local lib = exports and exports.DRAMATIC_SHAPE and exports.DRAMATIC_SHAPE.lib
  if not lib then
    U.log("DRAMATIC_SHAPE is not loaded -- enable it and run again")
    return
  end
  local LetsGo = lib.require("LetsGo")
  LetsGo.setting:setValue(MODE == "off" and false or MODE, game)
  U.log("LET'S GO mode: " .. tostring(LetsGo.mode()))

  -- count the summary cards at their SOURCE rather than by catching them
  -- on the stack: the driver taps fast enough to dismiss one between two
  -- polls, and an absence there would look like a card that never came
  local ExpPanel = lib.require("ExpPanel")
  local innerNew, cards = ExpPanel.new, 0
  ExpPanel.new = function(g, rows)
    local panel = innerNew(g, rows)
    cards = cards + 1
    -- read at DRAW time in the real thing; here the loop that fills it
    -- has already run, so peeking now is honest
    local parts = {}
    for _, r in ipairs(rows or {}) do
      parts[#parts + 1] = ("%s+%d%s"):format(
        (g.data.pokemon[r.mon.species] or {}).name or "?", r.gained or 0,
        (r.to or 0) > (r.from or 0) and ("->L" .. r.to) or "")
    end
    U.log(("EXP CARD #%d: %s"):format(cards, table.concat(parts, "  ")))
    CARD_HOLD = 20        -- frames to stop tapping, so it can be shot
    return panel
  end

  -- one strong fighter and two bystanders: only the fighter gains on the
  -- engine's own rules, so the bystanders ARE the test
  game.save.party = {
    Pokemon.new(game.data, "CHARIZARD", 45),
    Pokemon.new(game.data, "PIKACHU", 10),
    Pokemon.new(game.data, "RATTATA", 5),
  }
  game.save.player.name = "RED"

  -- The WEAKEST party in the game: one Pokemon, lowest level. Picked by
  -- measuring rather than by name -- an alphabetical first pick lands on
  -- OPP_AGATHA, whose Elite Four ghosts a level 45 Charizard does not
  -- reliably clear, and a fight that never resolves reads exactly like a
  -- payout that never happened.
  local class, partyIx, best = nil, 1, nil
  local ids = {}
  for id in pairs(game.data.trainers) do
    if type(id) == "string" and id:sub(1, 1) ~= "_" then ids[#ids + 1] = id end
  end
  table.sort(ids)                       -- stable across runs
  for _, id in ipairs(ids) do
    local rec = game.data.trainers[id]
    for pi, party in ipairs((type(rec) == "table" and rec.parties) or {}) do
      local n, top = 0, 0
      for _, mon in ipairs(party) do
        n = n + 1
        top = math.max(top, tonumber(mon.level) or 0)
      end
      if n > 0 then
        local score = n * 100 + top
        if not best or score < best then
          best, class, partyIx = score, id, pi
        end
      end
    end
  end
  U.log(("fighting %s party %d (weakest of %d classes)")
        :format(tostring(class), partyIx, #ids))

  local before = {}
  for i, m in ipairs(game.save.party) do
    before[i] = { exp = m.exp, level = m.level }
  end

  U.teleport(game, "ROUTE_1", 5, 8, "down")
  U.wait(60)

  local battle = BattleState.newTrainer(game, class, partyIx)
  battle.onFinish = function() end
  game.overworld:pushBattle(battle)
  U.wait(70)

  -- through the intro to the menu, then FIGHT + first move, over and
  -- over until the fight resolves. The enemy's HP is logged as it goes,
  -- so a fight that stalls is visibly a stall rather than a silent zero.
  local lastHP, shotPanel = nil, false
  for i = 1, 900 do
    -- the fight ending does NOT end the loop until the card has been
    -- photographed: the last knockout resolves the battle in the same
    -- breath that queues the card, so breaking on `result` alone leaves
    -- every run with the card built and never seen
    if battle.result and shotPanel then break end
    -- hold the taps while the card is up, so it can be shot rather than
    -- dismissed on the next frame
    if (CARD_HOLD or 0) > 0 then
      CARD_HOLD = CARD_HOLD - 1
      if not shotPanel then
        local top = game.stack and game.stack:top()
        if top and rawget(top, "rows") then
          shotPanel = true
          U.shot(game, (os.getenv("SHOT_DIR") or ".scratchpad")
                       .. "/exp_panel.png")
          U.log("shot the card")
        end
      end
      U.wait(1)
    elseif battle.phase == "menu" then
      battle.menuIndex = 1              -- FIGHT
      U.tap(game, "a")
    elseif battle.phase == "moveSelect" then
      battle.moveIndex = 1
      U.tap(game, "a")
    else
      U.tap(game, "a")
    end
    local hp = battle.enemy and battle.enemy.mon and battle.enemy.mon.hp
    if hp ~= lastHP then
      lastHP = hp
      U.log(("  foe HP -> %s  (phase %s, step %d)")
            :format(tostring(hp), tostring(battle.phase), i))
    end
    -- photograph the summary card the moment it is on top, then let the
    -- taps carry on and dismiss it
    local top = game.stack and game.stack:top()
    if top and not shotPanel and top ~= battle and rawget(top, "rows") then
      shotPanel = true
      U.shot(game, (os.getenv("SHOT_DIR") or ".scratchpad")
                   .. "/exp_panel.png")
      U.log("shot the card")
    end
    U.wait(5)
  end
  U.log("battle result: " .. tostring(battle.result)
        .. "  phase=" .. tostring(battle.phase))

  for i, m in ipairs(game.save.party) do
    local was = before[i]
    U.log(("EXP %-10s Lv%-3d -> Lv%-3d  exp %d -> %d  (+%d)")
          :format(game.data.pokemon[m.species].name, was.level, m.level,
                  was.exp, m.exp, m.exp - was.exp))
  end
  U.log("done")
end