From c871c72dd77f28a5012baa185c45a584b8b05f51 Mon Sep 17 00:00:00 2001 From: Mads Boddum Date: Thu, 10 Sep 2026 18:15:05 +0200 Subject: [PATCH 1/2] Player item condition decays when cloning Insurance is accounted for. At present time, it is not possible to insure your items. --- .../gameplay/combat/cloning/CloningService.kt | 51 +++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/cloning/CloningService.kt b/src/main/java/com/projectswg/holocore/services/gameplay/combat/cloning/CloningService.kt index afe025daf..0b80a66dd 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/cloning/CloningService.kt +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/cloning/CloningService.kt @@ -31,6 +31,7 @@ import com.projectswg.common.data.encodables.tangible.Posture import com.projectswg.common.data.encodables.tangible.PvpFaction import com.projectswg.common.data.encodables.tangible.PvpStatus import com.projectswg.common.data.location.Location +import com.projectswg.common.data.objects.GameObjectType import com.projectswg.common.data.sui.SuiEvent import com.projectswg.common.network.packets.swg.zone.PlayClientEffectObjectMessage import com.projectswg.common.network.packets.swg.zone.PlayMusicMessage @@ -52,6 +53,8 @@ import com.projectswg.holocore.resources.support.global.zone.sui.SuiWindow import com.projectswg.holocore.resources.support.objects.swg.building.BuildingObject import com.projectswg.holocore.resources.support.objects.swg.cell.CellObject import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject +import com.projectswg.holocore.resources.support.objects.swg.tangible.OptionFlag +import com.projectswg.holocore.resources.support.objects.swg.tangible.TangibleObject import com.projectswg.holocore.services.support.objects.ObjectStorageService.BuildingLookup import com.projectswg.holocore.utilities.HolocoreCoroutine import com.projectswg.holocore.utilities.cancelAndWait @@ -67,7 +70,7 @@ import java.util.stream.Collectors class CloningService : Service() { private val coroutineScope = HolocoreCoroutine.childScope() - private val reviveTimers: MutableMap = HashMap() + private val reviveTimers: MutableMap = HashMap() private val cloningFacilities: MutableList = ArrayList() override fun stop(): Boolean { @@ -86,7 +89,7 @@ class CloningService : Service() { SystemMessageIntent(corpseOwner, ProsePackage(StringId("base_player", "revive_exp_msg"), "TT", "$CLONE_TIMER minutes.")).broadcast() } - scheduleCloneTimer(corpse) + scheduleCloneTimer(corpse, i.killer.isPlayer) } @IntentHandler @@ -114,7 +117,7 @@ class CloningService : Service() { private fun handlePlayerEventIntent(i: PlayerEventIntent) { val creature = i.player.creatureObject when (i.event) { - PlayerEvent.PE_DISAPPEAR -> reviveTimers.remove(creature)?.cancel() + PlayerEvent.PE_DISAPPEAR -> reviveTimers.remove(creature)?.job?.cancel() PlayerEvent.PE_FIRST_ZONE -> if (creature.posture == Posture.DEAD) { // They're dead, but they have no active revive timer. // In this case, they didn't clone before the application was shut down and started back up. @@ -126,7 +129,7 @@ class CloningService : Service() { } } - private fun scheduleCloneTimer(corpse: CreatureObject) { + private fun scheduleCloneTimer(corpse: CreatureObject, pvpDeath: Boolean = false) { val availableFacilities = getAvailableFacilities(corpse) if (availableFacilities.isEmpty()) { val defaultCloner = defaultCloner @@ -137,10 +140,11 @@ class CloningService : Service() { cloningWindow.display(corpse.owner!!) synchronized(reviveTimers) { - reviveTimers[corpse] = coroutineScope.launch { + val job = coroutineScope.launch { delay(TimeUnit.MINUTES.toMillis(CLONE_TIMER)) expireCloneTimer(corpse, availableFacilities, cloningWindow) } + reviveTimers[corpse] = ReviveTimer(job, pvpDeath) } StandardLog.onPlayerEvent(this, corpse, "has %d minutes to clone", CLONE_TIMER) @@ -202,13 +206,14 @@ class CloningService : Service() { // Cancel the forced cloning timer - synchronized(reviveTimers) { - reviveTimers.remove(corpse)?.cancel() + val reviveTimer = synchronized(reviveTimers) { + reviveTimers.remove(corpse) } + reviveTimer?.job?.cancel() StandardLog.onPlayerEvent(this, corpse, "cloned to %s @ %s", selectedFacility, selectedFacility.location) val diedOnTerrain = corpse.terrain - teleport(corpse, cellObject, getCloneLocation(facilityData, selectedFacility)) + teleport(corpse, cellObject, getCloneLocation(facilityData, selectedFacility), reviveTimer?.pvpDeath ?: false) CloneActivatedIntent(corpse, diedOnTerrain).broadcast() return CloneResult.SUCCESS } @@ -234,12 +239,13 @@ class CloningService : Service() { return cloneLocation.build() } - private fun teleport(corpse: CreatureObject, cellObject: CellObject, cloneLocation: Location) { + private fun teleport(corpse: CreatureObject, cellObject: CellObject, cloneLocation: Location, pvpDeath: Boolean) { corpse.moveToContainer(cellObject, cloneLocation) corpse.posture = Posture.UPRIGHT corpse.setTurnScale(1.0) corpse.setMovementPercent(1.0) applyCloneWounds(corpse) + if (!pvpDeath) decayItems(corpse) corpse.health = corpse.maxHealth - corpse.healthWounds corpse.sendObservers(PlayClientEffectObjectMessage("clienteffect/player_clone_compile.cef", "", corpse.objectId, "")) corpse.sendSelf(PlayMusicMessage(0, "sound/item_repairobj.snd", 1, false)) @@ -254,6 +260,26 @@ class CloningService : Service() { corpse.healthWounds = (corpse.healthWounds + CLONE_WOUNDS).coerceAtMost(woundCeiling) } + private fun decayItems(corpse: CreatureObject) { + getDecayableItems(corpse).filterNot { it.hasOptionFlags(OptionFlag.INSURED, OptionFlag.UNINSURABLE) }.forEach { decayItem(it) } + } + + private fun decayItem(item: TangibleObject) { + val insured = item.hasOptionFlags(OptionFlag.INSURED) + val decay = (item.maxHitPoints * if (insured) INSURED_DECAY_PERCENTAGE else UNINSURED_DECAY_PERCENTAGE).toInt() + + item.conditionDamage = (item.conditionDamage + decay).coerceAtMost(item.maxHitPoints) + + if (insured) item.removeOptionFlags(OptionFlag.INSURED) + } + + private fun getDecayableItems(corpse: CreatureObject): List { + val equipped = corpse.slottedObjects + val carried = corpse.inventory.childObjectsRecursively + + return (equipped + carried).filterIsInstance().filter { it.gameObjectType.mask in DECAYABLE_TYPES } + } + /** * Picks the closest cloning facility and clones `cloneRequestor` * there. If an error occurs upon attempting to clone, it will pick the next @@ -276,7 +302,7 @@ class CloningService : Service() { } private fun expireCloneTimer(corpse: CreatureObject, facilitiesInTerrain: List, suiWindow: SuiWindow) { - if (reviveTimers.remove(corpse) != null) { + if (reviveTimers.containsKey(corpse)) { val corpseOwner = corpse.owner if (corpseOwner != null) { @@ -316,6 +342,8 @@ class CloningService : Service() { return factionRestriction == null || factionRestriction == corpse.pvpFaction } + private data class ReviveTimer(val job: Job, val pvpDeath: Boolean) + private enum class CloneResult { INVALID_SELECTION, TEMPLATE_MISSING, @@ -327,6 +355,9 @@ class CloningService : Service() { private const val CLONE_TIMER: Long = 30 // Amount of minutes before a player is forced to clone private const val CLONE_WOUNDS = 100 // Health wounds received when cloning away from your clone data private const val MAX_WOUND_PERCENTAGE = 0.7 // Wounds can never take more than this share of unmodified maximum health + private const val INSURED_DECAY_PERCENTAGE = 0.01 + private const val UNINSURED_DECAY_PERCENTAGE = 0.05 + private val DECAYABLE_TYPES = setOf(GameObjectType.GOT_ARMOR, GameObjectType.GOT_CLOTHING, GameObjectType.GOT_JEWELRY, GameObjectType.GOT_TOOL) private val defaultCloner: BuildingObject? get() { val defaultCloner = BuildingLookup.getBuildingByTag("tat_moseisley_cloning1") From 103efef8270c7a456919811650a9cb57cad17b15 Mon Sep 17 00:00:00 2001 From: Mads Boddum Date: Thu, 10 Sep 2026 21:50:22 +0200 Subject: [PATCH 2/2] Refactored teleport function, extracting the decayItems invocation --- .../services/gameplay/combat/cloning/CloningService.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/cloning/CloningService.kt b/src/main/java/com/projectswg/holocore/services/gameplay/combat/cloning/CloningService.kt index 0b80a66dd..5deb68e4f 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/cloning/CloningService.kt +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/cloning/CloningService.kt @@ -213,7 +213,8 @@ class CloningService : Service() { StandardLog.onPlayerEvent(this, corpse, "cloned to %s @ %s", selectedFacility, selectedFacility.location) val diedOnTerrain = corpse.terrain - teleport(corpse, cellObject, getCloneLocation(facilityData, selectedFacility), reviveTimer?.pvpDeath ?: false) + if (reviveTimer?.pvpDeath != true) decayItems(corpse) + teleport(corpse, cellObject, getCloneLocation(facilityData, selectedFacility)) CloneActivatedIntent(corpse, diedOnTerrain).broadcast() return CloneResult.SUCCESS } @@ -239,13 +240,12 @@ class CloningService : Service() { return cloneLocation.build() } - private fun teleport(corpse: CreatureObject, cellObject: CellObject, cloneLocation: Location, pvpDeath: Boolean) { + private fun teleport(corpse: CreatureObject, cellObject: CellObject, cloneLocation: Location) { corpse.moveToContainer(cellObject, cloneLocation) corpse.posture = Posture.UPRIGHT corpse.setTurnScale(1.0) corpse.setMovementPercent(1.0) applyCloneWounds(corpse) - if (!pvpDeath) decayItems(corpse) corpse.health = corpse.maxHealth - corpse.healthWounds corpse.sendObservers(PlayClientEffectObjectMessage("clienteffect/player_clone_compile.cef", "", corpse.objectId, "")) corpse.sendSelf(PlayMusicMessage(0, "sound/item_repairobj.snd", 1, false))