mirror of
https://github.com/ProjectSWGCore/Holocore.git
synced 2026-10-06 21:13:37 -04:00
Restructured loot generation and added a StaticItemCreator to streamline creating static items
This commit is contained in:
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||
***********************************************************************************/
|
||||
|
||||
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<String, Int>) {
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
+2
@@ -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());
|
||||
|
||||
+58
-142
@@ -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<String, NPCLoot> = 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<SWGObject>()
|
||||
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<NPCTable> = ArrayList())
|
||||
|
||||
private class NPCTable(val chance: Int, val lootTable: String)
|
||||
|
||||
private class CreateStaticItemCallback : StaticItemService.ObjectCreationHandler {
|
||||
|
||||
override val isIgnoreVolume = true
|
||||
|
||||
override fun success(createdObjects: List<SWGObject>) {
|
||||
// do nothing - loot disc is created on the return of the generateLoot method
|
||||
}
|
||||
|
||||
}
|
||||
private class NPCLoot(val isDropCredits: Boolean, val npcTables: MutableList<NPCLootTable> = ArrayList())
|
||||
|
||||
}
|
||||
|
||||
-92
@@ -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<String> rareItems;
|
||||
private final List<String> exceptionalItems;
|
||||
private final List<String> 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<String> items) {
|
||||
int availableItems = items.size();
|
||||
int randomIndex = ThreadLocalRandom.current().nextInt(availableItems);
|
||||
|
||||
+70
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||
***********************************************************************************/
|
||||
|
||||
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<SWGObject>) {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
+97
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||
***********************************************************************************/
|
||||
|
||||
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<SWGObject>, lootTables: List<NPCLootTable>) {
|
||||
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())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+3
-46
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||
***********************************************************************************/
|
||||
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)
|
||||
+80
@@ -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 <http://www.gnu.org/licenses/>. *
|
||||
***********************************************************************************/
|
||||
|
||||
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<SWGObject>) {
|
||||
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
|
||||
|
||||
}
|
||||
}
|
||||
+6
-226
@@ -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<SWGObject>()
|
||||
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<String, Int>) {
|
||||
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<SWGObject>)
|
||||
|
||||
open fun containerFull() {
|
||||
fun containerFull() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user