From 2ff6719c101d81efd5e2c9785d2d552a86c0dafd Mon Sep 17 00:00:00 2001 From: Obique Date: Thu, 15 Aug 2019 02:20:39 -0400 Subject: [PATCH] Restructured loot generation and added a StaticItemCreator to streamline creating static items --- .../support/objects/StaticItemCreator.kt | 251 ++++++++++++++++++ .../combat/loot/GrantLootService.java | 2 + .../combat/loot/LootGenerationService.kt | 200 ++++---------- .../gameplay/combat/loot/RareLootService.java | 92 ------- .../loot/generation/CreditLootGenerator.kt | 70 +++++ .../loot/generation/ItemLootGenerator.kt | 97 +++++++ .../combat/loot/generation/NPCLootTable.kt} | 49 +--- .../loot/generation/RareItemLootGenerator.kt | 80 ++++++ .../objects/items/StaticItemService.kt | 232 +--------------- 9 files changed, 567 insertions(+), 506 deletions(-) create mode 100644 src/main/java/com/projectswg/holocore/resources/support/objects/StaticItemCreator.kt create mode 100644 src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/CreditLootGenerator.kt create mode 100644 src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/ItemLootGenerator.kt rename src/{test/java/com/projectswg/holocore/services/gameplay/combat/loot/TestRareLootService.java => main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/NPCLootTable.kt} (57%) create mode 100644 src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/RareItemLootGenerator.kt diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/StaticItemCreator.kt b/src/main/java/com/projectswg/holocore/resources/support/objects/StaticItemCreator.kt new file mode 100644 index 000000000..bec6c1e61 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/StaticItemCreator.kt @@ -0,0 +1,251 @@ +/*********************************************************************************** + * Copyright (c) 2019 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create an emulator which will provide a server for players to * + * continue playing a game similar to the one they used to play. We are basing * + * it on the final publish of the game prior to end-game events. * + * * + * This file is part of Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * Holocore is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with Holocore. If not, see . * + ***********************************************************************************/ + +package com.projectswg.holocore.resources.support.objects + +import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader +import com.projectswg.holocore.resources.support.data.server_info.loader.StaticItemLoader +import com.projectswg.holocore.resources.support.objects.swg.SWGObject +import com.projectswg.holocore.resources.support.objects.swg.tangible.TangibleObject +import com.projectswg.holocore.resources.support.objects.swg.weapon.WeaponObject +import java.util.* +import kotlin.math.floor + +object StaticItemCreator { + + fun createItem(itemName: String): SWGObject? { + val info = DataLoader.staticItems().getItemByName(itemName) ?: return null + val swgObject = ObjectCreator.createObjectFromTemplate(info.iffTemplate) as? TangibleObject ?: return null + + applyAttributes(swgObject, info) + return swgObject + } + + private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.StaticItemInfo) { + obj.addAttribute("condition", String.format("%d/%d", info.hitPoints, info.hitPoints)) + obj.addAttribute("volume", info.volume.toString()) + obj.objectName = info.stringName + + applyAttributes(obj, info.armorInfo) + applyAttributes(obj, info.wearableInfo) + applyAttributes(obj, info.weaponInfo) + applyAttributes(obj, info.collectionInfo) + applyAttributes(obj, info.costumeInfo) + applyAttributes(obj, info.dnaInfo) + applyAttributes(obj, info.grantInfo) + applyAttributes(obj, info.genericInfo) + applyAttributes(obj, info.objectInfo) + applyAttributes(obj, info.schematicInfo) + applyAttributes(obj, info.storytellerInfo) + } + + private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.ArmorItemInfo?) { + if (info == null) + return + + if (info.requiredProfession.isNotEmpty()) + obj.addAttribute("class_required", "@ui_roadmap:title_" + info.requiredProfession) + obj.addAttribute("required_combat_level", info.requiredLevel.toString()) + val kineticMax: Int + val energyMax: Int + when (info.armorType) { + StaticItemLoader.ArmorItemInfo.ArmorType.ASSAULT -> { + kineticMax = 7000 + energyMax = 5000 + obj.addAttribute("armor_category", "@obj_attr_n:armor_assault") + } + StaticItemLoader.ArmorItemInfo.ArmorType.BATTLE -> { + kineticMax = 6000 + energyMax = 6000 + obj.addAttribute("armor_category", "@obj_attr_n:armor_battle") + } + StaticItemLoader.ArmorItemInfo.ArmorType.RECON -> { + kineticMax = 5000 + energyMax = 7000 + obj.addAttribute("armor_category", "@obj_attr_n:armor_reconnaissance") + } + } + + obj.addAttribute("cat_armor_standard_protection.kinetic", calculateProtection(kineticMax, info.protection)) + obj.addAttribute("cat_armor_standard_protection.energy", calculateProtection(energyMax, info.protection)) + obj.addAttribute("cat_armor_special_protection.special_protection_type_heat", calculateProtection(6000, info.protection)) + obj.addAttribute("cat_armor_special_protection.special_protection_type_cold", calculateProtection(6000, info.protection)) + obj.addAttribute("cat_armor_special_protection.special_protection_type_acid", calculateProtection(6000, info.protection)) + obj.addAttribute("cat_armor_special_protection.special_protection_type_electricity", calculateProtection(6000, info.protection)) + + applySkillMods(obj, info.skillMods) + applyColors(obj, info.color) + } + + private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.WearableItemInfo?) { + if (info == null) + return + + if (info.requiredProfession.isNotEmpty()) + obj.addAttribute("class_required", "@ui_roadmap:title_" + info.requiredProfession) + obj.addAttribute("required_combat_level", info.requiredLevel.toString()) + + if (info.requiredFaction.isNotEmpty()) + obj.addAttribute("faction_restriction", "@pvp_factions:" + info.requiredFaction) + + // Apply the mods! + applySkillMods(obj, info.skillMods) + applyColors(obj, info.color) + + // Add the race restrictions only if there are any + val raceRestriction = buildRaceRestrictionString(info) + if (raceRestriction.isNotEmpty()) + obj.addAttribute("species_restrictions.species_name", raceRestriction) + } + + private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.WeaponItemInfo?) { + if (info == null) + return + + if (info.requiredProfession.isNotEmpty()) + obj.addAttribute("class_required", "@ui_roadmap:title_" + info.requiredProfession) + obj.addAttribute("required_combat_level", info.requiredLevel.toString()) + obj.addAttribute("cat_wpn_damage.wpn_damage_type", "@obj_attr_n:${info.damageType.name.toLowerCase(Locale.US)}") + obj.addAttribute("cat_wpn_damage.wpn_category", "@obj_attr_n:wpn_category_" + info.weaponType.num) + obj.addAttribute("cat_wpn_damage.wpn_attack_speed", (info.attackSpeed / 100).toString()) + obj.addAttribute("cat_wpn_damage.damage", "${info.minDamage}-${info.maxDamage}") + if (info.elementalType != null) { // Not all weapons have elemental damage. + obj.addAttribute("cat_wpn_damage.wpn_elemental_type", "@obj_attr_n:${info.elementalType.name.toLowerCase(Locale.US)}") + obj.addAttribute("cat_wpn_damage.wpn_elemental_value", info.elementalDamage.toString()) + } + + obj.addAttribute("cat_wpn_damage.weapon_dps", info.actualDps.toString()) + + if (info.procEffect.isNotEmpty()) + // Not all weapons have a proc effect + obj.addAttribute("proc_name", info.procEffect) + + // TODO set DPS + + obj.addAttribute("cat_wpn_other.wpn_range", String.format("%d-%dm", info.minRange, info.maxRange)) + // Ziggy: Special Action Cost would go under cat_wpn_other as well, but it's a pre-NGE artifact. + + val weapon = obj as WeaponObject + weapon.type = info.weaponType + weapon.attackSpeed = info.attackSpeed.toFloat() + weapon.minRange = info.minRange.toFloat() + weapon.maxRange = info.maxRange.toFloat() + weapon.damageType = info.damageType + weapon.elementalType = info.elementalType + weapon.minDamage = info.minDamage + weapon.maxDamage = info.maxDamage + } + + private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.CollectionItemInfo?) { + if (info == null) + return + + obj.addAttribute("collection_name", info.slotName) + + applyColors(obj, info.color) + } + + private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.CostumeItemInfo?) { +// if (info == null) +// return; + } + + private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.DnaItemInfo?) { +// if (info == null) +// return; + } + + private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.GrantItemInfo?) { +// if (info == null) +// return; + } + + private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.GenericItemInfo?) { + if (info == null) + return + + if (info.value != 0) + obj.addAttribute("charges", info.value.toString()) + + applyColors(obj, info.color) + } + + private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.ObjectItemInfo?) { + if (info == null) + return + + if (info.value != 0) + obj.addAttribute("charges", info.value.toString()) + + applyColors(obj, info.color) + } + + private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.SchematicItemInfo?) { +// if (info == null) +// return; + } + + private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.StorytellerItemInfo?) { +// if (info == null) +// return; + } + + private fun applySkillMods(obj: TangibleObject, skillMods: Map) { + for ((key, value) in skillMods) + obj.addAttribute(key, value.toString()) + } + + private fun applyColors(obj: TangibleObject, colors: IntArray) { + for (i in 0..3) { + if (colors[i] >= 0) { + obj.putCustomization("/private/index_color_$i", colors[i]) + } + } + } + + private fun buildRaceRestrictionString(info: StaticItemLoader.WearableItemInfo): String { + var races = "" + + if (info.isRaceWookie) + races += "Wookiee " + if (info.isRaceIthorian) + races += "Ithorian " + if (info.isRaceRodian) + races += "Rodian " + if (info.isRaceTrandoshan) + races += "Trandoshan " + if (info.isRaceRest) + races += "MonCal Human Zabrak Bothan Sullustan Twi'lek " + + return if (races.isEmpty()) "" else races.substring(0, races.length - 1) + } + + private fun calculateProtection(max: Int, protection: Double): String { + return floor(max * protection).toInt().toString() + } + +} diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/GrantLootService.java b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/GrantLootService.java index 0880f25a3..fae1faad5 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/GrantLootService.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/GrantLootService.java @@ -256,6 +256,8 @@ public final class GrantLootService extends Service { switch (item.moveToContainer(looter, looter.getInventory())) { case SUCCESS: { String itemName = item.getObjectName(); + if (itemName.isEmpty()) + itemName = item.getStringId().toString(); if (item instanceof CreditObject) { onLootedCredits(looter, ((CreditObject) item).getAmount()); diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/LootGenerationService.kt b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/LootGenerationService.kt index 47538461c..0334f0c7a 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/LootGenerationService.kt +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/LootGenerationService.kt @@ -1,35 +1,40 @@ package com.projectswg.holocore.services.gameplay.combat.loot import com.projectswg.common.data.location.Location +import com.projectswg.common.network.packets.swg.zone.PlayClientEffectObjectMessage import com.projectswg.common.network.packets.swg.zone.PlayClientEffectObjectTransformMessage +import com.projectswg.common.network.packets.swg.zone.PlayMusicMessage +import com.projectswg.common.network.packets.swg.zone.object_controller.ShowLootBox import com.projectswg.holocore.intents.gameplay.combat.CreatureKilledIntent import com.projectswg.holocore.intents.gameplay.combat.loot.CorpseLootedIntent import com.projectswg.holocore.intents.gameplay.combat.loot.LootGeneratedIntent -import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent -import com.projectswg.holocore.intents.support.objects.items.CreateStaticItemIntent import com.projectswg.holocore.intents.support.objects.swg.ObjectCreatedIntent import com.projectswg.holocore.resources.support.data.server_info.StandardLog import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData import com.projectswg.holocore.resources.support.data.server_info.mongodb.PswgDatabase import com.projectswg.holocore.resources.support.objects.ObjectCreator import com.projectswg.holocore.resources.support.objects.swg.SWGObject -import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureDifficulty import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject import com.projectswg.holocore.resources.support.objects.swg.group.GroupObject -import com.projectswg.holocore.resources.support.objects.swg.tangible.CreditObject +import com.projectswg.holocore.services.gameplay.combat.loot.generation.CreditLootGenerator +import com.projectswg.holocore.services.gameplay.combat.loot.generation.ItemLootGenerator +import com.projectswg.holocore.services.gameplay.combat.loot.generation.NPCLootTable +import com.projectswg.holocore.services.gameplay.combat.loot.generation.RareItemLootGenerator import com.projectswg.holocore.services.support.objects.ObjectStorageService.ObjectLookup -import com.projectswg.holocore.services.support.objects.items.StaticItemService -import me.joshlarson.jlcommon.control.IntentChain +import kotlinx.coroutines.* import me.joshlarson.jlcommon.control.IntentHandler import me.joshlarson.jlcommon.control.Service import me.joshlarson.jlcommon.log.Log import java.util.* -import java.util.concurrent.ThreadLocalRandom class LootGenerationService : Service() { private val npcLoot: MutableMap = HashMap() // K: npc_id, V: possible loot + private val creditGenerator = CreditLootGenerator() + private val itemGenerator = ItemLootGenerator() + private val rareItemLootGenerator = RareItemLootGenerator() + private val scope = CoroutineScope(Dispatchers.Default) override fun initialize(): Boolean { loadNPCLoot() @@ -37,36 +42,58 @@ class LootGenerationService : Service() { return true } + override fun stop(): Boolean { + scope.coroutineContext.cancel() + return true + } + @IntentHandler private fun handleCreatureKilled(cki: CreatureKilledIntent) { val corpse = cki.corpse as? AIObject ?: return val creatureId = corpse.creatureId - val loot = npcLoot[creatureId] + val lootRecord = npcLoot[creatureId] - if (loot == null) { + if (lootRecord == null) { Log.w("No NPCLoot associated with NPC ID: $creatureId") return } + val loot = ArrayList() + val killer = cki.killer + if (!killer.isPlayer) + return + + // Various kinds of loot + if (lootRecord.isDropCredits && PswgDatabase.config.getBoolean(this, "cashLoot", true)) + creditGenerator.generate(corpse, loot) + if (PswgDatabase.config.getBoolean(this, "itemLoot", true)) + itemGenerator.generate(corpse, killer, loot, lootRecord.npcTables) + rareItemLootGenerator.generate(corpse = corpse, killer = killer, loot = loot) + + if (loot.isEmpty()) { + CorpseLootedIntent(corpse).broadcast() + return + } + val lootInventory = ObjectCreator.createObjectFromTemplate("object/tangible/inventory/shared_creature_inventory.iff") + loot.forEach { obj -> + obj.moveToContainer(lootInventory) + ObjectCreatedIntent.broadcast(obj) + + scope.launch { + delay(60) + when (obj.template) { + RareLootService.RARE_CHEST, + RareLootService.LEGENDARY_CHEST, + RareLootService.EXCEPTIONAL_CHEST -> sendRareLootPackets(obj, corpse, killer) + } + } + } lootInventory.moveToContainer(corpse, corpse.location) ObjectCreatedIntent.broadcast(lootInventory) - val killer = cki.killer - - var cashGenerated = false - var lootGenerated = false - - if (PswgDatabase.config.getBoolean(this, "cashLoot", true)) - cashGenerated = generateCreditChip(loot, lootInventory, corpse.difficulty, corpse.level.toInt()) - if (PswgDatabase.config.getBoolean(this, "itemLoot", true)) - lootGenerated = generateLoot(loot, killer, lootInventory, corpse.difficulty, corpse.level.toInt()) - - if (!cashGenerated && !lootGenerated) - CorpseLootedIntent(corpse).broadcast() - else - showLootDisc(killer, corpse) + showLootDisc(killer, corpse) } /** @@ -100,7 +127,7 @@ class LootGenerationService : Service() { if (chance <= 0) return - loot.npcTables.add(NPCTable(chance, table)) + loot.npcTables.add(NPCLootTable(chance, table)) } private fun showLootDisc(requester: CreatureObject, corpse: AIObject) { @@ -122,125 +149,14 @@ class LootGenerationService : Service() { LootGeneratedIntent.broadcast(corpse) } - private fun generateCreditChip(loot: NPCLoot, lootInventory: SWGObject, difficulty: CreatureDifficulty, combatLevel: Int): Boolean { - if (!loot.isDropCredits) - return false - - val random = ThreadLocalRandom.current() - val config = PswgDatabase.config - - val range = config.getDouble(this, "lootCashHumanRange", 0.05) - val cashLootRoll = random.nextDouble() - - val credits = combatLevel * (1 + random.nextDouble(0.0, range)).toInt() * when (difficulty) { - CreatureDifficulty.NORMAL -> { - if (cashLootRoll > config.getDouble(this, "lootCashHumanNormalChance", 0.60)) - return false - config.getInt(this, "lootCashHumanNormal", 2) - } - CreatureDifficulty.ELITE -> { - if (cashLootRoll > config.getDouble(this, "lootCashHumanElitechance", 0.80)) - return false - config.getInt(this, "lootCashHumanElite", 5) - } - CreatureDifficulty.BOSS -> // bosses always drop cash loot, so no need to check - config.getInt(this, "lootCashHumanBoss", 9) - } - - // TODO scale with group size? - - val cashObject = ObjectCreator.createObjectFromTemplate("object/tangible/item/shared_loot_cash.iff", CreditObject::class.java) - - cashObject.amount = credits.toLong() - cashObject.moveToContainer(lootInventory) - - ObjectCreatedIntent.broadcast(cashObject) - return true + private fun sendRareLootPackets(chest: SWGObject, corpse: CreatureObject, killer: CreatureObject) { + val effect = PlayClientEffectObjectMessage("appearance/pt_rare_chest.prt", "", corpse.objectId, "") + val sound = PlayMusicMessage(0, "sound/rare_loot_chest.snd", 1, false) + val box = ShowLootBox(killer.objectId, longArrayOf(chest.objectId)) + + killer.owner?.sendPacket(effect, sound, box) } - /** - * Generates loot and places it in the inventory of the corpse. - * - * @param loot the loot info of the creature killed - * @param killer the person that killed the NPC - * @param lootInventory the inventory the loot will be placed in (corpse inventory) - * @return whether loot was generated or not - */ - private fun generateLoot(loot: NPCLoot, killer: CreatureObject, lootInventory: SWGObject, difficulty: CreatureDifficulty, level: Int): Boolean { - val random = ThreadLocalRandom.current() - var lootGenerated = false - - val tableRoll = random.nextInt(100) + 1 - - // Admin Variables - val admin = killer.hasCommand("admin") - val adminOutput1 = StringBuilder("$tableRoll //") - val adminOutput2 = StringBuilder() - - val lootTables = ServerData.lootTables - for ((tableId, npcTable) in loot.npcTables.withIndex()) { - val tableChance = npcTable.chance - - if (tableRoll > tableChance) { - // Skip ahead if there's no drop chance - adminOutput1.append("/ \\#FF0000 loot_table").append(tableId+1) - break - } - adminOutput1.append("/ \\#00FF00 loot_table").append(tableId+1) - - val groupRoll = random.nextInt(100) + 1 - val lootTableItem = lootTables.getLootTableItem(npcTable.lootTable, difficulty.name, level) ?: continue - - for ((chance, items) in lootTableItem.groups) { // group of items to be granted - if (groupRoll > chance) - continue - - val itemName = items[random.nextInt(items.size)] - - adminOutput2.append(itemName).append('\n') - when { - itemName.startsWith("dynamic_") -> { // TODO dynamic item handling - SystemMessageIntent(killer.owner!!, "We don't support this loot item yet: $itemName").broadcast() - } - itemName.endsWith(".iff") -> { - val obj = ObjectCreator.createObjectFromTemplate(itemName) - obj.moveToContainer(lootInventory) - ObjectCreatedIntent.broadcast(obj) - - lootGenerated = true - } - else -> { - CreateStaticItemIntent(killer, lootInventory, CreateStaticItemCallback(), itemName).broadcast() - - lootGenerated = true - } - } - - break - } - } - if (admin) { - IntentChain.broadcastChain( - SystemMessageIntent(killer.owner!!, adminOutput1.toString()), - SystemMessageIntent(killer.owner!!, adminOutput2.toString()) - ) - } - - return lootGenerated - } - - private class NPCLoot(val isDropCredits: Boolean, val npcTables: MutableCollection = ArrayList()) - - private class NPCTable(val chance: Int, val lootTable: String) - - private class CreateStaticItemCallback : StaticItemService.ObjectCreationHandler { - - override val isIgnoreVolume = true - - override fun success(createdObjects: List) { - // do nothing - loot disc is created on the return of the generateLoot method - } - - } + private class NPCLoot(val isDropCredits: Boolean, val npcTables: MutableList = ArrayList()) } diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/RareLootService.java b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/RareLootService.java index 50d4ba64c..bb89c40ad 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/RareLootService.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/RareLootService.java @@ -26,22 +26,14 @@ ***********************************************************************************/ package com.projectswg.holocore.services.gameplay.combat.loot; -import com.projectswg.common.data.encodables.oob.StringId; -import com.projectswg.common.network.packets.swg.zone.PlayClientEffectObjectMessage; -import com.projectswg.common.network.packets.swg.zone.PlayMusicMessage; -import com.projectswg.common.network.packets.swg.zone.object_controller.ShowLootBox; -import com.projectswg.holocore.intents.gameplay.combat.CreatureKilledIntent; import com.projectswg.holocore.intents.gameplay.combat.loot.OpenRareChestIntent; import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent; import com.projectswg.holocore.intents.support.objects.items.CreateStaticItemIntent; import com.projectswg.holocore.intents.support.objects.swg.DestroyObjectIntent; -import com.projectswg.holocore.intents.support.objects.swg.ObjectCreatedIntent; import com.projectswg.holocore.resources.support.data.server_info.SdbLoader; import com.projectswg.holocore.resources.support.data.server_info.StandardLog; import com.projectswg.holocore.resources.support.global.player.Player; -import com.projectswg.holocore.resources.support.objects.ObjectCreator; import com.projectswg.holocore.resources.support.objects.swg.SWGObject; -import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureDifficulty; import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject; import com.projectswg.holocore.resources.support.objects.swg.tangible.TangibleObject; import com.projectswg.holocore.services.support.objects.items.StaticItemService; @@ -54,7 +46,6 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Random; import java.util.concurrent.ThreadLocalRandom; public class RareLootService extends Service { @@ -63,18 +54,11 @@ public class RareLootService extends Service { public static final String EXCEPTIONAL_CHEST = "object/tangible/item/shared_rare_loot_chest_2.iff"; public static final String LEGENDARY_CHEST = "object/tangible/item/shared_rare_loot_chest_3.iff"; - // TODO these two could be config options - private static final short MAX_LEVEL_DIFFERENCE = 6; // +-6 difference is allowed between killer and corpse - private static final int DROP_CHANCE = 1; // One in a hundred eligible kills will drop a chest - - private final Random random; private final List rareItems; private final List exceptionalItems; private final List legendaryItems; public RareLootService() { - random = new Random(); - rareItems = new ArrayList<>(); exceptionalItems = new ArrayList<>(); legendaryItems = new ArrayList<>(); @@ -112,38 +96,6 @@ public class RareLootService extends Service { return super.initialize(); } - @IntentHandler - private void handleCreatureKilledIntent(CreatureKilledIntent cki) { - // TODO only the player that delivered the killing blow is considered for this - CreatureObject corpse = cki.getCorpse(); - CreatureObject killer = cki.getKiller(); - - if (!isPlayerEligible(killer.isPlayer(), corpse.isPlayer())) { - return; - } - - if (!isLevelEligible(corpse.getLevel(), killer.getLevel())) { - return; - } - - int roll = random.nextInt(100) + 1; // Rolls from 0 to 99, then we add 1 and it becomes 1 to 100 - - if (!isDrop(roll)) { - Log.d("No RLS drop from %s with roll %d", corpse, roll); - return; - } - - String template = templateForDifficulty(corpse.getDifficulty()); - SWGObject chest = ObjectCreator.createObjectFromTemplate(template); - SWGObject inventory = killer.getSlottedObject("inventory"); - - chest.setStf("loot_n", chestIdForTemplate(template) + "_n"); - chest.setDetailStf(new StringId("loot_n", chestIdForTemplate(template) + "_d")); // Not located in loot_d, for whatever reason... - - chest.moveToContainer(inventory); - sendSuccessPackets(chest, corpse, killer); - } - @IntentHandler private void handleOpenRareChestIntent(OpenRareChestIntent intent) { CreatureObject actor = intent.getActor(); @@ -183,50 +135,6 @@ public class RareLootService extends Service { } - boolean isPlayerEligible(boolean killerPlayer, boolean corpsePlayer) { - return killerPlayer && !corpsePlayer; - } - - boolean isLevelEligible(int corpseLevel, int killerLevel) { - // Ensure a positive levelDifference - int highestLevel = Math.max(corpseLevel, killerLevel); - int lowestLevel = Math.min(corpseLevel, killerLevel); - int levelDifference = highestLevel - lowestLevel; - - return levelDifference <= MAX_LEVEL_DIFFERENCE; - } - - boolean isDrop(int roll) { - return roll <= DROP_CHANCE; - } - - String templateForDifficulty(CreatureDifficulty difficulty) { - switch (difficulty) { - case NORMAL: - return RARE_CHEST; - case ELITE: - return EXCEPTIONAL_CHEST; - case BOSS: - return LEGENDARY_CHEST; - default: - throw new IllegalArgumentException("Unknown CreatureDifficulty: " + difficulty); - } - } - - String chestIdForTemplate(String template) { - return template.replace("object/tangible/item/shared_", "").replace(".iff", ""); - } - - private void sendSuccessPackets(SWGObject chest, CreatureObject corpse, CreatureObject killer) { - ObjectCreatedIntent.broadcast(chest); - - PlayClientEffectObjectMessage effect = new PlayClientEffectObjectMessage("appearance/pt_rare_chest.prt", "", corpse.getObjectId(), ""); - PlayMusicMessage sound = new PlayMusicMessage(0, "sound/rare_loot_chest.snd", 1, false); - ShowLootBox box = new ShowLootBox(killer.getObjectId(), new long[] { chest.getObjectId() }); - - killer.getOwner().sendPacket(effect, sound, box); - } - private String randomItem(List items) { int availableItems = items.size(); int randomIndex = ThreadLocalRandom.current().nextInt(availableItems); diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/CreditLootGenerator.kt b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/CreditLootGenerator.kt new file mode 100644 index 000000000..39c6c3078 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/CreditLootGenerator.kt @@ -0,0 +1,70 @@ +/*********************************************************************************** + * Copyright (c) 2019 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create an emulator which will provide a server for players to * + * continue playing a game similar to the one they used to play. We are basing * + * it on the final publish of the game prior to end-game events. * + * * + * This file is part of Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * Holocore is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with Holocore. If not, see . * + ***********************************************************************************/ + +package com.projectswg.holocore.services.gameplay.combat.loot.generation + +import com.projectswg.holocore.resources.support.data.server_info.mongodb.PswgDatabase +import com.projectswg.holocore.resources.support.objects.ObjectCreator +import com.projectswg.holocore.resources.support.objects.swg.SWGObject +import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureDifficulty +import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject +import com.projectswg.holocore.resources.support.objects.swg.tangible.CreditObject +import java.util.concurrent.ThreadLocalRandom + +class CreditLootGenerator() { + + fun generate(corpse: CreatureObject, loot: MutableList) { + val random = ThreadLocalRandom.current() + val config = PswgDatabase.config + + val range = config.getDouble(this, "lootCashHumanRange", 0.05) + val cashLootRoll = random.nextDouble() + + val credits = corpse.level.toInt() * (1 + random.nextDouble(0.0, range)).toInt() * when (corpse.difficulty) { + CreatureDifficulty.ELITE -> { + if (cashLootRoll > config.getDouble(this, "lootCashHumanElitechance", 0.80)) + return + config.getInt(this, "lootCashHumanElite", 5) + } + CreatureDifficulty.BOSS -> // bosses always drop cash loot, so no need to check + config.getInt(this, "lootCashHumanBoss", 9) + else -> { + if (cashLootRoll > config.getDouble(this, "lootCashHumanNormalChance", 0.60)) + return + config.getInt(this, "lootCashHumanNormal", 2) + } + } + + // TODO scale with group size? + + val cashObject = ObjectCreator.createObjectFromTemplate("object/tangible/item/shared_loot_cash.iff", CreditObject::class.java) + + cashObject.amount = credits.toLong() + loot.add(cashObject) + } + +} diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/ItemLootGenerator.kt b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/ItemLootGenerator.kt new file mode 100644 index 000000000..8a8501f00 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/ItemLootGenerator.kt @@ -0,0 +1,97 @@ +/*********************************************************************************** + * Copyright (c) 2019 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create an emulator which will provide a server for players to * + * continue playing a game similar to the one they used to play. We are basing * + * it on the final publish of the game prior to end-game events. * + * * + * This file is part of Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * Holocore is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with Holocore. If not, see . * + ***********************************************************************************/ + +package com.projectswg.holocore.services.gameplay.combat.loot.generation + +import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent +import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData +import com.projectswg.holocore.resources.support.objects.ObjectCreator +import com.projectswg.holocore.resources.support.objects.StaticItemCreator +import com.projectswg.holocore.resources.support.objects.swg.SWGObject +import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject +import me.joshlarson.jlcommon.control.IntentChain +import java.util.concurrent.ThreadLocalRandom + +class ItemLootGenerator { + + fun generate(corpse: CreatureObject, killer: CreatureObject, loot: MutableList, lootTables: List) { + val random = ThreadLocalRandom.current() + + val tableRoll = random.nextInt(100) + 1 + + // Admin Variables + val admin = killer.hasCommand("admin") + val adminOutput1 = StringBuilder("$tableRoll //") + val adminOutput2 = StringBuilder() + + val lootTableLookup = ServerData.lootTables + for ((tableId, npcTable) in lootTables.withIndex()) { + val tableChance = npcTable.chance + + if (tableRoll > tableChance) { + // Skip ahead if there's no drop chance + adminOutput1.append("/ \\#FF0000 loot_table").append(tableId+1) + break + } + adminOutput1.append("/ \\#00FF00 loot_table").append(tableId+1) + + val groupRoll = random.nextInt(100) + 1 + val lootTableItem = lootTableLookup.getLootTableItem(npcTable.lootTable, corpse.difficulty.name, corpse.level.toInt()) ?: continue + + for ((chance, items) in lootTableItem.groups) { // group of items to be granted + if (groupRoll > chance) + continue + + val itemName = items[random.nextInt(items.size)] + + adminOutput2.append(itemName).append('\n') + when { + itemName.startsWith("dynamic_") -> { // TODO dynamic item handling + SystemMessageIntent(killer.owner!!, "We don't support this loot item yet: $itemName").broadcast() + } + itemName.endsWith(".iff") -> { + loot.add(ObjectCreator.createObjectFromTemplate(itemName)) + } + else -> { + val obj = StaticItemCreator.createItem(itemName) + if (obj != null) + loot.add(obj) + } + } + + break + } + } + if (admin) { + IntentChain.broadcastChain( + SystemMessageIntent(killer.owner!!, adminOutput1.toString()), + SystemMessageIntent(killer.owner!!, adminOutput2.toString()) + ) + } + } + +} diff --git a/src/test/java/com/projectswg/holocore/services/gameplay/combat/loot/TestRareLootService.java b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/NPCLootTable.kt similarity index 57% rename from src/test/java/com/projectswg/holocore/services/gameplay/combat/loot/TestRareLootService.java rename to src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/NPCLootTable.kt index 4cbb4427c..ac88ff7d3 100644 --- a/src/test/java/com/projectswg/holocore/services/gameplay/combat/loot/TestRareLootService.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/NPCLootTable.kt @@ -1,5 +1,5 @@ /*********************************************************************************** - * Copyright (c) 2018 /// Project SWG /// www.projectswg.com * + * Copyright (c) 2019 /// Project SWG /// www.projectswg.com * * * * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * @@ -24,50 +24,7 @@ * You should have received a copy of the GNU Affero General Public License * * along with Holocore. If not, see . * ***********************************************************************************/ -package com.projectswg.holocore.services.gameplay.combat.loot; -import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureDifficulty; -import com.projectswg.holocore.test.runners.TestRunnerNoIntents; -import org.junit.Assert; -import org.junit.Test; +package com.projectswg.holocore.services.gameplay.combat.loot.generation -public class TestRareLootService extends TestRunnerNoIntents { - - private final RareLootService rls; - - public TestRareLootService() { - rls = new RareLootService(); - } - - @Test - public void testIsPlayerEligible() { - Assert.assertTrue(rls.isPlayerEligible(true, false)); - Assert.assertFalse(rls.isPlayerEligible(false, true)); - Assert.assertFalse(rls.isPlayerEligible(false, false)); - } - - @Test - public void testIsLevelEligible() { - Assert.assertTrue(rls.isLevelEligible(90, 84)); - Assert.assertFalse(rls.isLevelEligible(90, 83)); - } - - @Test - public void testIsDrop() { - Assert.assertTrue(rls.isDrop(1)); - Assert.assertFalse(rls.isDrop(2)); - } - - @Test - public void testTemplateForDifficulty() { - Assert.assertEquals("object/tangible/item/shared_rare_loot_chest_1.iff", rls.templateForDifficulty(CreatureDifficulty.NORMAL)); - Assert.assertEquals("object/tangible/item/shared_rare_loot_chest_2.iff", rls.templateForDifficulty(CreatureDifficulty.ELITE)); - Assert.assertEquals("object/tangible/item/shared_rare_loot_chest_3.iff", rls.templateForDifficulty(CreatureDifficulty.BOSS)); - } - - @Test - public void testChestIdForTemplate() { - Assert.assertEquals("rare_loot_chest_1", rls.chestIdForTemplate("object/tangible/item/shared_rare_loot_chest_1.iff")); - } - -} +data class NPCLootTable(val chance: Int, val lootTable: String) diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/RareItemLootGenerator.kt b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/RareItemLootGenerator.kt new file mode 100644 index 000000000..8a8b3fbf5 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/loot/generation/RareItemLootGenerator.kt @@ -0,0 +1,80 @@ +/*********************************************************************************** + * Copyright (c) 2019 /// Project SWG /// www.projectswg.com * + * * + * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on * + * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. * + * Our goal is to create an emulator which will provide a server for players to * + * continue playing a game similar to the one they used to play. We are basing * + * it on the final publish of the game prior to end-game events. * + * * + * This file is part of Holocore. * + * * + * --------------------------------------------------------------------------------* + * * + * Holocore is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * Holocore is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with Holocore. If not, see . * + ***********************************************************************************/ + +package com.projectswg.holocore.services.gameplay.combat.loot.generation + +import com.projectswg.common.data.encodables.oob.StringId +import com.projectswg.holocore.resources.support.objects.ObjectCreator +import com.projectswg.holocore.resources.support.objects.swg.SWGObject +import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureDifficulty +import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject +import com.projectswg.holocore.services.gameplay.combat.loot.RareLootService.* +import me.joshlarson.jlcommon.log.Log +import java.util.concurrent.ThreadLocalRandom +import kotlin.math.abs + +class RareItemLootGenerator { + + fun generate(corpse: CreatureObject, killer: CreatureObject, loot: MutableList) { + if (abs(corpse.level.toInt() - killer.level.toInt()) > MAX_LEVEL_DIFFERENCE) // Levels can't be too far apart + return + + val roll = 0 //ThreadLocalRandom.current().nextInt(100) + 1 // Rolls from 0 to 99, then we add 1 and it becomes 1 to 100 + if (roll >= DROP_CHANCE) { + Log.d("No RLS drop from %s with roll %d", corpse, roll) + return + } + + val template = templateForDifficulty(corpse.difficulty) + val chest = ObjectCreator.createObjectFromTemplate(template) + + chest.setStf("loot_n", chestIdForTemplate(template) + "_n") + chest.setDetailStf(StringId("loot_n", chestIdForTemplate(template) + "_d")) // Not located in loot_d, for whatever reason... + + loot.add(chest) + } + + private fun chestIdForTemplate(template: String): String { + return template.replace("object/tangible/item/shared_", "").replace(".iff", "") + } + + private fun templateForDifficulty(difficulty: CreatureDifficulty): String { + return when (difficulty) { + CreatureDifficulty.NORMAL -> RARE_CHEST + CreatureDifficulty.ELITE -> EXCEPTIONAL_CHEST + CreatureDifficulty.BOSS -> LEGENDARY_CHEST + } + } + + companion object { + + // TODO these two could be config options + private const val MAX_LEVEL_DIFFERENCE: Short = 6 // +-6 difference is allowed between killer and corpse + private const val DROP_CHANCE = 1 // One in a hundred eligible kills will drop a chest + + } +} diff --git a/src/main/java/com/projectswg/holocore/services/support/objects/items/StaticItemService.kt b/src/main/java/com/projectswg/holocore/services/support/objects/items/StaticItemService.kt index ff6451e10..5acb00885 100644 --- a/src/main/java/com/projectswg/holocore/services/support/objects/items/StaticItemService.kt +++ b/src/main/java/com/projectswg/holocore/services/support/objects/items/StaticItemService.kt @@ -30,14 +30,9 @@ import com.projectswg.common.network.packets.swg.zone.object_controller.ShowLoot import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent import com.projectswg.holocore.intents.support.objects.items.CreateStaticItemIntent import com.projectswg.holocore.intents.support.objects.swg.ObjectCreatedIntent -import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader -import com.projectswg.holocore.resources.support.data.server_info.loader.StaticItemLoader -import com.projectswg.holocore.resources.support.data.server_info.loader.StaticItemLoader.StaticItemInfo -import com.projectswg.holocore.resources.support.objects.ObjectCreator +import com.projectswg.holocore.resources.support.objects.StaticItemCreator import com.projectswg.holocore.resources.support.objects.swg.SWGObject import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject -import com.projectswg.holocore.resources.support.objects.swg.tangible.TangibleObject -import com.projectswg.holocore.resources.support.objects.swg.weapon.WeaponObject import kotlinx.coroutines.* import me.joshlarson.jlcommon.control.Intent import me.joshlarson.jlcommon.control.IntentChain @@ -45,7 +40,6 @@ import me.joshlarson.jlcommon.control.IntentHandler import me.joshlarson.jlcommon.control.Service import me.joshlarson.jlcommon.log.Log import java.util.* -import kotlin.math.floor class StaticItemService : Service() { @@ -63,7 +57,7 @@ class StaticItemService : Service() { val objectCreationHandler = csii.objectCreationHandler // If adding these items to the container would exceed the max capacity... - if (!objectCreationHandler.isIgnoreVolume && container.volume + itemNames.size > container.maxContainerSize) { + if (!objectCreationHandler.isIgnoreVolume && container.containedObjects.size + itemNames.size > container.maxContainerSize) { objectCreationHandler.containerFull() return } @@ -71,9 +65,11 @@ class StaticItemService : Service() { val intentChain = IntentChain() val objects = ArrayList() for (itemName in itemNames) { - val obj = createItem(intentChain, itemName, container) + val obj = StaticItemCreator.createItem(itemName) if (obj != null) { objects.add(obj) + obj.moveToContainer(container) + intentChain.broadcastAfter(ObjectCreatedIntent(obj)) } else { Log.d("%s could not be spawned because the item name is unknown", itemName) val requesterOwner = csii.requester.owner @@ -93,227 +89,11 @@ class StaticItemService : Service() { } } - private fun createItem(intentChain: IntentChain, itemName: String, container: SWGObject): SWGObject? { - val info = DataLoader.staticItems().getItemByName(itemName) ?: return null - val swgObject = ObjectCreator.createObjectFromTemplate(info.iffTemplate) as? TangibleObject ?: return null - - applyAttributes(swgObject, info) - - intentChain.broadcastAfter(ObjectCreatedIntent(swgObject)) - swgObject.moveToContainer(container) - Log.d("Successfully moved %s into container %s", itemName, container) - - return swgObject - } - - private fun applyAttributes(obj: TangibleObject, info: StaticItemInfo) { - obj.addAttribute("condition", String.format("%d/%d", info.hitPoints, info.hitPoints)) - obj.addAttribute("volume", info.volume.toString()) - obj.objectName = info.stringName - - applyAttributes(obj, info.armorInfo) - applyAttributes(obj, info.wearableInfo) - applyAttributes(obj, info.weaponInfo) - applyAttributes(obj, info.collectionInfo) - applyAttributes(obj, info.costumeInfo) - applyAttributes(obj, info.dnaInfo) - applyAttributes(obj, info.grantInfo) - applyAttributes(obj, info.genericInfo) - applyAttributes(obj, info.objectInfo) - applyAttributes(obj, info.schematicInfo) - applyAttributes(obj, info.storytellerInfo) - } - - private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.ArmorItemInfo?) { - if (info == null) - return - - if (info.requiredProfession.isNotEmpty()) - obj.addAttribute("class_required", "@ui_roadmap:title_" + info.requiredProfession) - obj.addAttribute("required_combat_level", info.requiredLevel.toString()) - val kineticMax: Int - val energyMax: Int - when (info.armorType) { - StaticItemLoader.ArmorItemInfo.ArmorType.ASSAULT -> { - kineticMax = 7000 - energyMax = 5000 - obj.addAttribute("armor_category", "@obj_attr_n:armor_assault") - } - StaticItemLoader.ArmorItemInfo.ArmorType.BATTLE -> { - kineticMax = 6000 - energyMax = 6000 - obj.addAttribute("armor_category", "@obj_attr_n:armor_battle") - } - StaticItemLoader.ArmorItemInfo.ArmorType.RECON -> { - kineticMax = 5000 - energyMax = 7000 - obj.addAttribute("armor_category", "@obj_attr_n:armor_reconnaissance") - } - } - - obj.addAttribute("cat_armor_standard_protection.kinetic", calculateProtection(kineticMax, info.protection)) - obj.addAttribute("cat_armor_standard_protection.energy", calculateProtection(energyMax, info.protection)) - obj.addAttribute("cat_armor_special_protection.special_protection_type_heat", calculateProtection(6000, info.protection)) - obj.addAttribute("cat_armor_special_protection.special_protection_type_cold", calculateProtection(6000, info.protection)) - obj.addAttribute("cat_armor_special_protection.special_protection_type_acid", calculateProtection(6000, info.protection)) - obj.addAttribute("cat_armor_special_protection.special_protection_type_electricity", calculateProtection(6000, info.protection)) - - applySkillMods(obj, info.skillMods) - applyColors(obj, info.color) - } - - private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.WearableItemInfo?) { - if (info == null) - return - - if (info.requiredProfession.isNotEmpty()) - obj.addAttribute("class_required", "@ui_roadmap:title_" + info.requiredProfession) - obj.addAttribute("required_combat_level", info.requiredLevel.toString()) - - if (info.requiredFaction.isNotEmpty()) - obj.addAttribute("faction_restriction", "@pvp_factions:" + info.requiredFaction) - - // Apply the mods! - applySkillMods(obj, info.skillMods) - applyColors(obj, info.color) - - // Add the race restrictions only if there are any - val raceRestriction = buildRaceRestrictionString(info) - if (raceRestriction.isNotEmpty()) - obj.addAttribute("species_restrictions.species_name", raceRestriction) - } - - private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.WeaponItemInfo?) { - if (info == null) - return - - if (info.requiredProfession.isNotEmpty()) - obj.addAttribute("class_required", "@ui_roadmap:title_" + info.requiredProfession) - obj.addAttribute("required_combat_level", info.requiredLevel.toString()) - obj.addAttribute("cat_wpn_damage.wpn_damage_type", "@obj_attr_n:${info.damageType.name.toLowerCase(Locale.US)}") - obj.addAttribute("cat_wpn_damage.wpn_category", "@obj_attr_n:wpn_category_" + info.weaponType.num) - obj.addAttribute("cat_wpn_damage.wpn_attack_speed", (info.attackSpeed / 100).toString()) - obj.addAttribute("cat_wpn_damage.damage", "${info.minDamage}-${info.maxDamage}") - if (info.elementalType != null) { // Not all weapons have elemental damage. - obj.addAttribute("cat_wpn_damage.wpn_elemental_type", "@obj_attr_n:${info.elementalType.name.toLowerCase(Locale.US)}") - obj.addAttribute("cat_wpn_damage.wpn_elemental_value", info.elementalDamage.toString()) - } - - obj.addAttribute("cat_wpn_damage.weapon_dps", info.actualDps.toString()) - - if (info.procEffect.isNotEmpty()) - // Not all weapons have a proc effect - obj.addAttribute("proc_name", info.procEffect) - - // TODO set DPS - - obj.addAttribute("cat_wpn_other.wpn_range", String.format("%d-%dm", info.minRange, info.maxRange)) - // Ziggy: Special Action Cost would go under cat_wpn_other as well, but it's a pre-NGE artifact. - - val weapon = obj as WeaponObject - weapon.type = info.weaponType - weapon.attackSpeed = info.attackSpeed.toFloat() - weapon.minRange = info.minRange.toFloat() - weapon.maxRange = info.maxRange.toFloat() - weapon.damageType = info.damageType - weapon.elementalType = info.elementalType - weapon.minDamage = info.minDamage - weapon.maxDamage = info.maxDamage - } - - private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.CollectionItemInfo?) { - if (info == null) - return - - obj.addAttribute("collection_name", info.slotName) - - applyColors(obj, info.color) - } - - private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.CostumeItemInfo?) { -// if (info == null) -// return; - } - - private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.DnaItemInfo?) { -// if (info == null) -// return; - } - - private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.GrantItemInfo?) { -// if (info == null) -// return; - } - - private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.GenericItemInfo?) { - if (info == null) - return - - if (info.value != 0) - obj.addAttribute("charges", info.value.toString()) - - applyColors(obj, info.color) - } - - private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.ObjectItemInfo?) { - if (info == null) - return - - if (info.value != 0) - obj.addAttribute("charges", info.value.toString()) - - applyColors(obj, info.color) - } - - private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.SchematicItemInfo?) { -// if (info == null) -// return; - } - - private fun applyAttributes(obj: TangibleObject, info: StaticItemLoader.StorytellerItemInfo?) { -// if (info == null) -// return; - } - - private fun applySkillMods(obj: TangibleObject, skillMods: Map) { - for ((key, value) in skillMods) - obj.addAttribute(key, value.toString()) - } - - private fun applyColors(obj: TangibleObject, colors: IntArray) { - for (i in 0..3) { - if (colors[i] >= 0) { - obj.putCustomization("/private/index_color_$i", colors[i]) - } - } - } - - private fun buildRaceRestrictionString(info: StaticItemLoader.WearableItemInfo): String { - var races = "" - - if (info.isRaceWookie) - races += "Wookiee " - if (info.isRaceIthorian) - races += "Ithorian " - if (info.isRaceRodian) - races += "Rodian " - if (info.isRaceTrandoshan) - races += "Trandoshan " - if (info.isRaceRest) - races += "MonCal Human Zabrak Bothan Sullustan Twi'lek " - - return if (races.isEmpty()) "" else races.substring(0, races.length - 1) - } - - private fun calculateProtection(max: Int, protection: Double): String { - return floor(max * protection).toInt().toString() - } - interface ObjectCreationHandler { val isIgnoreVolume: Boolean fun success(createdObjects: List) - open fun containerFull() { + fun containerFull() { } }