mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 14:38:32 +08:00
Only huntable items can now be added to the wanted list.
This commit is contained in:
parent
321fb3a475
commit
aeded71dde
@ -16,6 +16,7 @@ subprojects {
|
||||
project.extra["coroutinesVersion"] = "1.4.2"
|
||||
project.extra["kotlinLoggingVersion"] = "2.0.2"
|
||||
project.extra["ktorVersion"] = "1.4.3"
|
||||
project.extra["log4jVersion"] = "2.14.0"
|
||||
project.extra["serializationVersion"] = "1.0.1"
|
||||
project.extra["slf4jVersion"] = "1.7.30"
|
||||
|
||||
|
@ -15,6 +15,13 @@ fun <E> MutableList<E>.replaceAll(elements: Sequence<E>): Boolean {
|
||||
return addAll(elements)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove [n] elements at [startIndex].
|
||||
*/
|
||||
fun <E> MutableList<E>.removeAt(startIndex: Int, n: Int) {
|
||||
repeat(n) { removeAt(startIndex) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace [amount] elements at [startIndex] with [elements].
|
||||
*/
|
||||
|
@ -5,6 +5,12 @@ enum class Episode {
|
||||
II,
|
||||
IV;
|
||||
|
||||
fun toInt(): Int = when(this) {
|
||||
I -> 1
|
||||
II -> 2
|
||||
IV -> 4
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromInt(episode: Int) = when (episode) {
|
||||
1 -> I
|
||||
|
@ -0,0 +1,16 @@
|
||||
package world.phantasmal.lib.fileFormats.quest
|
||||
|
||||
import world.phantasmal.lib.Episode
|
||||
|
||||
private val EP_AND_NAME_TO_NPC_TYPE: Map<Pair<String, Episode>, NpcType> =
|
||||
mutableMapOf<Pair<String, Episode>, NpcType>().also { map ->
|
||||
for (npcType in NpcType.VALUES) {
|
||||
if (npcType.episode != null) {
|
||||
map[Pair(npcType.simpleName, npcType.episode)] = npcType
|
||||
map[Pair(npcType.ultimateName, npcType.episode)] = npcType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun NpcType.Companion.fromNameAndEpisode(name: String, episode: Episode): NpcType? =
|
||||
EP_AND_NAME_TO_NPC_TYPE[Pair(name, episode)]
|
@ -199,5 +199,12 @@ actual class Buffer private constructor(
|
||||
|
||||
actual fun fromBase64(data: String, endianness: Endianness): Buffer =
|
||||
fromByteArray(Base64.getDecoder().decode(data), endianness)
|
||||
|
||||
fun fromResource(name: String): Buffer {
|
||||
val stream = (Buffer::class.java.getResourceAsStream(name)
|
||||
?: error("""Couldn't load resource "$name"."""))
|
||||
|
||||
return stream.use { fromByteArray(it.readBytes()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,12 +6,5 @@ import world.phantasmal.lib.buffer.Buffer
|
||||
import world.phantasmal.lib.cursor.Cursor
|
||||
import world.phantasmal.lib.cursor.cursor
|
||||
|
||||
actual suspend fun readFile(path: String): Cursor {
|
||||
val stream = {}::class.java.getResourceAsStream(path)
|
||||
?: error("""Couldn't load resource "$path".""")
|
||||
|
||||
stream.use {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
return Buffer.fromByteArray(it.readAllBytes()).cursor()
|
||||
}
|
||||
}
|
||||
actual suspend fun readFile(path: String): Cursor =
|
||||
Buffer.fromResource(path).cursor()
|
||||
|
@ -12,9 +12,15 @@ tasks.withType<KotlinCompile> {
|
||||
}
|
||||
}
|
||||
|
||||
val kotlinLoggingVersion: String by project.extra
|
||||
val log4jVersion: String by project.extra
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib"))
|
||||
implementation(project(":web:shared"))
|
||||
implementation("org.jsoup:jsoup:1.13.1")
|
||||
implementation("io.github.microutils:kotlin-logging:$kotlinLoggingVersion")
|
||||
runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion")
|
||||
}
|
||||
|
||||
tasks.register<JavaExec>("generateAssets") {
|
||||
@ -22,6 +28,6 @@ tasks.register<JavaExec>("generateAssets") {
|
||||
outputs.dir(outputFile)
|
||||
|
||||
classpath = sourceSets.main.get().runtimeClasspath
|
||||
main = "world.phantasmal.web.assetsGeneration.Main"
|
||||
main = "world.phantasmal.web.assetsGeneration.MainKt"
|
||||
args = listOf(outputFile.absolutePath)
|
||||
}
|
||||
|
@ -1,219 +0,0 @@
|
||||
package world.phantasmal.web.assetsGeneration
|
||||
|
||||
import kotlinx.serialization.encodeToString
|
||||
import world.phantasmal.core.splice
|
||||
import world.phantasmal.lib.buffer.Buffer
|
||||
import world.phantasmal.lib.compression.prs.prsDecompress
|
||||
import world.phantasmal.lib.cursor.cursor
|
||||
import world.phantasmal.lib.fileFormats.ItemPmt
|
||||
import world.phantasmal.lib.fileFormats.parseItemPmt
|
||||
import world.phantasmal.lib.fileFormats.parseUnitxt
|
||||
import world.phantasmal.web.shared.JSON_FORMAT_PRETTY
|
||||
import world.phantasmal.web.shared.dto.*
|
||||
import java.io.File
|
||||
import java.util.Comparator.comparing
|
||||
|
||||
object Ephinea {
|
||||
/**
|
||||
* ItemPMT.bin and ItemPT.gsl comes from stock Tethealla. ItemPT.gsl is not used at the moment.
|
||||
* unitxt_j.prs comes from the Ephinea client.
|
||||
* TODO: manual fixes:
|
||||
* - Clio is equipable by HUnewearls
|
||||
* - Red Ring has a requirement of 180, not 108
|
||||
*/
|
||||
fun generateAssets(outputDir: File) {
|
||||
val items = loadItems(loadItemNames())
|
||||
|
||||
File(outputDir, "item_types.ephinea.json")
|
||||
.writeText(JSON_FORMAT_PRETTY.encodeToString(items))
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts item names from unitxt file.
|
||||
*/
|
||||
private fun loadItemNames(): List<String> {
|
||||
val unitxtBuffer =
|
||||
object {}::class.java.getResourceAsStream(
|
||||
"/ephinea/client/data/unitxt_j.prs"
|
||||
).use { Buffer.fromByteArray(it.readBytes()) }
|
||||
|
||||
val unitxt = parseUnitxt(prsDecompress(unitxtBuffer.cursor()).unwrap())
|
||||
|
||||
val itemNames = unitxt.categories[1].toMutableList()
|
||||
// Strip custom Ephinea items until we have the Ephinea ItemPMT.bin.
|
||||
itemNames.splice(177, 50, emptyList())
|
||||
itemNames.splice(639, 59, emptyList())
|
||||
|
||||
return itemNames
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads items from ItemPMT.
|
||||
*/
|
||||
private fun loadItems(itemNames: List<String>): List<ItemType> {
|
||||
val itemPmtBuffer =
|
||||
object {}::class.java.getResourceAsStream(
|
||||
"/ephinea/ship-config/param/ItemPMT.bin"
|
||||
).use { Buffer.fromByteArray(it.readBytes()) }
|
||||
|
||||
val itemPmt = parseItemPmt(itemPmtBuffer.cursor())
|
||||
val itemTypes = mutableListOf<ItemType>()
|
||||
val ids = mutableSetOf<Int>()
|
||||
|
||||
fun checkId(id: Int, type: String, name: String) {
|
||||
check(ids.add(id)) {
|
||||
"""Trying to add $type with ID $id ($name) but ID already exists."""
|
||||
}
|
||||
}
|
||||
|
||||
for ((categoryI, category) in itemPmt.weapons.withIndex()) {
|
||||
for ((i, weapon) in category.withIndex()) {
|
||||
val id = (categoryI shl 8) + i
|
||||
val name = itemNames[weapon.id]
|
||||
checkId(id, "weapon", name)
|
||||
|
||||
itemTypes.add(WeaponItemType(
|
||||
id,
|
||||
name,
|
||||
weapon.minAtp,
|
||||
weapon.maxAtp,
|
||||
weapon.ata,
|
||||
weapon.maxGrind,
|
||||
weapon.reqAtp,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
for ((i, frame) in itemPmt.frames.withIndex()) {
|
||||
val id = 0x10100 + i
|
||||
val name = itemNames[frame.id]
|
||||
checkId(id, "frame", name)
|
||||
|
||||
val stats = getStatBoosts(itemPmt, frame.statBoost)
|
||||
|
||||
itemTypes.add(FrameItemType(
|
||||
id,
|
||||
name,
|
||||
stats.atp,
|
||||
stats.ata,
|
||||
minEvp = frame.evp + stats.minEvp,
|
||||
maxEvp = frame.evp + stats.minEvp + frame.evpRange,
|
||||
minDfp = frame.dfp + stats.minDfp,
|
||||
maxDfp = frame.dfp + stats.minDfp + frame.dfpRange,
|
||||
stats.mst,
|
||||
stats.hp,
|
||||
stats.lck,
|
||||
))
|
||||
}
|
||||
|
||||
for ((i, barrier) in itemPmt.barriers.withIndex()) {
|
||||
val id = 0x10200 + i
|
||||
val name = itemNames[barrier.id]
|
||||
checkId(id, "barrier", name)
|
||||
|
||||
val stats = getStatBoosts(itemPmt, barrier.statBoost)
|
||||
|
||||
itemTypes.add(BarrierItemType(
|
||||
id,
|
||||
name,
|
||||
stats.atp,
|
||||
stats.ata,
|
||||
minEvp = barrier.evp + stats.minEvp,
|
||||
maxEvp = barrier.evp + stats.minEvp + barrier.evpRange,
|
||||
minDfp = barrier.dfp + stats.minDfp,
|
||||
maxDfp = barrier.dfp + stats.minDfp + barrier.dfpRange,
|
||||
stats.mst,
|
||||
stats.hp,
|
||||
stats.lck,
|
||||
))
|
||||
}
|
||||
|
||||
for ((i, unit) in itemPmt.units.withIndex()) {
|
||||
val id = 0x10300 + i
|
||||
val name = itemNames[unit.id]
|
||||
checkId(id, "unit", name)
|
||||
|
||||
itemTypes.add(UnitItemType(
|
||||
id,
|
||||
name,
|
||||
))
|
||||
}
|
||||
|
||||
for ((categoryI, category) in itemPmt.tools.withIndex()) {
|
||||
for ((i, tool) in category.withIndex()) {
|
||||
val id = (0x30000 or (categoryI shl 8)) + i
|
||||
val name = itemNames[tool.id]
|
||||
checkId(id, "tool", name)
|
||||
|
||||
itemTypes.add(ToolItemType(
|
||||
id,
|
||||
name,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
itemTypes.sortWith(comparing({ it.name }, String.CASE_INSENSITIVE_ORDER))
|
||||
|
||||
return itemTypes
|
||||
}
|
||||
}
|
||||
|
||||
private class Boosts(
|
||||
val atp: Int,
|
||||
val ata: Int,
|
||||
val minEvp: Int,
|
||||
val minDfp: Int,
|
||||
val mst: Int,
|
||||
val hp: Int,
|
||||
val lck: Int,
|
||||
)
|
||||
|
||||
private fun getStatBoosts(itemPmt: ItemPmt, index: Int): Boosts {
|
||||
val statBoosts = itemPmt.statBoosts[index]
|
||||
val amount = statBoosts.amount1
|
||||
|
||||
var atp = 0
|
||||
var ata = 0
|
||||
var minEvp = 0
|
||||
var minDfp = 0
|
||||
var mst = 0
|
||||
var hp = 0
|
||||
var lck = 0
|
||||
|
||||
when (statBoosts.stat1) {
|
||||
1 -> atp += amount
|
||||
2 -> ata += amount
|
||||
3 -> minEvp += amount
|
||||
4 -> minDfp += amount
|
||||
5 -> mst += amount
|
||||
6 -> hp += amount
|
||||
7 -> lck += amount
|
||||
8 -> {
|
||||
atp += amount
|
||||
ata += amount
|
||||
minEvp += amount
|
||||
minDfp += amount
|
||||
mst += amount
|
||||
hp += amount
|
||||
lck += amount
|
||||
}
|
||||
9 -> atp -= amount
|
||||
10 -> ata -= amount
|
||||
11 -> minEvp -= amount
|
||||
12 -> minDfp -= amount
|
||||
13 -> mst -= amount
|
||||
14 -> hp -= amount
|
||||
15 -> lck -= amount
|
||||
16 -> {
|
||||
atp -= amount
|
||||
ata -= amount
|
||||
minEvp -= amount
|
||||
minDfp -= amount
|
||||
mst -= amount
|
||||
hp -= amount
|
||||
lck -= amount
|
||||
}
|
||||
}
|
||||
|
||||
return Boosts(atp, ata, minEvp, minDfp, mst, hp, lck)
|
||||
}
|
@ -1,17 +1,15 @@
|
||||
package world.phantasmal.web.assetsGeneration
|
||||
|
||||
import world.phantasmal.web.assetsGeneration.ephinea.generateEphineaAssets
|
||||
import java.io.File
|
||||
|
||||
object Main {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
require(args.isNotEmpty()) {
|
||||
"Expected at least one argument denoting the directory where assets should be generated."
|
||||
}
|
||||
|
||||
val outputDir = File(args.first())
|
||||
outputDir.mkdirs()
|
||||
|
||||
Ephinea.generateAssets(outputDir)
|
||||
fun main(args: Array<String>) {
|
||||
require(args.isNotEmpty()) {
|
||||
"Expected at least one argument denoting the directory where assets should be generated."
|
||||
}
|
||||
|
||||
val outputDir = File(args.first())
|
||||
outputDir.mkdirs()
|
||||
|
||||
generateEphineaAssets(outputDir)
|
||||
}
|
||||
|
@ -0,0 +1,230 @@
|
||||
package world.phantasmal.web.assetsGeneration.ephinea
|
||||
|
||||
import kotlinx.serialization.encodeToString
|
||||
import mu.KotlinLogging
|
||||
import world.phantasmal.core.removeAt
|
||||
import world.phantasmal.lib.buffer.Buffer
|
||||
import world.phantasmal.lib.compression.prs.prsDecompress
|
||||
import world.phantasmal.lib.cursor.cursor
|
||||
import world.phantasmal.lib.fileFormats.ItemPmt
|
||||
import world.phantasmal.lib.fileFormats.parseItemPmt
|
||||
import world.phantasmal.lib.fileFormats.parseUnitxt
|
||||
import world.phantasmal.web.shared.JSON_FORMAT_PRETTY
|
||||
import world.phantasmal.web.shared.dto.*
|
||||
import java.io.File
|
||||
import java.util.Comparator.comparing
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* ItemPMT.bin and ItemPT.gsl comes from stock Tethealla. ItemPT.gsl is not used at the moment.
|
||||
* unitxt_j.prs comes from the Ephinea client.
|
||||
* TODO: manual fixes:
|
||||
* - Clio is equipable by HUnewearls
|
||||
* - Red Ring has a requirement of 180, not 108
|
||||
*/
|
||||
fun generateEphineaAssets(outputDir: File) {
|
||||
logger.info("Updating static Ephinea assets.")
|
||||
|
||||
val items = loadItems(loadItemNames())
|
||||
|
||||
logger.info("Updating item type data.")
|
||||
|
||||
File(outputDir, "item_types.ephinea.json")
|
||||
.writeText(JSON_FORMAT_PRETTY.encodeToString(items))
|
||||
|
||||
updateDropsFromWebsite(outputDir, items)
|
||||
|
||||
logger.info("Done updating static Ephinea assets.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts item names from unitxt file.
|
||||
*/
|
||||
private fun loadItemNames(): List<String> {
|
||||
val file = "/ephinea/client/data/unitxt_j.prs"
|
||||
|
||||
logger.info("Loading item names from $file.")
|
||||
|
||||
val unitxt = parseUnitxt(prsDecompress(Buffer.fromResource(file).cursor()).unwrap())
|
||||
|
||||
val itemNames = unitxt.categories[1].toMutableList()
|
||||
// Strip custom Ephinea items until we have the Ephinea ItemPMT.bin.
|
||||
itemNames.removeAt(177, 50)
|
||||
itemNames.removeAt(639, 59)
|
||||
|
||||
logger.info("Done loading item names.")
|
||||
|
||||
return itemNames
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads items from ItemPMT.
|
||||
*/
|
||||
private fun loadItems(itemNames: List<String>): List<ItemType> {
|
||||
val file = "/ephinea/ship-config/param/ItemPMT.bin"
|
||||
|
||||
logger.info("Loading item type data from $file.")
|
||||
|
||||
val itemPmt = parseItemPmt(Buffer.fromResource(file).cursor())
|
||||
val itemTypes = mutableListOf<ItemType>()
|
||||
val ids = mutableSetOf<Int>()
|
||||
|
||||
fun checkId(id: Int, type: String, name: String) {
|
||||
check(ids.add(id)) {
|
||||
"""Trying to add $type with ID $id ($name) but ID already exists."""
|
||||
}
|
||||
}
|
||||
|
||||
for ((categoryI, category) in itemPmt.weapons.withIndex()) {
|
||||
for ((i, weapon) in category.withIndex()) {
|
||||
val id = (categoryI shl 8) + i
|
||||
val name = itemNames[weapon.id]
|
||||
checkId(id, "weapon", name)
|
||||
|
||||
itemTypes.add(WeaponItemType(
|
||||
id,
|
||||
name,
|
||||
weapon.minAtp,
|
||||
weapon.maxAtp,
|
||||
weapon.ata,
|
||||
weapon.maxGrind,
|
||||
weapon.reqAtp,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
for ((i, frame) in itemPmt.frames.withIndex()) {
|
||||
val id = 0x10100 + i
|
||||
val name = itemNames[frame.id]
|
||||
checkId(id, "frame", name)
|
||||
|
||||
val stats = getStatBoosts(itemPmt, frame.statBoost)
|
||||
|
||||
itemTypes.add(FrameItemType(
|
||||
id,
|
||||
name,
|
||||
stats.atp,
|
||||
stats.ata,
|
||||
minEvp = frame.evp + stats.minEvp,
|
||||
maxEvp = frame.evp + stats.minEvp + frame.evpRange,
|
||||
minDfp = frame.dfp + stats.minDfp,
|
||||
maxDfp = frame.dfp + stats.minDfp + frame.dfpRange,
|
||||
stats.mst,
|
||||
stats.hp,
|
||||
stats.lck,
|
||||
))
|
||||
}
|
||||
|
||||
for ((i, barrier) in itemPmt.barriers.withIndex()) {
|
||||
val id = 0x10200 + i
|
||||
val name = itemNames[barrier.id]
|
||||
checkId(id, "barrier", name)
|
||||
|
||||
val stats = getStatBoosts(itemPmt, barrier.statBoost)
|
||||
|
||||
itemTypes.add(BarrierItemType(
|
||||
id,
|
||||
name,
|
||||
stats.atp,
|
||||
stats.ata,
|
||||
minEvp = barrier.evp + stats.minEvp,
|
||||
maxEvp = barrier.evp + stats.minEvp + barrier.evpRange,
|
||||
minDfp = barrier.dfp + stats.minDfp,
|
||||
maxDfp = barrier.dfp + stats.minDfp + barrier.dfpRange,
|
||||
stats.mst,
|
||||
stats.hp,
|
||||
stats.lck,
|
||||
))
|
||||
}
|
||||
|
||||
for ((i, unit) in itemPmt.units.withIndex()) {
|
||||
val id = 0x10300 + i
|
||||
val name = itemNames[unit.id]
|
||||
checkId(id, "unit", name)
|
||||
|
||||
itemTypes.add(UnitItemType(
|
||||
id,
|
||||
name,
|
||||
))
|
||||
}
|
||||
|
||||
for ((categoryI, category) in itemPmt.tools.withIndex()) {
|
||||
for ((i, tool) in category.withIndex()) {
|
||||
val id = (0x30000 or (categoryI shl 8)) + i
|
||||
val name = itemNames[tool.id]
|
||||
checkId(id, "tool", name)
|
||||
|
||||
itemTypes.add(ToolItemType(
|
||||
id,
|
||||
name,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
itemTypes.sortWith(comparing({ it.name }, String.CASE_INSENSITIVE_ORDER))
|
||||
|
||||
logger.info("Done loading item type data.")
|
||||
|
||||
return itemTypes
|
||||
}
|
||||
|
||||
private class Boosts(
|
||||
val atp: Int,
|
||||
val ata: Int,
|
||||
val minEvp: Int,
|
||||
val minDfp: Int,
|
||||
val mst: Int,
|
||||
val hp: Int,
|
||||
val lck: Int,
|
||||
)
|
||||
|
||||
private fun getStatBoosts(itemPmt: ItemPmt, index: Int): Boosts {
|
||||
val statBoosts = itemPmt.statBoosts[index]
|
||||
val amount = statBoosts.amount1
|
||||
|
||||
var atp = 0
|
||||
var ata = 0
|
||||
var minEvp = 0
|
||||
var minDfp = 0
|
||||
var mst = 0
|
||||
var hp = 0
|
||||
var lck = 0
|
||||
|
||||
when (statBoosts.stat1) {
|
||||
1 -> atp += amount
|
||||
2 -> ata += amount
|
||||
3 -> minEvp += amount
|
||||
4 -> minDfp += amount
|
||||
5 -> mst += amount
|
||||
6 -> hp += amount
|
||||
7 -> lck += amount
|
||||
8 -> {
|
||||
atp += amount
|
||||
ata += amount
|
||||
minEvp += amount
|
||||
minDfp += amount
|
||||
mst += amount
|
||||
hp += amount
|
||||
lck += amount
|
||||
}
|
||||
9 -> atp -= amount
|
||||
10 -> ata -= amount
|
||||
11 -> minEvp -= amount
|
||||
12 -> minDfp -= amount
|
||||
13 -> mst -= amount
|
||||
14 -> hp -= amount
|
||||
15 -> lck -= amount
|
||||
16 -> {
|
||||
atp -= amount
|
||||
ata -= amount
|
||||
minEvp -= amount
|
||||
minDfp -= amount
|
||||
mst -= amount
|
||||
hp -= amount
|
||||
lck -= amount
|
||||
}
|
||||
}
|
||||
|
||||
return Boosts(atp, ata, minEvp, minDfp, mst, hp, lck)
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
package world.phantasmal.web.assetsGeneration.ephinea
|
||||
|
||||
import kotlinx.serialization.encodeToString
|
||||
import mu.KotlinLogging
|
||||
import org.jsoup.Jsoup
|
||||
import world.phantasmal.lib.Episode
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.lib.fileFormats.quest.fromNameAndEpisode
|
||||
import world.phantasmal.web.shared.JSON_FORMAT_PRETTY
|
||||
import world.phantasmal.web.shared.dto.*
|
||||
import java.io.File
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun updateDropsFromWebsite(outputDir: File, itemTypes: List<ItemType>) {
|
||||
logger.info("Updating item drops.")
|
||||
|
||||
val enemyDrops = mutableListOf<EnemyDrop>()
|
||||
val boxDrops = mutableListOf<BoxDrop>()
|
||||
|
||||
download(itemTypes, Difficulty.Normal, "normal", enemyDrops, boxDrops)
|
||||
download(itemTypes, Difficulty.Hard, "hard", enemyDrops, boxDrops)
|
||||
download(itemTypes, Difficulty.VHard, "very-hard", enemyDrops, boxDrops)
|
||||
download(itemTypes, Difficulty.Ultimate, "ultimate", enemyDrops, boxDrops)
|
||||
|
||||
File(outputDir, "enemy_drops.ephinea.json")
|
||||
.writeText(JSON_FORMAT_PRETTY.encodeToString(enemyDrops))
|
||||
|
||||
File(outputDir, "box_drops.ephinea.json")
|
||||
.writeText(JSON_FORMAT_PRETTY.encodeToString(boxDrops))
|
||||
|
||||
logger.info("Done updating item drops.")
|
||||
}
|
||||
|
||||
private fun download(
|
||||
itemTypes: List<ItemType>,
|
||||
difficulty: Difficulty,
|
||||
difficultyUrl: String,
|
||||
enemyDrops: MutableList<EnemyDrop>,
|
||||
boxDrops: MutableList<BoxDrop>,
|
||||
) {
|
||||
val doc = Jsoup.connect("https://ephinea.pioneer2.net/drop-charts/${difficultyUrl}/").get()
|
||||
|
||||
var episode: Episode? = null
|
||||
|
||||
for ((tableI, table) in doc.select("table").withIndex()) {
|
||||
val isBox = tableI >= 3
|
||||
|
||||
for (tr in table.select("tr")) {
|
||||
val enemyOrBoxText = tr.child(0).text()
|
||||
|
||||
if (enemyOrBoxText.isBlank()) {
|
||||
continue
|
||||
} else if (enemyOrBoxText.startsWith("EPISODE ")) {
|
||||
val ep = enemyOrBoxText.takeLast(1).toInt()
|
||||
episode = Episode.fromInt(ep)
|
||||
continue
|
||||
}
|
||||
|
||||
checkNotNull(episode) { "Couldn't determine episode." }
|
||||
|
||||
try {
|
||||
val sanitizedEnemyOrBoxText = enemyOrBoxText.split("/")
|
||||
.getOrElse(if (difficulty == Difficulty.Ultimate) 1 else 0) {
|
||||
enemyOrBoxText
|
||||
}
|
||||
|
||||
val enemyOrBox = when (sanitizedEnemyOrBoxText) {
|
||||
"Halo Rappy" -> "Hallo Rappy"
|
||||
"Dal Ral Lie" -> "Dal Ra Lie"
|
||||
"Vol Opt ver. 2" -> "Vol Opt ver.2"
|
||||
"Za Boota" -> "Ze Boota"
|
||||
"Saint Million" -> "Saint-Milion"
|
||||
else -> sanitizedEnemyOrBoxText
|
||||
}
|
||||
|
||||
for ((tdI, td) in tr.select("td").withIndex()) {
|
||||
if (tdI == 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
val sectionId = SectionId.VALUES[tdI - 1]
|
||||
|
||||
if (isBox) {
|
||||
// TODO:
|
||||
// $('font font', td).each((_, font) => {
|
||||
// const item = $('b', font).text();
|
||||
// const rateNum = parseFloat($('sup', font).text());
|
||||
// const rateDenom = parseFloat($('sub', font).text());
|
||||
|
||||
// data.boxDrops.push({
|
||||
// difficulty: Difficulty[difficulty],
|
||||
// episode,
|
||||
// sectionId: SectionId[sectionId],
|
||||
// box: enemyOrBox,
|
||||
// item,
|
||||
// dropRate: rateNum / rateDenom
|
||||
// });
|
||||
|
||||
// data.items.add(item);
|
||||
// });
|
||||
continue
|
||||
} else {
|
||||
val item = td.select("font b").text()
|
||||
|
||||
if (item.isBlank()) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
val itemType = itemTypes.find { it.name == item }
|
||||
|
||||
checkNotNull(itemType) { """No item type found with name "$item".""" }
|
||||
|
||||
val npcType = NpcType.fromNameAndEpisode(enemyOrBox, episode)
|
||||
|
||||
checkNotNull(npcType) {
|
||||
"Couldn't determine NpcType of $enemyOrBox ($episode)."
|
||||
}
|
||||
|
||||
val title = td.select("font abbr").attr("title").replace("\r", "")
|
||||
|
||||
val (dropRateNum, dropRateDenom) =
|
||||
Regex(""".*Drop Rate: (\d+)/(\d+(\.\d+)?).*""")
|
||||
.matchEntire(title)!!
|
||||
.destructured
|
||||
|
||||
val (rareRateNum, rareRateDenom) =
|
||||
Regex(""".*Rare Rate: (\d+)/(\d+(\.\d+)?).*""")
|
||||
.matchEntire(title)!!
|
||||
.destructured
|
||||
|
||||
enemyDrops.add(EnemyDrop(
|
||||
difficulty,
|
||||
episode,
|
||||
sectionId,
|
||||
enemy = npcType,
|
||||
itemTypeId = itemType.id,
|
||||
dropRate = dropRateNum.toDouble() / dropRateDenom.toDouble(),
|
||||
rareRate = rareRateNum.toDouble() / rareRateDenom.toDouble(),
|
||||
))
|
||||
} catch (e: Exception) {
|
||||
logger.error(
|
||||
"Error while processing item $item of $enemyOrBox in episode $episode ${difficulty}.",
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error(
|
||||
"Error while processing $enemyOrBoxText in episode $episode ${difficulty}.",
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
web/assets-generation/src/main/resources/log4j2.xml
Normal file
13
web/assets-generation/src/main/resources/log4j2.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="warn">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %msg%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
@ -0,0 +1,13 @@
|
||||
package world.phantasmal.web.shared.dto
|
||||
|
||||
enum class Difficulty {
|
||||
Normal,
|
||||
Hard,
|
||||
VHard,
|
||||
Ultimate;
|
||||
|
||||
companion object {
|
||||
val VALUES: Array<Difficulty> = values()
|
||||
val VALUES_LIST: List<Difficulty> = VALUES.toList()
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package world.phantasmal.web.shared.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import world.phantasmal.lib.Episode
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
|
||||
@Serializable
|
||||
class EnemyDrop(
|
||||
val difficulty: Difficulty,
|
||||
val episode: Episode,
|
||||
val sectionId: SectionId,
|
||||
val enemy: NpcType,
|
||||
val itemTypeId: Int,
|
||||
val dropRate: Double,
|
||||
val rareRate: Double,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class BoxDrop(
|
||||
val difficulty: Difficulty,
|
||||
val episode: Episode,
|
||||
val sectionId: SectionId,
|
||||
val areaId: Int,
|
||||
val itemTypeId: Int,
|
||||
val dropRate: Double,
|
||||
)
|
@ -11,6 +11,14 @@ import kotlinx.serialization.Serializable
|
||||
sealed class ItemType {
|
||||
abstract val id: Int
|
||||
abstract val name: String
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
return id == (other as ItemType).id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = id
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
@ -1,4 +1,4 @@
|
||||
package world.phantasmal.web.core.models
|
||||
package world.phantasmal.web.shared.dto
|
||||
|
||||
enum class SectionId {
|
||||
Viridia,
|
@ -0,0 +1,44 @@
|
||||
package world.phantasmal.web.core.stores
|
||||
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.models.Server
|
||||
import world.phantasmal.web.questEditor.loading.LoadingCache
|
||||
import world.phantasmal.web.shared.dto.Difficulty
|
||||
import world.phantasmal.web.shared.dto.EnemyDrop
|
||||
import world.phantasmal.web.shared.dto.ItemType
|
||||
import world.phantasmal.web.shared.dto.SectionId
|
||||
import world.phantasmal.webui.stores.Store
|
||||
|
||||
class ItemDropStore(
|
||||
private val assetLoader: AssetLoader,
|
||||
) : Store() {
|
||||
private val cache: LoadingCache<Server, EnemyDropTable> = addDisposable(
|
||||
LoadingCache(::loadEnemyDropTable) {}
|
||||
)
|
||||
|
||||
suspend fun getEnemyDropTable(server: Server): EnemyDropTable =
|
||||
cache.get(server)
|
||||
|
||||
private suspend fun loadEnemyDropTable(server: Server): EnemyDropTable {
|
||||
val drops = assetLoader.load<List<EnemyDrop>>("/enemy_drops.${server.slug}.json")
|
||||
|
||||
val table = mutableMapOf<Triple<Difficulty, SectionId, NpcType>, EnemyDrop>()
|
||||
val itemTypeToDrops = mutableMapOf<Int, MutableList<EnemyDrop>>()
|
||||
|
||||
for (drop in drops) {
|
||||
table[Triple(drop.difficulty, drop.sectionId, drop.enemy)] = drop
|
||||
itemTypeToDrops.getOrPut(drop.itemTypeId) { mutableListOf() }.add(drop)
|
||||
}
|
||||
|
||||
return EnemyDropTable(table, itemTypeToDrops)
|
||||
}
|
||||
}
|
||||
|
||||
class EnemyDropTable(
|
||||
private val table: Map<Triple<Difficulty, SectionId, NpcType>, EnemyDrop>,
|
||||
private val itemTypeToDrops: Map<Int, List<EnemyDrop>>,
|
||||
) {
|
||||
fun getDropsForItemType(itemType: ItemType): List<EnemyDrop> =
|
||||
itemTypeToDrops[itemType.id] ?: emptyList()
|
||||
}
|
@ -1,32 +1,20 @@
|
||||
package world.phantasmal.web.core.stores
|
||||
|
||||
import kotlinx.coroutines.launch
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.models.Server
|
||||
import world.phantasmal.web.questEditor.loading.LoadingCache
|
||||
import world.phantasmal.web.shared.dto.*
|
||||
import world.phantasmal.web.shared.dto.ItemType
|
||||
import world.phantasmal.webui.stores.Store
|
||||
|
||||
class ItemTypeStore(
|
||||
private val uiStore: UiStore,
|
||||
private val assetLoader: AssetLoader,
|
||||
) : Store() {
|
||||
private val cache: LoadingCache<Server, ServerData> = addDisposable(
|
||||
LoadingCache(::loadItemTypes) {}
|
||||
)
|
||||
private val _itemTypes = mutableListVal<ItemType>()
|
||||
|
||||
val itemTypes: ListVal<ItemType> by lazy {
|
||||
observe(uiStore.server) {
|
||||
scope.launch {
|
||||
_itemTypes.value = cache.get(it).itemTypes
|
||||
}
|
||||
}
|
||||
|
||||
_itemTypes
|
||||
}
|
||||
suspend fun getItemTypes(server: Server): List<ItemType> =
|
||||
cache.get(server).itemTypes
|
||||
|
||||
suspend fun getById(server: Server, id: Int): ItemType? =
|
||||
cache.get(server).idToItemType[id]
|
||||
|
@ -3,6 +3,7 @@ package world.phantasmal.web.huntOptimizer
|
||||
import world.phantasmal.web.core.PwTool
|
||||
import world.phantasmal.web.core.PwToolType
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.stores.ItemDropStore
|
||||
import world.phantasmal.web.core.stores.ItemTypeStore
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.huntOptimizer.controllers.HuntOptimizerController
|
||||
@ -24,7 +25,7 @@ class HuntOptimizer(
|
||||
override val toolType = PwToolType.HuntOptimizer
|
||||
|
||||
override fun initialize(): Widget {
|
||||
val itemTypeStore = addDisposable(ItemTypeStore(uiStore, assetLoader))
|
||||
val itemTypeStore = addDisposable(ItemTypeStore(assetLoader))
|
||||
|
||||
// Persistence
|
||||
val huntMethodPersister = HuntMethodPersister()
|
||||
@ -33,13 +34,18 @@ class HuntOptimizer(
|
||||
// Stores
|
||||
val huntMethodStore =
|
||||
addDisposable(HuntMethodStore(uiStore, assetLoader, huntMethodPersister))
|
||||
val huntOptimizerStore =
|
||||
addDisposable(HuntOptimizerStore(wantedItemPersister, uiStore, huntMethodStore))
|
||||
val itemDropStore = addDisposable(ItemDropStore(assetLoader))
|
||||
val huntOptimizerStore = addDisposable(HuntOptimizerStore(
|
||||
wantedItemPersister,
|
||||
uiStore,
|
||||
huntMethodStore,
|
||||
itemTypeStore,
|
||||
itemDropStore,
|
||||
))
|
||||
|
||||
// Controllers
|
||||
val huntOptimizerController = addDisposable(HuntOptimizerController(uiStore))
|
||||
val wantedItemsController =
|
||||
addDisposable(WantedItemsController(huntOptimizerStore, itemTypeStore))
|
||||
val wantedItemsController = addDisposable(WantedItemsController(huntOptimizerStore))
|
||||
val methodsController = addDisposable(MethodsController(uiStore))
|
||||
|
||||
// Main Widget
|
||||
|
@ -4,7 +4,6 @@ import world.phantasmal.observable.value.MutableVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.web.core.stores.ItemTypeStore
|
||||
import world.phantasmal.web.huntOptimizer.models.WantedItemModel
|
||||
import world.phantasmal.web.huntOptimizer.stores.HuntOptimizerStore
|
||||
import world.phantasmal.web.shared.dto.ItemType
|
||||
@ -12,13 +11,12 @@ import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class WantedItemsController(
|
||||
private val huntOptimizerStore: HuntOptimizerStore,
|
||||
itemTypeStore: ItemTypeStore,
|
||||
) : Controller() {
|
||||
private val selectableItemsFilter: MutableVal<(ItemType) -> Boolean> = mutableVal { true }
|
||||
|
||||
// TODO: Use ListVal.filtered with a Val when this is supported.
|
||||
val selectableItems: Val<List<ItemType>> = selectableItemsFilter.flatMap { filter ->
|
||||
itemTypeStore.itemTypes.filtered(filter)
|
||||
huntOptimizerStore.huntableItems.filtered(filter)
|
||||
}
|
||||
|
||||
val wantedItems: ListVal<WantedItemModel> by lazy { huntOptimizerStore.wantedItems }
|
||||
|
@ -6,6 +6,8 @@ import kotlinx.coroutines.withContext
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.web.core.models.Server
|
||||
import world.phantasmal.web.core.stores.ItemDropStore
|
||||
import world.phantasmal.web.core.stores.ItemTypeStore
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.huntOptimizer.models.WantedItemModel
|
||||
import world.phantasmal.web.huntOptimizer.persistence.WantedItemPersister
|
||||
@ -23,9 +25,28 @@ class HuntOptimizerStore(
|
||||
private val wantedItemPersister: WantedItemPersister,
|
||||
private val uiStore: UiStore,
|
||||
private val huntMethodStore: HuntMethodStore,
|
||||
private val itemTypeStore: ItemTypeStore,
|
||||
private val itemDropStore: ItemDropStore,
|
||||
) : Store() {
|
||||
private val _huntableItems = mutableListVal<ItemType>()
|
||||
private val _wantedItems = mutableListVal<WantedItemModel> { arrayOf(it.amount) }
|
||||
|
||||
val huntableItems: ListVal<ItemType> by lazy {
|
||||
observe(uiStore.server) { server ->
|
||||
_huntableItems.clear()
|
||||
|
||||
scope.launch {
|
||||
val dropTable = itemDropStore.getEnemyDropTable(server)
|
||||
|
||||
_huntableItems.value = itemTypeStore.getItemTypes(server).filter {
|
||||
dropTable.getDropsForItemType(it).isNotEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_huntableItems
|
||||
}
|
||||
|
||||
val wantedItems: ListVal<WantedItemModel> by lazy {
|
||||
observe(uiStore.server) { loadWantedItems(it) }
|
||||
_wantedItems
|
||||
|
@ -2,7 +2,7 @@ package world.phantasmal.web.viewer.controller
|
||||
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.plus
|
||||
import world.phantasmal.web.core.models.SectionId
|
||||
import world.phantasmal.web.shared.dto.SectionId
|
||||
import world.phantasmal.web.viewer.store.ViewerStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
|
@ -6,7 +6,7 @@ import world.phantasmal.lib.cursor.cursor
|
||||
import world.phantasmal.lib.fileFormats.ninja.*
|
||||
import world.phantasmal.lib.fileFormats.parseAfs
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.models.SectionId
|
||||
import world.phantasmal.web.shared.dto.SectionId
|
||||
import world.phantasmal.web.questEditor.loading.LoadingCache
|
||||
import world.phantasmal.web.viewer.models.CharacterClass
|
||||
import world.phantasmal.web.viewer.models.CharacterClass.*
|
||||
|
@ -10,7 +10,7 @@ import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.web.core.models.SectionId
|
||||
import world.phantasmal.web.shared.dto.SectionId
|
||||
import world.phantasmal.web.viewer.loading.CharacterClassAssetLoader
|
||||
import world.phantasmal.web.viewer.models.CharacterClass
|
||||
import world.phantasmal.webui.stores.Store
|
||||
|
@ -3,7 +3,7 @@ package world.phantasmal.web.viewer.widgets
|
||||
import kotlinx.coroutines.launch
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.observable.value.value
|
||||
import world.phantasmal.web.core.models.SectionId
|
||||
import world.phantasmal.web.shared.dto.SectionId
|
||||
import world.phantasmal.web.viewer.controller.CharacterClassOptionsController
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.dom.table
|
||||
|
@ -1 +1,2 @@
|
||||
[]
|
||||
[
|
||||
]
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user