409 lines
13 KiB
Lua
409 lines
13 KiB
Lua
local Combat = {}
|
|
|
|
local Movelist = require "data.movelist"
|
|
local Opponents = require "data.opponents"
|
|
local Utils = require "util.utils"
|
|
|
|
local Memory = require "util.memory"
|
|
local Pokemon = require "storage.pokemon"
|
|
|
|
local damageMultiplier = { -- http://bulbapedia.bulbagarden.net/wiki/Type_chart#Generation_I
|
|
normal = {normal=1.0, fighting=1.0, flying=1.0, poison=1.0, ground=1.0, rock=0.5, bug=1.0, ghost=0.0, fire=1.0, water=1.0, grass=1.0, electric=1.0, psychic=1.0, ice=1.0, dragon=1.0, },
|
|
fighting = {normal=2.0, fighting=1.0, flying=0.5, poison=0.5, ground=1.0, rock=2.0, bug=0.5, ghost=0.0, fire=1.0, water=1.0, grass=1.0, electric=1.0, psychic=0.5, ice=2.0, dragon=1.0, },
|
|
flying = {normal=1.0, fighting=2.0, flying=1.0, poison=1.0, ground=1.0, rock=0.5, bug=2.0, ghost=1.0, fire=1.0, water=1.0, grass=2.0, electric=0.5, psychic=1.0, ice=1.0, dragon=1.0, },
|
|
poison = {normal=1.0, fighting=1.0, flying=1.0, poison=0.5, ground=0.5, rock=0.5, bug=2.0, ghost=0.5, fire=1.0, water=1.0, grass=2.0, electric=1.0, psychic=1.0, ice=1.0, dragon=1.0, },
|
|
ground = {normal=1.0, fighting=1.0, flying=0.0, poison=2.0, ground=1.0, rock=2.0, bug=0.5, ghost=1.0, fire=2.0, water=1.0, grass=0.5, electric=2.0, psychic=1.0, ice=1.0, dragon=1.0, },
|
|
rock = {normal=1.0, fighting=0.5, flying=2.0, poison=1.0, ground=0.5, rock=1.0, bug=2.0, ghost=1.0, fire=2.0, water=1.0, grass=1.0, electric=1.0, psychic=1.0, ice=2.0, dragon=1.0, },
|
|
bug = {normal=1.0, fighting=0.5, flying=0.5, poison=2.0, ground=1.0, rock=1.0, bug=1.0, ghost=0.5, fire=0.5, water=1.0, grass=2.0, electric=1.0, psychic=2.0, ice=1.0, dragon=1.0, },
|
|
ghost = {normal=0.0, fighting=1.0, flying=1.0, poison=1.0, ground=1.0, rock=1.0, bug=1.0, ghost=2.0, fire=1.0, water=1.0, grass=1.0, electric=1.0, psychic=0.0, ice=1.0, dragon=1.0, },
|
|
fire = {normal=1.0, fighting=1.0, flying=1.0, poison=1.0, ground=1.0, rock=0.5, bug=2.0, ghost=1.0, fire=0.5, water=0.5, grass=2.0, electric=1.0, psychic=1.0, ice=2.0, dragon=0.5, },
|
|
water = {normal=1.0, fighting=1.0, flying=1.0, poison=1.0, ground=2.0, rock=2.0, bug=1.0, ghost=1.0, fire=2.0, water=0.5, grass=0.5, electric=1.0, psychic=1.0, ice=1.0, dragon=0.5, },
|
|
grass = {normal=1.0, fighting=1.0, flying=0.5, poison=0.5, ground=2.0, rock=2.0, bug=0.5, ghost=1.0, fire=0.5, water=2.0, grass=0.5, electric=1.0, psychic=1.0, ice=1.0, dragon=0.5, },
|
|
electric = {normal=1.0, fighting=1.0, flying=2.0, poison=1.0, ground=0.0, rock=1.0, bug=1.0, ghost=1.0, fire=1.0, water=2.0, grass=0.5, electric=0.5, psychic=1.0, ice=1.0, dragon=0.5, },
|
|
psychic = {normal=1.0, fighting=2.0, flying=1.0, poison=2.0, ground=1.0, rock=1.0, bug=1.0, ghost=1.0, fire=1.0, water=1.0, grass=1.0, electric=1.0, psychic=0.5, ice=1.0, dragon=1.0, },
|
|
ice = {normal=1.0, fighting=1.0, flying=2.0, poison=1.0, ground=2.0, rock=1.0, bug=1.0, ghost=1.0, fire=1.0, water=0.5, grass=2.0, electric=1.0, psychic=1.0, ice=0.5, dragon=2.0, },
|
|
dragon = {normal=1.0, fighting=1.0, flying=1.0, poison=1.0, ground=1.0, rock=1.0, bug=1.0, ghost=1.0, fire=1.0, water=1.0, grass=1.0, electric=1.0, psychic=1.0, ice=1.0, dragon=2.0, },
|
|
}
|
|
|
|
local types = {}
|
|
types[0] = "normal"
|
|
types[1] = "fighting"
|
|
types[2] = "flying"
|
|
types[3] = "poison"
|
|
types[4] = "ground"
|
|
types[5] = "rock"
|
|
types[7] = "bug"
|
|
types[8] = "ghost"
|
|
types[20] = "fire"
|
|
types[21] = "water"
|
|
types[22] = "grass"
|
|
types[23] = "electric"
|
|
types[24] = "psychic"
|
|
types[25] = "ice"
|
|
types[26] = "dragon"
|
|
|
|
local savedEncounters = {}
|
|
local conservePP = false
|
|
|
|
local floor = math.floor
|
|
|
|
local function isDisabled(mid)
|
|
return mid == Memory.value("battle", "disabled")
|
|
end
|
|
Combat.isDisabled = isDisabled
|
|
|
|
local function calcDamage(move, attacker, defender, rng)
|
|
if move.fixed then
|
|
return move.fixed, move.fixed
|
|
end
|
|
if move.power == 0 or isDisabled(move.id) then
|
|
return 0, 0
|
|
end
|
|
if move.power > 9000 then
|
|
if Memory.value("battle", "x_accuracy") == 1 and defender.speed < attacker.speed then
|
|
return 9001, 9001
|
|
end
|
|
return 0, 0
|
|
end
|
|
if move.name == "Thrash" and Combat.disableThrash then
|
|
return 0, 0
|
|
end
|
|
|
|
local attFactor, defFactor
|
|
if move.special then
|
|
attFactor, defFactor = attacker.spec, defender.spec
|
|
else
|
|
attFactor, defFactor = attacker.att, defender.def
|
|
end
|
|
local damage = floor(floor(floor(2 * attacker.level / 5 + 2) * math.max(1, attFactor) * move.power / math.max(1, defFactor)) / 50) + 2
|
|
|
|
if move.move_type == attacker.type1 or move.move_type == attacker.type2 then
|
|
damage = floor(damage * 1.5) -- STAB
|
|
end
|
|
|
|
local dmp = damageMultiplier[move.move_type]
|
|
local typeEffect1, typeEffect2 = dmp[defender.type1], dmp[defender.type2]
|
|
if defender.type1 == defender.type2 then
|
|
typeEffect2 = 1
|
|
end
|
|
damage = floor(damage * typeEffect1 * typeEffect2)
|
|
if move.multiple then
|
|
damage = damage * move.multiple
|
|
end
|
|
if rng then
|
|
return damage, damage
|
|
end
|
|
return floor(damage * 217 / 255), damage
|
|
end
|
|
|
|
local function getOpponentType(ty)
|
|
local t1 = types[Memory.value("battle", "opponent_type1")]
|
|
if ty ~= 0 then
|
|
t1 = types[Memory.value("battle", "opponent_type2")]
|
|
if not t1 then
|
|
return Memory.value("battle", "opponent_type2")
|
|
end
|
|
end
|
|
if t1 then
|
|
return t1
|
|
end
|
|
return Memory.value("battle", "opponent_type1")
|
|
end
|
|
Combat.getOpponentType = getOpponentType
|
|
|
|
function getOurType(ty)
|
|
local t1 = types[Memory.value("battle", "our_type1")]
|
|
if ty ~= 0 then
|
|
t1 = types[Memory.value("battle", "our_type2")]
|
|
if not t1 then
|
|
return Memory.value("battle", "opponent_type2")
|
|
end
|
|
end
|
|
if t1 then
|
|
return t1
|
|
end
|
|
return Memory.value("battle", "opponent_type1")
|
|
end
|
|
Combat.getOurType = getOurType
|
|
|
|
local function getMoves(who)--Get the moveset of us [0] or them [1]
|
|
local moves = {}
|
|
local base
|
|
if who == 1 then
|
|
base = 0x0FED
|
|
else
|
|
base = 0x101C
|
|
end
|
|
for idx=0, 3 do
|
|
local val = Memory.raw(base + idx)
|
|
if val > 0 then
|
|
local moveTable = Movelist.get(val)
|
|
if who == 0 then
|
|
moveTable.pp = Memory.raw(0x102D + idx)
|
|
end
|
|
moves[idx + 1] = moveTable
|
|
end
|
|
end
|
|
return moves
|
|
end
|
|
Combat.getMoves = getMoves
|
|
|
|
local function modPlayerStats(user, enemy, move)
|
|
local effect = move.effects
|
|
if effect then
|
|
local diff = effect.diff
|
|
local hitThem = diff < 0
|
|
local stat = effect.stat
|
|
if hitThem then
|
|
enemy[stat] = math.max(2, enemy[stat] + diff)
|
|
else
|
|
user[stat] = user[stat] + diff
|
|
end
|
|
end
|
|
return user, enemy
|
|
end
|
|
|
|
local function calcBestHit(attacker, defender, ours, rng)
|
|
local bestTurns, bestMinTurns = 9001, 9001
|
|
local bestDmg = -1
|
|
local ourMaxHit
|
|
local ret = nil
|
|
for idx,move in ipairs(attacker.moves) do
|
|
if not move.pp or move.pp > 0 then
|
|
local minDmg, maxDmg = calcDamage(move, attacker, defender, rng)
|
|
if maxDmg then
|
|
local minTurns, maxTurns
|
|
if maxDmg <= 0 then
|
|
minTurns, maxTurns = 9001, 9001
|
|
else
|
|
minTurns = math.ceil(defender.hp / maxDmg)
|
|
maxTurns = math.ceil(defender.hp / minDmg)
|
|
end
|
|
if ours then
|
|
local replaces
|
|
if not ret or minTurns < bestMinTurns or maxTurns < bestTurns then
|
|
replaces = true
|
|
elseif maxTurns == bestTurns and move.name == "Thrash" then
|
|
replaces = defender.hp == Memory.double("battle", "opponent_max_hp")
|
|
elseif maxTurns == bestTurns and ret.name == "Thrash" then
|
|
replaces = defender.hp ~= Memory.double("battle", "opponent_max_hp")
|
|
elseif move.fast and not ret.fast then
|
|
replaces = maxTurns <= bestTurns
|
|
elseif ret.fast then
|
|
replaces = maxTurns < bestTurns
|
|
elseif conservePP then
|
|
if maxTurns < 2 or maxTurns == bestMaxTurns then
|
|
if ret.name == "Earthquake" and (move.name == "Ice-Beam" or move.name == "Thunderbolt") then
|
|
replaces = true
|
|
elseif move.pp > ret.pp then
|
|
if ret.name == "Horn-Drill" then
|
|
replaces = true
|
|
elseif move.name ~= "Earthquake" then
|
|
replaces = true
|
|
end
|
|
end
|
|
end
|
|
elseif minDmg > bestDmg then
|
|
replaces = true
|
|
end
|
|
if replaces then
|
|
ret = move
|
|
bestMinTurns = minTurns
|
|
bestTurns = maxTurns
|
|
bestDmg = minDmg
|
|
ourMaxHit = maxDmg
|
|
end
|
|
elseif maxDmg > bestDmg then -- Opponents automatically hit max
|
|
ret = move
|
|
bestTurns = minTurns
|
|
bestDmg = maxDmg
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if ret then
|
|
ret.damage = bestDmg
|
|
ret.maxDamage = ourMaxHit
|
|
ret.minTurns = bestMinTurns
|
|
return ret, bestTurns
|
|
end
|
|
end
|
|
|
|
local function getBestMove(ours, enemy, draw)
|
|
if enemy.hp < 1 then
|
|
return
|
|
end
|
|
local bm, bestUs = calcBestHit(ours, enemy, true)
|
|
local jj, bestEnemy = calcBestHit(enemy, ours, false)
|
|
if not bm then
|
|
return
|
|
end
|
|
if draw and bm.midx then
|
|
Utils.drawText(0, 35, ''..bm.midx.." "..bm.name)
|
|
end
|
|
return bm, bestUs, bestEnemy
|
|
end
|
|
|
|
local function activePokemon(preset)
|
|
local ours = {
|
|
id = Memory.value("battle", "our_id"),
|
|
level = Memory.value("battle", "our_level"),
|
|
hp = Memory.double("battle", "our_hp"),
|
|
att = Memory.double("battle", "our_attack"),
|
|
def = Memory.double("battle", "our_defense"),
|
|
spec = Memory.double("battle", "our_special"),
|
|
speed = Memory.double("battle", "our_speed"),
|
|
type1 = getOurType(0),
|
|
type2 = getOurType(1),
|
|
moves = getMoves(0),
|
|
}
|
|
|
|
local enemy
|
|
if preset then
|
|
enemy = Opponents[preset]
|
|
local toBoost = enemy.boost
|
|
if toBoost then
|
|
local currSpec = ours.spec
|
|
local booster = toBoost.mp
|
|
if (currSpec < 140) == (booster > 1) then
|
|
ours.spec = math.floor(currSpec * booster)
|
|
end
|
|
end
|
|
else
|
|
enemy = {
|
|
id = Memory.value("battle", "opponent_id"),
|
|
level = Memory.value("battle", "opponent_level"),
|
|
hp = Memory.double("battle", "opponent_hp"),
|
|
att = Memory.double("battle", "opponent_attack"),
|
|
def = Memory.double("battle", "opponent_defense"),
|
|
spec = Memory.double("battle", "opponent_special"),
|
|
speed = Memory.double("battle", "opponent_speed"),
|
|
type1 = getOpponentType(0),
|
|
type2 = getOpponentType(1),
|
|
moves = getMoves(1),
|
|
}
|
|
end
|
|
return ours, enemy
|
|
end
|
|
Combat.activePokemon = activePokemon
|
|
|
|
-- STATUS
|
|
|
|
local function isSleeping()
|
|
return Memory.raw(0x116F) > 1
|
|
end
|
|
Combat.isSleeping = isSleeping
|
|
|
|
local function isConfused()
|
|
return Memory.raw(0x106B) > 0
|
|
end
|
|
Combat.isConfused = isConfused
|
|
|
|
-- HP
|
|
|
|
function Combat.hp()
|
|
return Pokemon.index(0, "hp")
|
|
end
|
|
|
|
function Combat.redHP()
|
|
return math.ceil(Pokemon.index(0, "max_hp") * 0.2)
|
|
end
|
|
|
|
function Combat.inRedBar()
|
|
return Combat.hp() <= Combat.redHP()
|
|
end
|
|
|
|
-- COMBAT
|
|
|
|
function Combat.factorPP(enabled)
|
|
conservePP = enabled
|
|
end
|
|
|
|
function Combat.reset()
|
|
conservePP = false
|
|
end
|
|
|
|
function Combat.healthFor(opponent)
|
|
local ours, enemy = activePokemon(opponent)
|
|
local enemyAttack, turnsToDie = calcBestHit(enemy, ours, false)
|
|
return enemyAttack.damage
|
|
end
|
|
|
|
function Combat.inKillRange(draw)
|
|
local ours, enemy = activePokemon()
|
|
local enemyAttack, __ = calcBestHit(enemy, ours, false)
|
|
local __, turnsToKill = calcBestHit(ours, enemy, true)
|
|
if not turnsToKill or not enemyAttack then
|
|
return false
|
|
end
|
|
if draw then
|
|
Utils.drawText(0, 21, ours.speed.." "..enemy.speed)
|
|
Utils.drawText(0, 28, turnsToDie.." "..ours.hp.." | "..turnsToKill.." "..enemy.hp)
|
|
end
|
|
local hpReq = enemyAttack.damage
|
|
local isConfused = isConfused()
|
|
if isConfused then
|
|
hpReq = hpReq + math.floor(ours.hp * 0.2)
|
|
end
|
|
if ours.hp <= hpReq then
|
|
local outsped = enemyAttack.outspeed
|
|
if outsped and outsped ~= true then
|
|
outsped = Memory.value("battle", "attack_turns") > 0
|
|
end
|
|
if outsped or isConfused or turnsToKill > 1 or ours.speed <= enemy.speed or isSleeping() then
|
|
return ours, hpReq
|
|
end
|
|
end
|
|
end
|
|
|
|
local function getBattlePokemon()
|
|
local ours, enemy = activePokemon()
|
|
if enemy.hp == 0 then
|
|
return
|
|
end
|
|
for idx=1,4 do
|
|
local move = ours.moves[idx]
|
|
if move then
|
|
move.midx = idx
|
|
end
|
|
end
|
|
return ours, enemy
|
|
end
|
|
|
|
function Combat.nonKill()
|
|
local ours, enemy = getBattlePokemon()
|
|
if not enemy then
|
|
return
|
|
end
|
|
local bestDmg = -1
|
|
local ret = nil
|
|
for idx,move in ipairs(ours.moves) do
|
|
if not move.pp or move.pp > 0 then
|
|
local __, maxDmg = calcDamage(move, ours, enemy, true)
|
|
local threshold = maxDmg * 0.975
|
|
if threshold and threshold < enemy.hp and threshold > bestDmg then
|
|
ret = move
|
|
bestDmg = threshold
|
|
end
|
|
end
|
|
end
|
|
return ret
|
|
end
|
|
|
|
function Combat.bestMove()
|
|
local ours, enemy = getBattlePokemon()
|
|
if enemy then
|
|
return getBestMove(ours, enemy)
|
|
end
|
|
end
|
|
|
|
function Combat.enemyAttack()
|
|
local ours, enemy = activePokemon()
|
|
if enemy.hp == 0 then
|
|
return
|
|
end
|
|
return calcBestHit(enemy, ours, false)
|
|
end
|
|
|
|
return Combat
|