crafted items support (#5)

This commit is contained in:
Evgenii Alekseev 2020-03-13 03:36:25 +03:00
parent 16ce0bf61c
commit 10c107d2c2
23 changed files with 307 additions and 172 deletions

View File

@ -0,0 +1,17 @@
-- loot
alter table loot add column piece_type text;
update loot set piece_type = 'Tome' where is_tome = 1;
update loot set piece_type = 'Savage' where is_tome = 0;
alter table loot alter column piece_type set not null;
alter table loot drop column is_tome;
-- bis
alter table bis add column piece_type text;
update bis set piece_type = 'Tome' where is_tome = 1;
update bis set piece_type = 'Savage' where is_tome = 0;
alter table bis alter column piece_type set not null;
alter table bis drop column is_tome;

View File

@ -0,0 +1,42 @@
-- loot
alter table loot add column piece_type text;
update loot set piece_type = 'Tome' where is_tome = 1;
update loot set piece_type = 'Savage' where is_tome = 0;
create table loot_new (
loot_id integer primary key autoincrement,
player_id integer not null,
created integer not null,
piece text not null,
piece_type text not null,
job text not null,
foreign key (player_id) references players(player_id) on delete cascade);
insert into loot_new select loot_id, player_id, created, piece, piece_type, job from loot;
drop index loot_owner_idx;
drop table loot;
alter table loot_new rename to loot;
create index loot_owner_idx on loot(player_id);
-- bis
alter table bis add column piece_type text;
update bis set piece_type = 'Tome' where is_tome = 1;
update bis set piece_type = 'Savage' where is_tome = 0;
create table bis_new (
player_id integer not null,
created integer not null,
piece text not null,
piece_type text not null,
job text not null,
foreign key (player_id) references players(player_id) on delete cascade);
insert into bis_new select player_id, created, piece, piece_type, job from bis;
drop index bis_piece_player_id_idx;
drop table bis;
alter table bis_new rename to bis;
create unique index bis_piece_player_id_idx on bis(player_id, piece);

View File

@ -16,12 +16,12 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Operation
import javax.ws.rs._ import javax.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece} import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType}
@Path("api/v1") @Path("api/v1")
class TypesEndpoint(config: Config) extends JsonSupport { class TypesEndpoint(config: Config) extends JsonSupport {
def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPriority def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority
@GET @GET
@Path("types/jobs") @Path("types/jobs")
@ -86,6 +86,27 @@ class TypesEndpoint(config: Config) extends JsonSupport {
} }
} }
@GET
@Path("types/pieces/types")
@Produces(value = Array("application/json"))
@Operation(summary = "piece types list", description = "Returns the available piece types",
responses = Array(
new ApiResponse(responseCode = "200", description = "List of available piece types",
content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
tags = Array("types"),
)
def getPieceTypes: Route =
path("types" / "pieces" / "types") {
get {
complete(PieceType.available.map(_.toString))
}
}
@GET @GET
@Path("types/priority") @Path("types/priority")
@Produces(value = Array("application/json")) @Produces(value = Array("application/json"))

View File

@ -9,16 +9,16 @@
package me.arcanis.ffxivbis.http.api.v1.json package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.{Job, Piece} import me.arcanis.ffxivbis.models.{Job, Piece, PieceType}
case class PieceResponse( case class PieceResponse(
@Schema(description = "is piece tome gear", required = true) isTome: Boolean, @Schema(description = "piece type", required = true) pieceType: String,
@Schema(description = "job name to which piece belong or AnyJob", required = true, example = "DNC") job: String, @Schema(description = "job name to which piece belong or AnyJob", required = true, example = "DNC") job: String,
@Schema(description = "piece name", required = true, example = "body") piece: String) { @Schema(description = "piece name", required = true, example = "body") piece: String) {
def toPiece: Piece = Piece(piece, isTome, Job.withName(job)) def toPiece: Piece = Piece(piece, PieceType.withName(pieceType), Job.withName(job))
} }
object PieceResponse { object PieceResponse {
def fromPiece(piece: Piece): PieceResponse = def fromPiece(piece: Piece): PieceResponse =
PieceResponse(piece.isTome, piece.job.toString, piece.piece) PieceResponse(piece.pieceType.toString, piece.job.toString, piece.piece)
} }

View File

@ -14,7 +14,7 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper} import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId} import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try import scala.util.Try
@ -46,10 +46,10 @@ class BiSView(override val storage: ActorRef, override val ariyala: ActorRef)(im
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post { post {
formFields("player".as[String], "piece".as[String].?, "is_tome".as[String].?, "link".as[String].?, "action".as[String]) { formFields("player".as[String], "piece".as[String].?, "piece_type".as[String].?, "link".as[String].?, "action".as[String]) {
(player, maybePiece, maybeIsTome, maybeLink, action) => (player, maybePiece, maybePieceType, maybeLink, action) =>
onComplete(modifyBiSCall(partyId, player, maybePiece, maybeIsTome, maybeLink, action)) { onComplete(modifyBiSCall(partyId, player, maybePiece, maybePieceType, maybeLink, action)) { _ =>
case _ => redirect(s"/party/$partyId/bis", StatusCodes.Found) redirect(s"/party/$partyId/bis", StatusCodes.Found)
} }
} }
} }
@ -58,25 +58,25 @@ class BiSView(override val storage: ActorRef, override val ariyala: ActorRef)(im
} }
private def modifyBiSCall(partyId: String, player: String, private def modifyBiSCall(partyId: String, player: String,
maybePiece: Option[String], maybeIsTome: Option[String], maybePiece: Option[String], maybePieceType: Option[String],
maybeLink: Option[String], action: String) maybeLink: Option[String], action: String)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
import me.arcanis.ffxivbis.utils.Implicits._ def getPiece(playerId: PlayerId, piece: String, pieceType: String) =
Try(Piece(piece, PieceType.withName(pieceType), playerId.job)).toOption
def getPiece(playerId: PlayerId, piece: String) = def bisAction(playerId: PlayerId, piece: String, pieceType: String)(fn: Piece => Future[Int]) =
Try(Piece(piece, maybeIsTome, playerId.job)).toOption getPiece(playerId, piece, pieceType) match {
case Some(item) => fn(item).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct piece from `$piece ($pieceType)`"))
}
PlayerId(partyId, player) match { PlayerId(partyId, player) match {
case Some(playerId) => (maybePiece, action, maybeLink) match { case Some(playerId) => (maybePiece, maybePieceType, action, maybeLink) match {
case (Some(piece), "add", _) => getPiece(playerId, piece) match { case (Some(piece), Some(pieceType), "add", _) =>
case Some(item) => addPieceBiS(playerId, item).map(_ => ()) bisAction(playerId, piece, pieceType) { item => addPieceBiS(playerId, item) }
case _ => Future.failed(new Error(s"Could not construct piece from `$piece`")) case (Some(piece), Some(pieceType), "remove", _) =>
} bisAction(playerId, piece, pieceType) { item => removePieceBiS(playerId, item) }
case (Some(piece), "remove", _) => getPiece(playerId, piece) match { case (_, _, "create", Some(link)) => putBiS(playerId, link).map(_ => ())
case Some(item) => removePieceBiS(playerId, item).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct piece from `$piece`"))
}
case (_, "create", Some(link)) => putBiS(playerId, link).map(_ => ())
case _ => Future.failed(new Error(s"Could not perform $action")) case _ => Future.failed(new Error(s"Could not perform $action"))
} }
case _ => Future.failed(new Error(s"Could not construct player id from `$player`")) case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
@ -107,8 +107,8 @@ object BiSView {
(for (player <- party) yield option(player.playerId.toString)), (for (player <- party) yield option(player.playerId.toString)),
select(name:="piece", id:="piece", title:="piece") select(name:="piece", id:="piece", title:="piece")
(for (piece <- Piece.available) yield option(piece)), (for (piece <- Piece.available) yield option(piece)),
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"), select(name:="piece_type", id:="piece_type", title:="piece type")
label(`for`:="is_tome")("is tome gear"), (for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="action", id:="action", `type`:="hidden", value:="add"), input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add") input(name:="add", id:="add", `type`:="submit", value:="add")
), ),
@ -125,18 +125,18 @@ object BiSView {
tr( tr(
th("player"), th("player"),
th("piece"), th("piece"),
th("is tome"), th("piece type"),
th("") th("")
), ),
for (player <- party; piece <- player.bis.pieces) yield tr( for (player <- party; piece <- player.bis.pieces) yield tr(
td(`class`:="include_search")(player.playerId.toString), td(`class`:="include_search")(player.playerId.toString),
td(`class`:="include_search")(piece.piece), td(`class`:="include_search")(piece.piece),
td(piece.isTomeToString), td(piece.pieceType.toString),
td( td(
form(action:=s"/party/$partyId/bis", method:="post")( form(action:=s"/party/$partyId/bis", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece), input(name:="piece", id:="piece", `type`:="hidden", value:=piece.piece),
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.isTomeToString), input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.pieceType.toString),
input(name:="action", id:="action", `type`:="hidden", value:="remove"), input(name:="action", id:="action", `type`:="hidden", value:="remove"),
input(name:="remove", id:="remove", `type`:="submit", value:="x") input(name:="remove", id:="remove", `type`:="submit", value:="x")
) )

View File

@ -14,7 +14,7 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, LootHelper} import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import me.arcanis.ffxivbis.models.{Job, Piece, PlayerIdWithCounters} import me.arcanis.ffxivbis.models.{Job, Piece, PieceType, PlayerIdWithCounters}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
@ -43,9 +43,8 @@ class LootSuggestView(override val storage: ActorRef)(implicit timeout: Timeout)
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
post { post {
formFields("piece".as[String], "job".as[String], "is_tome".as[String].?) { (piece, job, maybeTome) => formFields("piece".as[String], "job".as[String], "piece_type".as[String]) { (piece, job, pieceType) =>
import me.arcanis.ffxivbis.utils.Implicits._ val maybePiece = Try(Piece(piece, PieceType.withName(pieceType), Job.withName(job))).toOption
val maybePiece = Try(Piece(piece, maybeTome, Job.withName(job))).toOption
onComplete(suggestLootCall(partyId, maybePiece)) { onComplete(suggestLootCall(partyId, maybePiece)) {
case Success(players) => case Success(players) =>
@ -92,8 +91,8 @@ object LootSuggestView {
(for (piece <- Piece.available) yield option(piece)), (for (piece <- Piece.available) yield option(piece)),
select(name:="job", id:="job", title:="job") select(name:="job", id:="job", title:="job")
(for (job <- Job.availableWithAnyJob) yield option(job.toString)), (for (job <- Job.availableWithAnyJob) yield option(job.toString)),
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"), select(name:="piece_type", id:="piece_type", title:="piece type")
label(`for`:="is_tome")("is tome gear"), (for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest") input(name:="suggest", id:="suggest", `type`:="submit", value:="suggest")
), ),
@ -116,7 +115,7 @@ object LootSuggestView {
form(action:=s"/party/$partyId/loot", method:="post")( form(action:=s"/party/$partyId/loot", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=piece.map(_.piece).getOrElse("")), input(name:="piece", id:="piece", `type`:="hidden", value:=piece.map(_.piece).getOrElse("")),
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=piece.map(_.isTomeToString).getOrElse("")), input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=piece.map(_.pieceType.toString).getOrElse("")),
input(name:="action", id:="action", `type`:="hidden", value:="add"), input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add") input(name:="add", id:="add", `type`:="submit", value:="add")
) )

View File

@ -14,7 +14,7 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, LootHelper} import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId} import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try import scala.util.Try
@ -46,10 +46,10 @@ class LootView (override val storage: ActorRef)(implicit timeout: Timeout)
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post { post {
formFields("player".as[String], "piece".as[String], "is_tome".as[String].?, "action".as[String]) { formFields("player".as[String], "piece".as[String], "piece_type".as[String], "action".as[String]) {
(player, maybePiece, maybeIsTome, action) => (player, piece, pieceType, action) =>
onComplete(modifyLootCall(partyId, player, maybePiece, maybeIsTome, action)) { onComplete(modifyLootCall(partyId, player, piece, pieceType, action)) { _ =>
case _ => redirect(s"/party/$partyId/loot", StatusCodes.Found) redirect(s"/party/$partyId/loot", StatusCodes.Found)
} }
} }
} }
@ -57,20 +57,17 @@ class LootView (override val storage: ActorRef)(implicit timeout: Timeout)
} }
} }
private def modifyLootCall(partyId: String, player: String, private def modifyLootCall(partyId: String, player: String, maybePiece: String,
maybePiece: String, maybeIsTome: Option[String], maybePieceType: String, action: String)
action: String)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = {
import me.arcanis.ffxivbis.utils.Implicits._
def getPiece(playerId: PlayerId) = def getPiece(playerId: PlayerId) =
Try(Piece(maybePiece, maybeIsTome, playerId.job)).toOption Try(Piece(maybePiece, PieceType.withName(maybePieceType), playerId.job)).toOption
PlayerId(partyId, player) match { PlayerId(partyId, player) match {
case Some(playerId) => (getPiece(playerId), action) match { case Some(playerId) => (getPiece(playerId), action) match {
case (Some(piece), "add") => addPieceLoot(playerId, piece).map(_ => ()) case (Some(piece), "add") => addPieceLoot(playerId, piece).map(_ => ())
case (Some(piece), "remove") => removePieceLoot(playerId, piece).map(_ => ()) case (Some(piece), "remove") => removePieceLoot(playerId, piece).map(_ => ())
case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`")) case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`"))
} }
case _ => Future.failed(new Error(s"Could not construct player id from `$player`")) case _ => Future.failed(new Error(s"Could not construct player id from `$player`"))
} }
@ -100,8 +97,8 @@ object LootView {
(for (player <- party) yield option(player.playerId.toString)), (for (player <- party) yield option(player.playerId.toString)),
select(name:="piece", id:="piece", title:="piece") select(name:="piece", id:="piece", title:="piece")
(for (piece <- Piece.available) yield option(piece)), (for (piece <- Piece.available) yield option(piece)),
input(name:="is_tome", id:="is_tome", title:="is tome", `type`:="checkbox"), select(name:="piece_type", id:="piece_type", title:="piece type")
label(`for`:="is_tome")("is tome gear"), (for (pieceType <- PieceType.available) yield option(pieceType.toString)),
input(name:="action", id:="action", `type`:="hidden", value:="add"), input(name:="action", id:="action", `type`:="hidden", value:="add"),
input(name:="add", id:="add", `type`:="submit", value:="add") input(name:="add", id:="add", `type`:="submit", value:="add")
), ),
@ -110,20 +107,20 @@ object LootView {
tr( tr(
th("player"), th("player"),
th("piece"), th("piece"),
th("is tome"), th("piece type"),
th("timestamp"), th("timestamp"),
th("") th("")
), ),
for (player <- party; loot <- player.loot) yield tr( for (player <- party; loot <- player.loot) yield tr(
td(`class`:="include_search")(player.playerId.toString), td(`class`:="include_search")(player.playerId.toString),
td(`class`:="include_search")(loot.piece.piece), td(`class`:="include_search")(loot.piece.piece),
td(loot.piece.isTomeToString), td(loot.piece.pieceType.toString),
td(loot.timestamp.toString), td(loot.timestamp.toString),
td( td(
form(action:=s"/party/$partyId/loot", method:="post")( form(action:=s"/party/$partyId/loot", method:="post")(
input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString), input(name:="player", id:="player", `type`:="hidden", value:=player.playerId.toString),
input(name:="piece", id:="piece", `type`:="hidden", value:=loot.piece.piece), input(name:="piece", id:="piece", `type`:="hidden", value:=loot.piece.piece),
input(name:="is_tome", id:="is_tome", `type`:="hidden", value:=loot.piece.isTomeToString), input(name:="piece_type", id:="piece_type", `type`:="hidden", value:=loot.piece.pieceType.toString),
input(name:="action", id:="action", `type`:="hidden", value:="remove"), input(name:="action", id:="action", `type`:="hidden", value:="remove"),
input(name:="remove", id:="remove", `type`:="submit", value:="x") input(name:="remove", id:="remove", `type`:="submit", value:="x")
) )

View File

@ -101,9 +101,9 @@ object Job {
lazy val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob) lazy val availableWithAnyJob: Seq[Job] = available.prepended(AnyJob)
def withName(job: String): Job.Job = def withName(job: String): Job.Job =
availableWithAnyJob.find(_.toString.equalsIgnoreCase(job.toUpperCase)) match { availableWithAnyJob.find(_.toString.equalsIgnoreCase(job)) match {
case Some(value) => value case Some(value) => value
case None if job.isEmpty => AnyJob case None if job.isEmpty => AnyJob
case _ => throw new IllegalArgumentException("Invalid or unknown job") case _ => throw new IllegalArgumentException(s"Invalid or unknown job $job")
} }
} }

View File

@ -9,15 +9,14 @@
package me.arcanis.ffxivbis.models package me.arcanis.ffxivbis.models
sealed trait Piece { sealed trait Piece {
def isTome: Boolean def pieceType: PieceType.PieceType
def job: Job.Job def job: Job.Job
def piece: String def piece: String
def withJob(other: Job.Job): Piece def withJob(other: Job.Job): Piece
def isTomeToString: String = if (isTome) "yes" else "no"
def upgrade: Option[PieceUpgrade] = this match { def upgrade: Option[PieceUpgrade] = this match {
case _ if !isTome => None case _ if pieceType != PieceType.Tome => None
case _: Waist => Some(AccessoryUpgrade) case _: Waist => Some(AccessoryUpgrade)
case _: PieceAccessory => Some(AccessoryUpgrade) case _: PieceAccessory => Some(AccessoryUpgrade)
case _: PieceBody => Some(BodyUpgrade) case _: PieceBody => Some(BodyUpgrade)
@ -28,59 +27,59 @@ sealed trait Piece {
trait PieceAccessory extends Piece trait PieceAccessory extends Piece
trait PieceBody extends Piece trait PieceBody extends Piece
trait PieceUpgrade extends Piece { trait PieceUpgrade extends Piece {
val isTome: Boolean = true val pieceType: PieceType.PieceType = PieceType.Tome
val job: Job.Job = Job.AnyJob val job: Job.Job = Job.AnyJob
def withJob(other: Job.Job): Piece = this def withJob(other: Job.Job): Piece = this
} }
trait PieceWeapon extends Piece trait PieceWeapon extends Piece
case class Weapon(override val isTome: Boolean, override val job: Job.Job) extends PieceWeapon { case class Weapon(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceWeapon {
val piece: String = "weapon" val piece: String = "weapon"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Head(override val isTome: Boolean, override val job: Job.Job) extends PieceBody { case class Head(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "head" val piece: String = "head"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Body(override val isTome: Boolean, override val job: Job.Job) extends PieceBody { case class Body(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "body" val piece: String = "body"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Hands(override val isTome: Boolean, override val job: Job.Job) extends PieceBody { case class Hands(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "hands" val piece: String = "hands"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Waist(override val isTome: Boolean, override val job: Job.Job) extends PieceBody { case class Waist(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "waist" val piece: String = "waist"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Legs(override val isTome: Boolean, override val job: Job.Job) extends PieceBody { case class Legs(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "legs" val piece: String = "legs"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Feet(override val isTome: Boolean, override val job: Job.Job) extends PieceBody { case class Feet(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceBody {
val piece: String = "feet" val piece: String = "feet"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Ears(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory { case class Ears(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory {
val piece: String = "ears" val piece: String = "ears"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Neck(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory { case class Neck(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory {
val piece: String = "neck" val piece: String = "neck"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Wrist(override val isTome: Boolean, override val job: Job.Job) extends PieceAccessory { case class Wrist(override val pieceType: PieceType.PieceType, override val job: Job.Job) extends PieceAccessory {
val piece: String = "wrist" val piece: String = "wrist"
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
} }
case class Ring(override val isTome: Boolean, override val job: Job.Job, override val piece: String = "ring") case class Ring(override val pieceType: PieceType.PieceType, override val job: Job.Job, override val piece: String = "ring")
extends PieceAccessory { extends PieceAccessory {
def withJob(other: Job.Job): Piece = copy(job = other) def withJob(other: Job.Job): Piece = copy(job = other)
override def equals(obj: Any): Boolean = obj match { override def equals(obj: Any): Boolean = obj match {
case Ring(thatIsTome, thatJob, _) => (thatIsTome == isTome) && (thatJob == job) case Ring(thatPieceType, thatJob, _) => (thatPieceType == pieceType) && (thatJob == job)
case _ => false case _ => false
} }
} }
@ -96,19 +95,19 @@ case object WeaponUpgrade extends PieceUpgrade {
} }
object Piece { object Piece {
def apply(piece: String, isTome: Boolean, job: Job.Job = Job.AnyJob): Piece = def apply(piece: String, pieceType: PieceType.PieceType, job: Job.Job = Job.AnyJob): Piece =
piece.toLowerCase match { piece.toLowerCase match {
case "weapon" => Weapon(isTome, job) case "weapon" => Weapon(pieceType, job)
case "head" => Head(isTome, job) case "head" => Head(pieceType, job)
case "body" => Body(isTome, job) case "body" => Body(pieceType, job)
case "hands" => Hands(isTome, job) case "hands" => Hands(pieceType, job)
case "waist" => Waist(isTome, job) case "waist" => Waist(pieceType, job)
case "legs" => Legs(isTome, job) case "legs" => Legs(pieceType, job)
case "feet" => Feet(isTome, job) case "feet" => Feet(pieceType, job)
case "ears" => Ears(isTome, job) case "ears" => Ears(pieceType, job)
case "neck" => Neck(isTome, job) case "neck" => Neck(pieceType, job)
case "wrist" => Wrist(isTome, job) case "wrist" => Wrist(pieceType, job)
case ring @ ("ring" | "left ring" | "right ring") => Ring(isTome, job, ring) case ring @ ("ring" | "left ring" | "right ring") => Ring(pieceType, job, ring)
case "accessory upgrade" => AccessoryUpgrade case "accessory upgrade" => AccessoryUpgrade
case "body upgrade" => BodyUpgrade case "body upgrade" => BodyUpgrade
case "weapon upgrade" => WeaponUpgrade case "weapon upgrade" => WeaponUpgrade

View File

@ -0,0 +1,18 @@
package me.arcanis.ffxivbis.models
object PieceType {
sealed trait PieceType
case object Crafted extends PieceType
case object Tome extends PieceType
case object Savage extends PieceType
lazy val available: Seq[PieceType] =
Seq(Crafted, Tome, Savage)
def withName(pieceType: String): PieceType =
available.find(_.toString.equalsIgnoreCase(pieceType)) match {
case Some(value) => value
case _ => throw new IllegalArgumentException(s"Invalid or unknown piece type $pieceType")
}
}

View File

@ -46,7 +46,7 @@ case class Player(id: Long,
} }
} }
def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(!_.isTome) def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(_.pieceType == PieceType.Savage)
def lootCount(piece: Option[Piece]): Int = piece match { def lootCount(piece: Option[Piece]): Int = piece match {
case Some(p) => loot.count(_.piece == p) case Some(p) => loot.count(_.piece == p)
case None => lootCountTotal(piece) case None => lootCountTotal(piece)

View File

@ -18,7 +18,7 @@ import akka.stream.ActorMaterializer
import akka.stream.scaladsl.{Keep, Sink} import akka.stream.scaladsl.{Keep, Sink}
import akka.util.ByteString import akka.util.ByteString
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.models.{BiS, Job, Piece} import me.arcanis.ffxivbis.models.{BiS, Job, Piece, PieceType}
import spray.json._ import spray.json._
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
@ -54,10 +54,10 @@ class Ariyala extends Actor with StrictLogging {
.withPath(Uri.Path / "store.app") .withPath(Uri.Path / "store.app")
.withQuery(Uri.Query(Map("identifier" -> id))) .withQuery(Uri.Query(Map("identifier" -> id)))
sendRequest(uri, Ariyala.parseAriyalaJsonToPieces(job, getIsTome)) sendRequest(uri, Ariyala.parseAriyalaJsonToPieces(job, getPieceType))
} }
private def getIsTome(itemIds: Seq[Long]): Future[Map[Long, Boolean]] = { private def getPieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType.PieceType]] = {
val uriForItems = Uri(xivapiUrl) val uriForItems = Uri(xivapiUrl)
.withPath(Uri.Path / "item") .withPath(Uri.Path / "item")
.withQuery(Uri.Query(Map( .withQuery(Uri.Query(Map(
@ -77,7 +77,7 @@ class Ariyala extends Actor with StrictLogging {
"private_key" -> xivapiKey.getOrElse("") "private_key" -> xivapiKey.getOrElse("")
))) )))
sendRequest(uriForShops, Ariyala.parseXivapiJsonToTome(shops)) sendRequest(uriForShops, Ariyala.parseXivapiJsonToType(shops))
} }
} }
@ -120,28 +120,34 @@ object Ariyala {
} }
} }
private def parseAriyalaJsonToPieces(job: Job.Job, isTome: Seq[Long] => Future[Map[Long, Boolean]])(js: JsObject) private def parseAriyalaJsonToPieces(job: Job.Job, pieceTypes: Seq[Long] => Future[Map[Long, PieceType.PieceType]])
(js: JsObject)
(implicit executionContext: ExecutionContext): Future[Seq[Piece]] = (implicit executionContext: ExecutionContext): Future[Seq[Piece]] =
parseAriyalaJson(job)(js).flatMap { pieces => parseAriyalaJson(job)(js).flatMap { pieces =>
isTome(pieces.values.toSeq).map { tomePieces => pieceTypes(pieces.values.toSeq).map { types =>
pieces.view.mapValues(tomePieces).map { pieces.view.mapValues(types).map {
case (piece, isTomePiece) => Piece(piece, isTomePiece, job) case (piece, pieceType) => Piece(piece, pieceType, job)
}.toSeq }.toSeq
} }
} }
private def parseXivapiJsonToShop(js: JsObject) private def parseXivapiJsonToShop(js: JsObject)
(implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = { (implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = {
def extractTraderId(js: JsObject) = def extractTraderId(js: JsObject) = {
js.fields("SpecialShop").asJsObject js.fields
.fields.collectFirst { .get("Recipe").map(_ => "crafted" -> -1L) // you can craft this item
case (shopName, JsArray(array)) if shopName.startsWith("ItemReceive") => .orElse { // lets try shop items
val shopId = array.head match { js.fields("SpecialShop").asJsObject
case JsNumber(id) => id.toLong .fields.collectFirst {
case other => throw deserializationError(s"Could not parse $other") case (shopName, JsArray(array)) if shopName.startsWith("ItemReceive") =>
val shopId = array.head match {
case JsNumber(id) => id.toLong
case other => throw deserializationError(s"Could not parse $other")
}
shopName.replace("ItemReceive", "") -> shopId
} }
(shopName.replace("ItemReceive", ""), shopId) }.getOrElse(throw deserializationError(s"Could not parse $js"))
}.getOrElse(throw deserializationError(s"Could not parse $js")) }
Future { Future {
js.fields("Results") match { js.fields("Results") match {
@ -155,8 +161,8 @@ object Ariyala {
} }
} }
private def parseXivapiJsonToTome(shops: Map[Long, (String, Long)])(js: JsObject) private def parseXivapiJsonToType(shops: Map[Long, (String, Long)])(js: JsObject)
(implicit executionContext: ExecutionContext): Future[Map[Long, Boolean]] = (implicit executionContext: ExecutionContext): Future[Map[Long, PieceType.PieceType]] =
Future { Future {
val shopMap = js.fields("Results") match { val shopMap = js.fields("Results") match {
case array: JsArray => case array: JsArray =>
@ -170,14 +176,20 @@ object Ariyala {
} }
shops.map { case (itemId, (index, shopId)) => shops.map { case (itemId, (index, shopId)) =>
val isTome = Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject).toOption.getOrElse(throw new Exception(s"${shopMap(shopId).fields(s"ItemCost$index")}, $index")) val pieceType =
.getFields("IsUnique", "StackSize") match { if (index == "crafted" && shopId == -1) PieceType.Crafted
case Seq(JsNumber(isUnique), JsNumber(stackSize)) => else {
// either upgraded gear or tomes found Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject)
isUnique == 1 || stackSize.toLong == 2000 .toOption
case other => throw deserializationError(s"Could not parse $other") .getOrElse(throw new Exception(s"${shopMap(shopId).fields(s"ItemCost$index")}, $index"))
} .getFields("IsUnique", "StackSize") match {
itemId -> isTome case Seq(JsNumber(isUnique), JsNumber(stackSize)) =>
if (isUnique == 1 || stackSize.toLong == 2000) PieceType.Tome // either upgraded gear or tomes found
else PieceType.Savage
case other => throw deserializationError(s"Could not parse $other")
}
}
itemId -> pieceType
} }
} }

View File

@ -10,7 +10,7 @@ package me.arcanis.ffxivbis.storage
import java.time.Instant import java.time.Instant
import me.arcanis.ffxivbis.models.{Job, Loot, Piece} import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType}
import slick.lifted.ForeignKeyQuery import slick.lifted.ForeignKeyQuery
import scala.concurrent.Future import scala.concurrent.Future
@ -18,24 +18,27 @@ import scala.concurrent.Future
trait BiSProfile { this: DatabaseProfile => trait BiSProfile { this: DatabaseProfile =>
import dbConfig.profile.api._ import dbConfig.profile.api._
case class BiSRep(playerId: Long, created: Long, piece: String, isTome: Int, job: String) { case class BiSRep(playerId: Long, created: Long, piece: String,
def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job)), Instant.ofEpochMilli(created)) pieceType: String, job: String) {
def toLoot: Loot = Loot(
playerId, Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
Instant.ofEpochMilli(created))
} }
object BiSRep { object BiSRep {
def fromPiece(playerId: Long, piece: Piece) = def fromPiece(playerId: Long, piece: Piece): BiSRep =
BiSRep(playerId, DatabaseProfile.now, piece.piece, if (piece.isTome) 1 else 0, BiSRep(playerId, DatabaseProfile.now, piece.piece,
piece.job.toString) piece.pieceType.toString, piece.job.toString)
} }
class BiSPieces(tag: Tag) extends Table[BiSRep](tag, "bis") { class BiSPieces(tag: Tag) extends Table[BiSRep](tag, "bis") {
def playerId: Rep[Long] = column[Long]("player_id", O.PrimaryKey) def playerId: Rep[Long] = column[Long]("player_id", O.PrimaryKey)
def created: Rep[Long] = column[Long]("created") def created: Rep[Long] = column[Long]("created")
def piece: Rep[String] = column[String]("piece", O.PrimaryKey) def piece: Rep[String] = column[String]("piece", O.PrimaryKey)
def isTome: Rep[Int] = column[Int]("is_tome") def pieceType: Rep[String] = column[String]("piece_type")
def job: Rep[String] = column[String]("job") def job: Rep[String] = column[String]("job")
def * = def * =
(playerId, created, piece, isTome, job) <> ((BiSRep.apply _).tupled, BiSRep.unapply) (playerId, created, piece, pieceType, job) <> ((BiSRep.apply _).tupled, BiSRep.unapply)
def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] = def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] =
foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade) foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade)

View File

@ -10,7 +10,7 @@ package me.arcanis.ffxivbis.storage
import java.time.Instant import java.time.Instant
import me.arcanis.ffxivbis.models.{Job, Loot, Piece} import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType}
import slick.lifted.{ForeignKeyQuery, Index} import slick.lifted.{ForeignKeyQuery, Index}
import scala.concurrent.Future import scala.concurrent.Future
@ -18,14 +18,17 @@ import scala.concurrent.Future
trait LootProfile { this: DatabaseProfile => trait LootProfile { this: DatabaseProfile =>
import dbConfig.profile.api._ import dbConfig.profile.api._
case class LootRep(lootId: Option[Long], playerId: Long, created: Long, piece: String, case class LootRep(lootId: Option[Long], playerId: Long, created: Long,
isTome: Int, job: String) { piece: String, pieceType: String, job: String) {
def toLoot: Loot = Loot(playerId, Piece(piece, isTome == 1, Job.withName(job)), Instant.ofEpochMilli(created)) def toLoot: Loot = Loot(
playerId,
Piece(piece, PieceType.withName(pieceType), Job.withName(job)),
Instant.ofEpochMilli(created))
} }
object LootRep { object LootRep {
def fromPiece(playerId: Long, piece: Piece) = def fromPiece(playerId: Long, piece: Piece): LootRep =
LootRep(None, playerId, DatabaseProfile.now, piece.piece, if (piece.isTome) 1 else 0, LootRep(None, playerId, DatabaseProfile.now, piece.piece,
piece.job.toString) piece.pieceType.toString, piece.job.toString)
} }
class LootPieces(tag: Tag) extends Table[LootRep](tag, "loot") { class LootPieces(tag: Tag) extends Table[LootRep](tag, "loot") {
@ -33,11 +36,11 @@ trait LootProfile { this: DatabaseProfile =>
def playerId: Rep[Long] = column[Long]("player_id") def playerId: Rep[Long] = column[Long]("player_id")
def created: Rep[Long] = column[Long]("created") def created: Rep[Long] = column[Long]("created")
def piece: Rep[String] = column[String]("piece") def piece: Rep[String] = column[String]("piece")
def isTome: Rep[Int] = column[Int]("is_tome") def pieceType: Rep[String] = column[String]("piece_type")
def job: Rep[String] = column[String]("job") def job: Rep[String] = column[String]("job")
def * = def * =
(lootId.?, playerId, created, piece, isTome, job) <> ((LootRep.apply _).tupled, LootRep.unapply) (lootId.?, playerId, created, piece, pieceType, job) <> ((LootRep.apply _).tupled, LootRep.unapply)
def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] = def fkPlayerId: ForeignKeyQuery[Players, PlayerRep] =
foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade) foreignKey("player_id", playerId, playersTable)(_.playerId, onDelete = ForeignKeyAction.Cascade)

View File

@ -15,7 +15,8 @@ import scala.concurrent.Future
trait PartyProfile { this: DatabaseProfile => trait PartyProfile { this: DatabaseProfile =>
import dbConfig.profile.api._ import dbConfig.profile.api._
case class PartyRep(partyId: Option[Long], partyName: String, partyAlias: Option[String]) { case class PartyRep(partyId: Option[Long], partyName: String,
partyAlias: Option[String]) {
def toDescription: PartyDescription = PartyDescription(partyName, partyAlias) def toDescription: PartyDescription = PartyDescription(partyName, partyAlias)
} }
object PartyRep { object PartyRep {

View File

@ -15,10 +15,11 @@ import scala.concurrent.Future
trait PlayersProfile { this: DatabaseProfile => trait PlayersProfile { this: DatabaseProfile =>
import dbConfig.profile.api._ import dbConfig.profile.api._
case class PlayerRep(partyId: String, playerId: Option[Long], created: Long, nick: String, case class PlayerRep(partyId: String, playerId: Option[Long], created: Long,
job: String, link: Option[String], priority: Int) { nick: String, job: String, link: Option[String], priority: Int) {
def toPlayer: Player = def toPlayer: Player =
Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick, BiS(Seq.empty), List.empty, link, priority) Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick,
BiS(Seq.empty), List.empty, link, priority)
} }
object PlayerRep { object PlayerRep {
def fromPlayer(player: Player, id: Option[Long]): PlayerRep = def fromPlayer(player: Player, id: Option[Long]): PlayerRep =

View File

@ -16,8 +16,8 @@ import scala.concurrent.Future
trait UsersProfile { this: DatabaseProfile => trait UsersProfile { this: DatabaseProfile =>
import dbConfig.profile.api._ import dbConfig.profile.api._
case class UserRep(partyId: String, userId: Option[Long], username: String, password: String, case class UserRep(partyId: String, userId: Option[Long], username: String,
permission: String) { password: String, permission: String) {
def toUser: User = User(partyId, username, password, Permission.withName(permission)) def toUser: User = User(partyId, username, password, Permission.withName(permission))
} }
object UserRep { object UserRep {

View File

@ -5,33 +5,33 @@ import me.arcanis.ffxivbis.models._
object Fixtures { object Fixtures {
lazy val bis: BiS = BiS( lazy val bis: BiS = BiS(
Seq( Seq(
Weapon(isTome = false ,Job.DNC), Weapon(pieceType = PieceType.Savage ,Job.DNC),
Head(isTome = false, Job.DNC), Head(pieceType = PieceType.Savage, Job.DNC),
Body(isTome = false, Job.DNC), Body(pieceType = PieceType.Savage, Job.DNC),
Hands(isTome = true, Job.DNC), Hands(pieceType = PieceType.Tome, Job.DNC),
Waist(isTome = true, Job.DNC), Waist(pieceType = PieceType.Tome, Job.DNC),
Legs(isTome = true, Job.DNC), Legs(pieceType = PieceType.Tome, Job.DNC),
Feet(isTome = false, Job.DNC), Feet(pieceType = PieceType.Savage, Job.DNC),
Ears(isTome = false, Job.DNC), Ears(pieceType = PieceType.Savage, Job.DNC),
Neck(isTome = true, Job.DNC), Neck(pieceType = PieceType.Tome, Job.DNC),
Wrist(isTome = false, Job.DNC), Wrist(pieceType = PieceType.Savage, Job.DNC),
Ring(isTome = true, Job.DNC, "left ring"), Ring(pieceType = PieceType.Tome, Job.DNC, "left ring"),
Ring(isTome = true, Job.DNC, "right ring") Ring(pieceType = PieceType.Tome, Job.DNC, "right ring")
) )
) )
lazy val link: String = "https://ffxiv.ariyala.com/19V5R" lazy val link: String = "https://ffxiv.ariyala.com/19V5R"
lazy val link2: String = "https://ffxiv.ariyala.com/1A0WM" lazy val link2: String = "https://ffxiv.ariyala.com/1A0WM"
lazy val lootWeapon: Piece = Weapon(isTome = true, Job.AnyJob) lazy val lootWeapon: Piece = Weapon(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootBody: Piece = Body(isTome = false, Job.AnyJob) lazy val lootBody: Piece = Body(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootHands: Piece = Hands(isTome = true, Job.AnyJob) lazy val lootHands: Piece = Hands(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootWaist: Piece = Waist(isTome = true, Job.AnyJob) lazy val lootWaist: Piece = Waist(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootLegs: Piece = Legs(isTome = false, Job.AnyJob) lazy val lootLegs: Piece = Legs(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootEars: Piece = Ears(isTome = false, Job.AnyJob) lazy val lootEars: Piece = Ears(pieceType = PieceType.Savage, Job.AnyJob)
lazy val lootRing: Piece = Ring(isTome = true, Job.AnyJob) lazy val lootRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob)
lazy val lootLeftRing: Piece = Ring(isTome = true, Job.AnyJob, "left ring") lazy val lootLeftRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob, "left ring")
lazy val lootRightRing: Piece = Ring(isTome = true, Job.AnyJob, "right ring") lazy val lootRightRing: Piece = Ring(pieceType = PieceType.Tome, Job.AnyJob, "right ring")
lazy val lootUpgrade: Piece = BodyUpgrade lazy val lootUpgrade: Piece = BodyUpgrade
lazy val loot: Seq[Piece] = Seq(lootBody, lootHands, lootLegs, lootUpgrade) lazy val loot: Seq[Piece] = Seq(lootBody, lootHands, lootLegs, lootUpgrade)

View File

@ -6,7 +6,7 @@ import akka.http.scaladsl.server._
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.Settings import me.arcanis.ffxivbis.Settings
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece} import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType}
import org.scalatest.{Matchers, WordSpec} import org.scalatest.{Matchers, WordSpec}
import scala.language.postfixOps import scala.language.postfixOps
@ -41,6 +41,13 @@ class TypesEndpointTest extends WordSpec
} }
} }
"return all available piece types" in {
Get("/types/pieces/types") ~> route ~> check {
status shouldEqual StatusCodes.OK
responseAs[Seq[String]] shouldEqual PieceType.available.map(_.toString)
}
}
"return current priority" in { "return current priority" in {
Get("/types/priority") ~> route ~> check { Get("/types/priority") ~> route ~> check {
status shouldEqual StatusCodes.OK status shouldEqual StatusCodes.OK

View File

@ -7,11 +7,6 @@ class PieceTest extends WordSpecLike with Matchers with BeforeAndAfterAll {
"piece model" must { "piece model" must {
"convert `isTome` property to string" in {
Fixtures.lootBody.isTomeToString shouldEqual "no"
Fixtures.lootHands.isTomeToString shouldEqual "yes"
}
"return upgrade" in { "return upgrade" in {
Fixtures.lootWeapon.upgrade shouldEqual Some(WeaponUpgrade) Fixtures.lootWeapon.upgrade shouldEqual Some(WeaponUpgrade)
Fixtures.lootBody.upgrade shouldEqual None Fixtures.lootBody.upgrade shouldEqual None
@ -25,7 +20,7 @@ class PieceTest extends WordSpecLike with Matchers with BeforeAndAfterAll {
"build piece from string" in { "build piece from string" in {
Fixtures.bis.pieces.foreach { piece => Fixtures.bis.pieces.foreach { piece =>
Piece(piece.piece, piece.isTome, piece.job) shouldEqual piece Piece(piece.piece, piece.pieceType, piece.job) shouldEqual piece
} }
} }

View File

@ -0,0 +1,20 @@
package me.arcanis.ffxivbis.models
import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
class PieceTypeTest extends WordSpecLike with Matchers with BeforeAndAfterAll {
"piece type model" must {
"create piece type from string" in {
PieceType.available.foreach { pieceType =>
PieceType.withName(pieceType.toString) shouldEqual pieceType
}
}
"fail on unknown piece type" in {
an [IllegalArgumentException] should be thrownBy PieceType.withName("random string")
}
}
}

View File

@ -65,7 +65,7 @@ class DatabaseBiSHandlerTest
} }
"update piece in bis set" in { "update piece in bis set" in {
val newPiece = Hands(isTome = false, Job.DNC) val newPiece = Hands(pieceType = PieceType.Savage, Job.DNC)
database ! impl.DatabaseBiSHandler.AddPieceToBis(Fixtures.playerEmpty.playerId, newPiece) database ! impl.DatabaseBiSHandler.AddPieceToBis(Fixtures.playerEmpty.playerId, newPiece)
expectMsg(timeout, 1) expectMsg(timeout, 1)

View File

@ -37,11 +37,11 @@ class LootSelectorTest extends TestKit(ActorSystem("lootselector"))
"loot selector" must { "loot selector" must {
"suggest loot by isRequired" in { "suggest loot by isRequired" in {
toPlayerId(default.suggestLoot(Head(isTome = false, Job.AnyJob))) shouldEqual Seq(dnc.playerId, drg.playerId) toPlayerId(default.suggestLoot(Head(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(dnc.playerId, drg.playerId)
} }
"suggest loot if a player already have it" in { "suggest loot if a player already have it" in {
val piece = Body(isTome = false, Job.AnyJob) val piece = Body(pieceType = PieceType.Savage, Job.AnyJob)
val party = default.withPlayer(dnc.withLoot(piece)) val party = default.withPlayer(dnc.withLoot(piece))
toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId) toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId)
@ -50,7 +50,7 @@ class LootSelectorTest extends TestKit(ActorSystem("lootselector"))
"suggest upgrade" in { "suggest upgrade" in {
val party = default.withPlayer( val party = default.withPlayer(
dnc.withBiS( dnc.withBiS(
Some(dnc.bis.copy(weapon = Some(Weapon(isTome = true, Job.DNC)))) Some(dnc.bis.copy(weapon = Some(Weapon(pieceType = PieceType.Tome, Job.DNC))))
) )
) )
@ -60,24 +60,24 @@ class LootSelectorTest extends TestKit(ActorSystem("lootselector"))
"suggest loot by priority" in { "suggest loot by priority" in {
val party = default.withPlayer(dnc.copy(priority = 2)) val party = default.withPlayer(dnc.copy(priority = 2))
toPlayerId(party.suggestLoot(Body(isTome = false, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId) toPlayerId(party.suggestLoot(Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
} }
"suggest loot by bis pieces got" in { "suggest loot by bis pieces got" in {
val party = default.withPlayer(dnc.withLoot(Head(isTome = false, Job.AnyJob))) val party = default.withPlayer(dnc.withLoot(Head(pieceType = PieceType.Savage, Job.AnyJob)))
toPlayerId(party.suggestLoot(Body(isTome = false, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId) toPlayerId(party.suggestLoot(Body(pieceType = PieceType.Savage, Job.AnyJob))) shouldEqual Seq(drg.playerId, dnc.playerId)
} }
"suggest loot by this piece got" in { "suggest loot by this piece got" in {
val piece = Body(isTome = true, Job.AnyJob) val piece = Body(pieceType = PieceType.Tome, Job.AnyJob)
val party = default.withPlayer(dnc.withLoot(piece)) val party = default.withPlayer(dnc.withLoot(piece))
toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId) toPlayerId(party.suggestLoot(piece)) shouldEqual Seq(drg.playerId, dnc.playerId)
} }
"suggest loot by total piece got" in { "suggest loot by total piece got" in {
val piece = Body(isTome = true, Job.AnyJob) val piece = Body(pieceType = PieceType.Tome, Job.AnyJob)
val party = default val party = default
.withPlayer(dnc.withLoot(Seq(piece, piece).map(pieceToLoot))) .withPlayer(dnc.withLoot(Seq(piece, piece).map(pieceToLoot)))
.withPlayer(drg.withLoot(piece)) .withPlayer(drg.withLoot(piece))