Moved move activation from the game engine to the move itself and added the ability to switch out

This commit is contained in:
James Daly 2019-05-29 22:20:47 -04:00
parent 8ff9bdd938
commit 32c6913187
7 changed files with 237 additions and 178 deletions

View File

@ -23,8 +23,8 @@ object Game {
val movepool2 = IndexedSeq(Move("Absorb"))
val p1 = TrainerID("Jaeda", Gender.Female, 0)
val p2 = TrainerID("Wild Monster", Gender.Male, 0)
val party1 = new Party(p1, new Monster(new StorageMon("Allied Mon", Gene.randomGene(null, form1), form1, Statistic.emptyEvs, movepool1)), IndexedSeq())
val party2 = new Party(p2, new Monster(new StorageMon("Wild Mon", Gene.randomGene(null, form2), form2, Statistic.emptyEvs, movepool2)), IndexedSeq(
val party1 = new Party(p1, new MonsterPtr(new Monster(new StorageMon("Allied Mon", Gene.randomGene(null, form1), form1, Statistic.emptyEvs, movepool1))), IndexedSeq())
val party2 = new Party(p2, new MonsterPtr(new Monster(new StorageMon("Wild Mon", Gene.randomGene(null, form2), form2, Statistic.emptyEvs, movepool2))), IndexedSeq(
new Monster(new StorageMon("Sideboard Mon", Gene.randomGene(null, form2), form2, Statistic.emptyEvs, movepool2))
))
val engine = new BattleEngine(party1, party2)

View File

@ -3,7 +3,7 @@ package mon.battle
import mon.stat._
import mon.stat.Statistic.Speed
case class Action(user : Monster, move : Move, target : Monster) extends Comparable[Action] {
case class Action(user : MonsterPtr, move : MoveTurn, target : MonsterPtr) extends Comparable[Action] {
override def compareTo(other : Action) = {
if (move.prior == other.move.prior) {
other.user(Speed) compareTo user(Speed)

View File

@ -15,11 +15,11 @@ class BattleEngine(val player: Party, val enemy: Party)(implicit val rng: Random
}
def playTurn() = {
val playerMove = player.pollAction
val playerTarget = player.pollTarget(playerMove, player.lead, enemy)
val enemyMove = enemy.pollAction
val enemyTarget = enemy.pollTarget(enemyMove, enemy.lead, player)
val actions = Seq(Action(player.lead, playerMove, playerTarget), Action(enemy.lead, enemyMove, enemyTarget))
val playerAction = player.pollAction(enemy)
val enemyAction = enemy.pollAction(player)
println(enemyAction)
val actions = Seq(playerAction, enemyAction)
val queue = rng.shuffle(actions).sorted // Shuffle to randomize in the event of a tie
queue.foreach(useMove)
val eotQueue = Seq(player.lead, enemy.lead).sortBy(_(Speed))
@ -28,7 +28,7 @@ class BattleEngine(val player: Party, val enemy: Party)(implicit val rng: Random
if (!player.lead.isAlive) {
if (player.canFight) {
val replace = player.pollReplacement(enemy)
player.swapIn(replace)
player.switchIn(replace)
} else {
println(s"${player.trainer} has lost!")
}
@ -36,7 +36,7 @@ class BattleEngine(val player: Party, val enemy: Party)(implicit val rng: Random
if (!enemy.lead.isAlive) {
if (enemy.canFight) {
val replace = enemy.pollReplacement(player)
enemy.swapIn(replace)
enemy.switchIn(replace)
} else {
println(s"${enemy.trainer} has lost!")
}
@ -51,117 +51,7 @@ class BattleEngine(val player: Party, val enemy: Party)(implicit val rng: Random
val move = action.move
val target = action.target
if (user.isAlive) {
println(s"$user used $move.")
if (attackRoll(user, move, target)) {
if (move.pow > 0 || move.powCallback != null) {
val dmg = damageRoll(user, move, target)
val actualDmg = target.takeDamage(dmg)
if (dmg == actualDmg) {
println(s"$target takes $actualDmg damage!")
} else {
println(s"$target takes $actualDmg (+${dmg - actualDmg} overkill) damage!")
}
if (move.drain > 0.frac) {
val healing = Math.max(move.drain * actualDmg, 1)
user.recoverDamage(healing)
println(s"$user recovered $healing damage.")
} else if (move.drain < 0.frac) {
val recoil = Math.max(-move.drain * actualDmg, 1)
user.takeDamage(recoil)
println(s"$user took $recoil damage from recoil!")
}
if (!user.isAlive) {
println(s"$user fainted!")
}
if (!target.isAlive) {
println(s"$target fainted!")
}
}
applyBoosts(target, move.boosts)
applyStatus(target, move.status)
if (move.selfEffect != null) {
applyEffect(user, move.selfEffect)
}
if (move.secondary != null && rng.chance(move.secondary.chance.%%)) {
applyEffect(target, move.secondary)
}
// TODO : Support moves
// TODO : Multiparty
} else {
println("Missed!")
}
}
}
def attackRoll(user: Monster, move: Move, target: Monster) = {
if (move.accuracy == 0) {
true
} else {
val acc = user.boosts.getOrElse(Accuracy, 0) - target.boosts.getOrElse(Evasion, 0)
val mod = if (acc > 0) Fraction(3 + acc, 3) else Fraction(3, 3 - acc)
val chance = move.accuracy.%% * mod
rng.chance(chance)
}
}
def damageRoll(user: Monster, move: Move, target: Monster) = {
val atkStat = if (move.mvType == MoveType.Physical) PAtk else MAtk
val defStat = if (move.mvType == MoveType.Physical) PDef else MDef
// TODO : Fixed damage
val pow = if (move.powCallback != null) move.powCallback(user, target) else move.pow
val baseDmg = (2 * user.level / 5 + 2) * pow * user(atkStat) / (target(defStat) * 50) + 2
val effectiveness = target.effectiveness(move.element)
val multiplier = effectiveness * critMultiplier(user, move, target)
if (effectiveness > 1.0) {
println("It's super effective!")
} else if (effectiveness < 1.0) {
println("It's not very effective.")
}
val maxDmg = (baseDmg * multiplier).toInt
val minDmg = (17 \ 20) * maxDmg
rng.nextInt(minDmg, maxDmg)
}
def critMultiplier(user : Monster, move : Move, target: Monster) = {
// Percentage chance is different from Pokemon
val stage = move.crit
val chance = (1 << stage) \ 16 // Doubles per stage
if (rng.chance(chance)) {
println("A critical hit!")
1.5
} else {
1.0
}
}
def applyEffect(target: Monster, effect: Secondary) {
applyBoosts(target, effect.boosts)
applyStatus(target, effect.status)
}
def applyStatus(target: Monster, status: Status) {
if (target.isAlive) {
if (status != null && target.status == None) {
// apply status
target.status = Some(status)
status.onStart.map(_(target))
}
}
}
def applyBoosts(target: Monster, boosts: Map[Stat, Int]) {
if (target.isAlive) {
boosts.foreach {
case (s, b) => {
target.applyBoost(s, b)
if (b > 0) {
println(s"$target's $s rose!")
} else if (b < 0) {
println(s"$target's $s fell!")
}
}
}
move.useMove(user, target)
}
}
}

View File

@ -0,0 +1,6 @@
package mon.stat
class MonsterPtr(var mon : Monster) {
override def toString : String = mon.toString()
}

View File

@ -6,7 +6,9 @@ import scala.tools.reflect.ToolBox
import com.fasterxml.jackson.module.scala.JsonScalaEnumeration
import scala.io.Source
import scala.util.Random
import Statistic._
import mon.util._
object MoveType extends Enumeration {
@ -19,55 +21,190 @@ object Target extends Enumeration {
}
class TargetType extends TypeReference[Target.type]
abstract class Move {
val name : String
val desc : String
val mvType : MoveType
val pow : Int
val powCallback : (Monster, Monster) => Int
val prior : Int
val accuracy : Int
val pp : Int
val element : Element
val flags : Set[String]
val target : Target
val boosts : Map[Stat, Int]
val crit : Int
val status : Status
val drain : Fraction
val selfEffect : Secondary
trait MoveTurn {
def name: String
def prior: Int
def target: Target
def useMove(user: Monster, target: Monster)(implicit rng: Random): Unit
}
class SwitchOut(val party: Party, val replacement: Monster) extends MoveTurn {
def name = "Swap"
def prior = +6
def target = Target.Self
def useMove(user: Monster, target: Monster)(implicit rng: Random): Unit = {
println(s"${party.trainer} withdrew $user!")
party.switchIn(replacement)
println(s"${party.trainer} sent out $replacement")
}
override def toString: String = s"Swap -> $replacement"
}
abstract class Move extends MoveTurn {
val name: String
val desc: String
val mvType: MoveType
val pow: Int
val powCallback: (Monster, Monster) => Int
val prior: Int
val accuracy: Int
val pp: Int
val element: Element
val flags: Set[String]
val target: Target
val boosts: Map[Stat, Int]
val crit: Int
val status: Status
val drain: Fraction
val selfEffect: Secondary
val secondary: Secondary
// boosts
// onHit
// onTryHit
// zPower, zMoveEffect, zMoveBoost
override def useMove(user: Monster, target: Monster)(implicit rng: Random) = {
println(s"$user used $name.")
if (attackRoll(user, target)) {
if (pow > 0 || powCallback != null) {
val dmg = damageRoll(user, target)
val actualDmg = target.takeDamage(dmg)
if (dmg == actualDmg) {
println(s"$target takes $actualDmg damage!")
} else {
println(s"$target takes $actualDmg (+${dmg - actualDmg} overkill) damage!")
}
if (drain > 0.frac) {
val healing = Math.max(drain * actualDmg, 1)
user.recoverDamage(healing)
println(s"$user recovered $healing damage.")
} else if (drain < 0.frac) {
val recoil = Math.max(-drain * actualDmg, 1)
user.takeDamage(recoil)
println(s"$user took $recoil damage from recoil!")
}
if (!user.isAlive) {
println(s"$user fainted!")
}
if (!target.isAlive) {
println(s"$target fainted!")
}
}
applyBoosts(target, boosts)
applyStatus(target, status)
if (selfEffect != null) {
applyEffect(user, selfEffect)
}
if (secondary != null && rng.chance(secondary.chance.%%)) {
applyEffect(target, secondary)
}
// TODO : Support moves
// TODO : Multiparty
} else {
println("Missed!")
}
}
def attackRoll(user: Monster, target: Monster)(implicit rng: Random) = {
if (accuracy == 0) {
true
} else {
val acc = user.boosts.getOrElse(Accuracy, 0) - target.boosts.getOrElse(Evasion, 0)
val mod = if (acc > 0) Fraction(3 + acc, 3) else Fraction(3, 3 - acc)
val chance = accuracy.%% * mod
rng.chance(chance)
}
}
def damageRoll(user: Monster, target: Monster)(implicit rng: Random) = {
val atkStat = if (mvType == MoveType.Physical) PAtk else MAtk
val defStat = if (mvType == MoveType.Physical) PDef else MDef
// TODO : Fixed damage
val actualPow = if (powCallback != null) powCallback(user, target) else pow
val baseDmg = (2 * user.level / 5 + 2) * actualPow * user(atkStat) / (target(defStat) * 50) + 2
val effectiveness = target.effectiveness(element)
val multiplier = effectiveness * critMultiplier(user, target)
if (effectiveness > 1.0) {
println("It's super effective!")
} else if (effectiveness < 1.0) {
println("It's not very effective.")
}
val maxDmg = (baseDmg * multiplier).toInt
val minDmg = (17 \ 20) * maxDmg
rng.nextInt(minDmg, maxDmg)
}
def critMultiplier(user: Monster, target: Monster)(implicit rng: Random) = {
// Percentage chance is different from Pokemon
val stage = crit
val chance = (1 << stage) \ 16 // Doubles per stage
if (rng.chance(chance)) {
println("A critical hit!")
1.5
} else {
1.0
}
}
def applyEffect(target: Monster, effect: Secondary) {
applyBoosts(target, effect.boosts)
applyStatus(target, effect.status)
}
def applyStatus(target: Monster, status: Status) {
if (target.isAlive) {
if (status != null && target.status == None) {
// apply status
target.status = Some(status)
status.onStart.map(_(target))
}
}
}
def applyBoosts(target: Monster, boosts: Map[Stat, Int]) {
if (target.isAlive) {
boosts.foreach {
case (s, b) => {
target.applyBoost(s, b)
if (b > 0) {
println(s"$target's $s rose!")
} else if (b < 0) {
println(s"$target's $s fell!")
}
}
}
}
}
override def toString = {
name
}
}
case class MoveToken(
val name : String,
val shortDesc : String,
@JsonScalaEnumeration(classOf[MoveTypeType]) val category : MoveType,
val basePower : Option[Int],
val basePowerCallback : String,
val priority : Int,
val accuracy : Option[Int],
val pp : Int,
val `type` : String,
val flags : Map[String, Int],
val status : String,
val self : SecondaryToken,
val secondary : SecondaryToken,
val drain : Fraction,
val recoil : Fraction,
@JsonScalaEnumeration(classOf[TargetType]) val target : Target = Target.Normal,
val boosts : Map[String, Int] = Map(),
val critRatio : Int = 0) {
val name: String,
val shortDesc: String,
@JsonScalaEnumeration(classOf[MoveTypeType]) val category: MoveType,
val basePower: Option[Int],
val basePowerCallback: String,
val priority: Int,
val accuracy: Option[Int],
val pp: Int,
val `type`: String,
val flags: Map[String, Int],
val status: String,
val self: SecondaryToken,
val secondary: SecondaryToken,
val drain: Fraction,
val recoil: Fraction,
@JsonScalaEnumeration(classOf[TargetType]) val target: Target = Target.Normal,
val boosts: Map[String, Int] = Map(),
val critRatio: Int = 0) {
def instantiate() = {
val token = this
new Move {
@ -82,7 +219,7 @@ case class MoveToken(
val accuracy = token.accuracy.getOrElse(100)
val flags = token.flags.keySet
val target = token.target
val boosts = if (token.boosts != null) token.boosts.map{case (s, i) => (Statistic(s), i)} else Map()
val boosts = if (token.boosts != null) token.boosts.map { case (s, i) => (Statistic(s), i) } else Map()
val crit = token.critRatio
val status = if (token.status != null) Status(token.status) else null
val drain = if (token.drain != null) token.drain else if (token.recoil != null) -token.recoil else 0 \ 1
@ -95,14 +232,14 @@ case class MoveToken(
object Move {
private var moves = Map[String, Move]()
val tokens = YamlHelper.extractSeq[MoveToken](Source.fromInputStream(Move.getClass.getResourceAsStream("data/moves.yaml"))).map(t => (t.name, t)).toMap
def apply(name : String) : Move = {
def apply(name: String): Move = {
if (!moves.contains(name)) {
moves = moves.updated(name, tokens(name).instantiate())
}
moves(name)
}
def compilePowCallback(code: String): (Monster, Monster) => Int = {
if (code != null) {
val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()
@ -117,11 +254,11 @@ object Move {
""".stripMargin)
val f = tb.compile(tree)
val wrapper = f()
wrapper.asInstanceOf[(Monster, Monster) => Int]
} else {
null
}
}
}

View File

@ -2,30 +2,50 @@ package mon.stat
import scala.util.Random
import mon.battle.rngDice
import mon.battle.Action
import mon.stat.Target._
class Party(val trainer : TrainerID, var lead : Monster, var sideboard : IndexedSeq[Monster]) {
def pollAction(implicit rng : Random) : Move = rng.pick(lead.base.moves)
def pollTarget(move : Move, user : Monster, them: Party)(implicit rng : Random) : Monster = {
class Party(val trainer: TrainerID, val lead: MonsterPtr, var sideboard: IndexedSeq[Monster]) {
def pollAction(them: Party)(implicit rng: Random): Action = {
if (hasBackup && shouldSwitch(them)) {
val sub = pollReplacement(them: Party)
val move = new SwitchOut(this, sub)
Action(lead, move, lead)
} else {
val move = pollMove
val target = pollTarget(move, lead, them)
Action(lead, move, target)
}
}
def pollMove(implicit rng: Random): MoveTurn = rng.pick(lead.base.moves)
def pollTarget(move: MoveTurn, user: MonsterPtr, them: Party)(implicit rng: Random): MonsterPtr = {
if (move.target == Self) {
user
} else {
them.lead
}
}
def pollReplacement(them : Party)(implicit rng : Random) : Monster = {
def pollReplacement(them: Party)(implicit rng: Random): Monster = {
sideboard.filter(_.isAlive).head
}
def swapIn(mon : Monster) = {
def switchIn(mon: Monster) = {
val index = sideboard.indexOf(mon)
sideboard = sideboard.updated(index, lead)
lead = mon
sideboard = sideboard.updated(index, lead.mon)
lead.mon = mon
}
def canFight : Boolean = {
lead.isAlive || sideboard.exists(_.isAlive)
def shouldSwitch(them: Party)(implicit rng: Random): Boolean = {
true
}
def canFight: Boolean = {
lead.isAlive || hasBackup
}
def hasBackup: Boolean = {
sideboard.exists(_.isAlive)
}
}

View File

@ -3,6 +3,10 @@ package mon
import org.json4s.DefaultFormats
import org.json4s.ext.EnumNameSerializer
import scala.util.Random
import mon.util.Dice
package object stat {
type Stat = Statistic.Value
type MoveType = MoveType.Value
@ -10,4 +14,6 @@ package object stat {
type Target = Target.Value
implicit val formats = DefaultFormats + new EnumNameSerializer(MoveType) + new EnumNameSerializer(Gender)
implicit def rngDice(rng : Random) = new Dice(rng)
implicit def ptrToMonster(ptr : MonsterPtr) : Monster = ptr.mon
}