外观
战斗、治疗与资源 API 类型参考
本页是 API baseline 1.3 候选源码的公开签名参考。先阅读第三方插件接入教程,按其中的 provider 绑定、owner epoch、执行上下文和释放规则调用。
版本边界
正式交付 1.0.8 与 1.0.12 候选并不等价。依赖版本必须与服务器实际 JAR 配套,并重新编译、在目标服务端验收。
ResourceService.kt
kotlin
package cn.sealplugins.attributes.api.resource
import cn.sealplugins.attributes.api.definition.ResourceId
import cn.sealplugins.attributes.api.internal.requireFinite
import cn.sealplugins.attributes.api.subject.SubjectKey
import java.util.Optional
/** Provider-bound, synchronous in-memory access to committed dynamic resources. */
public interface ResourceService {
public fun query(subject: SubjectKey, resource: ResourceId): ResourceResult
public fun tryConsume(subject: SubjectKey, resource: ResourceId, baseAmount: Double): ResourceResult
public fun restore(subject: SubjectKey, resource: ResourceId, amount: Double): ResourceResult
public fun restorePercent(subject: SubjectKey, resource: ResourceId, maximumRatio: Double): ResourceResult
public fun fill(subject: SubjectKey, resource: ResourceId): ResourceResult
}
/** Immutable committed value of one resource for one exact Subject epoch. */
public class ResourceSnapshot(
public val subject: SubjectKey,
public val resource: ResourceId,
current: Double,
maximum: Double,
public val initialized: Boolean,
public val revision: Long,
) {
public val current: Double = requireFinite(current, "resource current")
public val maximum: Double = requireFinite(maximum, "resource maximum")
init {
require(this.maximum >= 0.0) { "resource maximum must not be negative" }
require(this.current >= 0.0 && this.current <= this.maximum) {
"resource current must be within 0..maximum"
}
require(revision >= 0L) { "resource revision must not be negative" }
}
public fun ratio(): Double = if (maximum > 0.0) current / maximum else 0.0
override fun equals(other: Any?): Boolean = other is ResourceSnapshot &&
subject == other.subject &&
resource == other.resource &&
current.toBits() == other.current.toBits() &&
maximum.toBits() == other.maximum.toBits() &&
initialized == other.initialized &&
revision == other.revision
override fun hashCode(): Int = listOf(
subject,
resource,
current.toBits(),
maximum.toBits(),
initialized,
revision,
).hashCode()
}
/** Typed result shared by resource reads and mutations. */
public class ResourceResult(
public val status: ResourceStatus,
public val subject: SubjectKey,
public val resource: ResourceId,
public val ownerEpoch: Long,
snapshot: Optional<ResourceSnapshot>,
consumedAmount: Double,
restoredAmount: Double,
) {
public val snapshot: Optional<ResourceSnapshot> = snapshot
public val consumedAmount: Double = requireFinite(consumedAmount, "consumedAmount")
public val restoredAmount: Double = requireFinite(restoredAmount, "restoredAmount")
init {
require(ownerEpoch > 0L) { "ownerEpoch must be positive" }
require(this.consumedAmount >= 0.0 && this.restoredAmount >= 0.0) {
"resource result amounts must not be negative"
}
require(this.consumedAmount == 0.0 || this.restoredAmount == 0.0) {
"one resource result cannot consume and restore simultaneously"
}
snapshot.ifPresent {
require(it.subject == subject && it.resource == resource) {
"resource result snapshot must match subject and resource"
}
}
}
public fun successful(): Boolean = status == ResourceStatus.APPLIED
}
public enum class ResourceStatus {
APPLIED,
INSUFFICIENT,
NOT_READY,
DEAD,
UNKNOWN_RESOURCE,
STALE_SUBJECT,
STALE_OWNER,
INVALID_AMOUNT,
CLOSED,
}HealingOrigin.kt
kotlin
package cn.sealplugins.attributes.api.healing
import cn.sealplugins.attributes.api.internal.requireNamespacedId
import cn.sealplugins.attributes.api.subject.SubjectKey
import java.util.Optional
/** Immutable attribution for an explicit healing request or a committed derived heal. */
public class HealingOrigin(
public val type: HealingOriginType,
sourceSubject: Optional<SubjectKey>,
originId: String,
) {
public val sourceSubject: Optional<SubjectKey> = sourceSubject
public val originId: String = requireNamespacedId(originId, "healing originId")
}
/** SKILL/API are public submission origins; all remaining values are emitted only by the core. */
public enum class HealingOriginType {
SKILL,
API,
REGENERATION,
LIFESTEAL,
REVIVE,
ADMIN,
}HealingService.kt
kotlin
package cn.sealplugins.attributes.api.healing
/**
* Synchronous active-healing service for a platform-approved target execution context.
*
* Provider facades may submit only [HealingOriginType.SKILL] and [HealingOriginType.API], and the
* origin ID namespace must match the facade provider ID. REGENERATION, LIFESTEAL, REVIVE and ADMIN
* are core-derived attribution values that may appear in results/events but cannot be injected by
* a third-party submitter. A violation returns [HealingStatus.INVALID_REQUEST] without mutation.
*/
public interface HealingService {
public fun submit(request: HealingRequest): HealingResult
}HealingResult.kt
kotlin
package cn.sealplugins.attributes.api.healing
import cn.sealplugins.attributes.api.internal.requireNonNegativeFinite
/** Immutable post-commit healing result. */
public class HealingResult(
public val status: HealingStatus,
public val request: HealingRequest,
public val runtimeRevision: Long,
public val targetSnapshotRevision: Long,
calculatedAmount: Double,
actualAmount: Double,
effectiveMaxHealth: Double,
) {
public val calculatedAmount: Double = requireNonNegativeFinite(calculatedAmount, "calculatedAmount")
public val actualAmount: Double = requireNonNegativeFinite(actualAmount, "actualAmount")
public val effectiveMaxHealth: Double = requireNonNegativeFinite(effectiveMaxHealth, "effectiveMaxHealth")
init {
require(runtimeRevision >= 0L) { "runtimeRevision must not be negative" }
require(targetSnapshotRevision >= 0L) { "targetSnapshotRevision must not be negative" }
require(this.actualAmount <= this.calculatedAmount) {
"actual healing cannot exceed calculated healing"
}
if (status != HealingStatus.APPLIED) {
require(this.actualAmount == 0.0) { "non-applied healing cannot report an actual amount" }
}
}
}
public enum class HealingStatus {
APPLIED,
ZERO,
NOT_READY,
STALE_SUBJECT,
STALE_RUNTIME,
INVALID_REQUEST,
TARGET_CLOSED,
QUIESCING,
CALCULATION_FAILED,
COMMIT_FAILED,
}HealingRequest.kt
kotlin
package cn.sealplugins.attributes.api.healing
import cn.sealplugins.attributes.api.internal.requireNonNegativeFinite
import cn.sealplugins.attributes.api.subject.SubjectKey
import java.util.UUID
/**
* Active healing request; healing is never represented as negative combat damage.
* An [expectedRuntimeRevision] of `0` captures the current root, while a positive value pins it.
*/
public class HealingRequest(
public val requestId: UUID,
public val targetSubject: SubjectKey,
public val origin: HealingOrigin,
baseAmount: Double,
public val expectedRuntimeRevision: Long,
) {
public val baseAmount: Double = requireNonNegativeFinite(baseAmount, "healing baseAmount")
init {
require(expectedRuntimeRevision >= 0L) { "expectedRuntimeRevision must not be negative" }
}
}CombatService.kt
kotlin
package cn.sealplugins.attributes.api.combat
import cn.sealplugins.attributes.api.definition.RegistrationResult
import java.util.Optional
/**
* Provider-bound plan/gate registration plus synchronous deterministic combat submission.
*
* Submission must occur in a platform-approved entity execution context. Invalid context,
* readiness or revision is reported in [CombatResult] without performing a partial commit.
* Public providers may submit only API/SKILL origins and may select built-in plans or plans in
* their own namespace.
*/
public interface CombatService {
public fun registerPlan(plan: CombatPlan): RegistrationResult
public fun registerGate(gate: CombatGate): RegistrationResult
public fun findPlan(id: CombatPlanId): Optional<CombatPlan>
public fun submit(request: CombatRequest): CombatResult
}CombatResult.kt
kotlin
package cn.sealplugins.attributes.api.combat
import cn.sealplugins.attributes.api.ApiLimits
import cn.sealplugins.attributes.api.internal.immutableList
import cn.sealplugins.attributes.api.internal.requireNamespacedId
import cn.sealplugins.attributes.api.internal.requireNonNegativeFinite
import java.util.Optional
/** Read-only result published only after the runtime has decided whether commit succeeded. */
public class CombatResult private constructor(
public val status: CombatStatus,
public val request: CombatRequest,
public val runtimeRevision: Long,
public val actorSnapshotRevision: Long,
public val targetSnapshotRevision: Long,
physicalDamage: Double,
spellDamage: Double,
trueDamage: Double,
blockedDamage: Double,
actualLifeLost: Double,
public val critical: Boolean,
public val feedback: CombatFeedback,
externalBlockReasonId: Optional<String>,
diagnostics: Collection<CombatDiagnostic>,
lifestealAmount: Double,
reflectionDamage: Double,
) {
public val physicalDamage: Double = requireNonNegativeFinite(physicalDamage, "physicalDamage")
public val spellDamage: Double = requireNonNegativeFinite(spellDamage, "spellDamage")
public val trueDamage: Double = requireNonNegativeFinite(trueDamage, "trueDamage")
public val blockedDamage: Double = requireNonNegativeFinite(blockedDamage, "blockedDamage")
public val actualLifeLost: Double = requireNonNegativeFinite(actualLifeLost, "actualLifeLost")
public val lifestealAmount: Double = requireNonNegativeFinite(lifestealAmount, "lifestealAmount")
public val reflectionDamage: Double = requireNonNegativeFinite(reflectionDamage, "reflectionDamage")
/** Gate-supplied canonical reason; gate exceptions use a GATE_EXCEPTION diagnostic instead. */
public val externalBlockReasonId: Optional<String> = externalBlockReasonId.map {
requireNamespacedId(it, "externalBlockReasonId")
}
public val diagnostics: List<CombatDiagnostic> = immutableList(
diagnostics,
ApiLimits.MAX_COMBAT_DIAGNOSTICS,
"combat diagnostics",
)
private val calculatedDamageValue: Double = physicalDamage + spellDamage + trueDamage
init {
require(runtimeRevision >= 0L) { "runtimeRevision must not be negative" }
require(actorSnapshotRevision >= 0L) { "actorSnapshotRevision must not be negative" }
require(targetSnapshotRevision >= 0L) { "targetSnapshotRevision must not be negative" }
require(calculatedDamageValue.isFinite()) { "calculated damage must remain finite" }
require(this.actualLifeLost <= calculatedDamageValue) {
"actual life loss cannot exceed calculated damage"
}
if (status != CombatStatus.APPLIED) {
require(this.actualLifeLost == 0.0) { "non-applied combat cannot report life loss" }
require(this.lifestealAmount == 0.0) { "non-applied combat cannot report lifesteal" }
require(this.reflectionDamage == 0.0) { "non-applied combat cannot report reflection" }
}
if (status == CombatStatus.EXTERNAL_BLOCKED) {
require(
this.externalBlockReasonId.isPresent ||
this.diagnostics.any { it.code == CombatDiagnosticCode.GATE_EXCEPTION },
) { "external blocks require a gate reason or GATE_EXCEPTION diagnostic" }
} else {
require(this.externalBlockReasonId.isEmpty) {
"only EXTERNAL_BLOCKED results may expose an external block reason"
}
}
}
public fun calculatedDamage(): Double = calculatedDamageValue
/** Preserves the V1 constructor binary signature; derived health changes default to zero. */
public constructor(
status: CombatStatus,
request: CombatRequest,
runtimeRevision: Long,
actorSnapshotRevision: Long,
targetSnapshotRevision: Long,
physicalDamage: Double,
spellDamage: Double,
trueDamage: Double,
blockedDamage: Double,
actualLifeLost: Double,
critical: Boolean,
feedback: CombatFeedback,
externalBlockReasonId: Optional<String>,
diagnostics: Collection<CombatDiagnostic>,
) : this(
status,
request,
runtimeRevision,
actorSnapshotRevision,
targetSnapshotRevision,
physicalDamage,
spellDamage,
trueDamage,
blockedDamage,
actualLifeLost,
critical,
feedback,
externalBlockReasonId,
diagnostics,
0.0,
0.0,
)
public companion object {
/** Creates the authoritative post-commit result with platform-observed derived amounts. */
@JvmStatic
public fun postCommit(
status: CombatStatus,
request: CombatRequest,
runtimeRevision: Long,
actorSnapshotRevision: Long,
targetSnapshotRevision: Long,
physicalDamage: Double,
spellDamage: Double,
trueDamage: Double,
blockedDamage: Double,
actualLifeLost: Double,
lifestealAmount: Double,
reflectionDamage: Double,
critical: Boolean,
feedback: CombatFeedback,
externalBlockReasonId: Optional<String>,
diagnostics: Collection<CombatDiagnostic>,
): CombatResult = CombatResult(
status,
request,
runtimeRevision,
actorSnapshotRevision,
targetSnapshotRevision,
physicalDamage,
spellDamage,
trueDamage,
blockedDamage,
actualLifeLost,
critical,
feedback,
externalBlockReasonId,
diagnostics,
lifestealAmount,
reflectionDamage,
)
}
}
public enum class CombatStatus {
APPLIED,
MISSED,
COOLDOWN,
BLOCKED,
EXTERNAL_BLOCKED,
NOT_READY,
STALE_SUBJECT,
STALE_RUNTIME,
UNKNOWN_PLAN,
INVALID_REQUEST,
TARGET_CLOSED,
QUIESCING,
CALCULATION_FAILED,
COMMIT_FAILED,
}
/** Typed, bounded diagnostic emitted when one extension is isolated. */
public class CombatDiagnostic(
extensionId: String,
public val code: CombatDiagnosticCode,
) {
public val extensionId: String = requireNamespacedId(extensionId, "diagnostic extensionId")
}
public enum class CombatDiagnosticCode {
EXTENSION_EXCEPTION,
INVALID_EXTENSION_RESULT,
EXTENSION_LIMIT_EXCEEDED,
FORMULA_EXCEPTION,
INVALID_FORMULA_RESULT,
GATE_EXCEPTION,
LIFESTEAL_COMMIT_FAILED,
REFLECTION_COMMIT_FAILED,
FEEDBACK_FAILED,
ATTRIBUTION_FAILED,
}CombatRequest.kt
kotlin
package cn.sealplugins.attributes.api.combat
import cn.sealplugins.attributes.api.subject.SubjectKey
import java.util.UUID
/**
* Immutable request; the service captures both committed snapshots from one runtime revision.
* An [expectedRuntimeRevision] of `0` requests the current committed revision; a positive value
* pins the call and produces `STALE_RUNTIME` if the root revision has changed.
*/
public class CombatRequest(
public val requestId: UUID,
public val planId: CombatPlanId,
public val origin: CombatOrigin,
public val targetSubject: SubjectKey,
public val expectedRuntimeRevision: Long,
) {
init {
require(expectedRuntimeRevision >= 0L) { "expectedRuntimeRevision must not be negative" }
}
}CombatPlanId.kt
kotlin
package cn.sealplugins.attributes.api.combat
import cn.sealplugins.attributes.api.internal.requireNamespace
import cn.sealplugins.attributes.api.internal.requireNamespacedId
import cn.sealplugins.attributes.api.internal.requirePath
/** Canonical `namespace:path` identifier for a prevalidated combat plan. */
public class CombatPlanId private constructor(
public val namespace: String,
public val path: String,
) : Comparable<CombatPlanId> {
public val value: String = "$namespace:$path"
override fun compareTo(other: CombatPlanId): Int = value.compareTo(other.value)
override fun equals(other: Any?): Boolean = other is CombatPlanId && value == other.value
override fun hashCode(): Int = value.hashCode()
override fun toString(): String = value
public companion object {
@JvmStatic
public fun of(namespace: String, path: String): CombatPlanId {
val canonicalNamespace = requireNamespace(namespace, "combat plan namespace")
val canonicalPath = requirePath(path, "combat plan path")
requireNamespacedId("$canonicalNamespace:$canonicalPath", "combatPlanId")
return CombatPlanId(canonicalNamespace, canonicalPath)
}
@JvmStatic
public fun parse(value: String): CombatPlanId {
requireNamespacedId(value, "combatPlanId")
val separator = value.indexOf(':')
return CombatPlanId(value.substring(0, separator), value.substring(separator + 1))
}
}
}CombatPlan.kt
kotlin
package cn.sealplugins.attributes.api.combat
import cn.sealplugins.attributes.api.internal.immutableSet
import cn.sealplugins.attributes.api.internal.requireNonNegativeFinite
/**
* Immutable, startup-registered damage-channel plan.
*
* The defense reference floors keep fixed-damage plans mathematically valid when the matching
* attacker attribute is zero. A floor must be positive whenever its damage channel is active.
*/
public class CombatPlan(
public val id: CombatPlanId,
fixedPhysicalDamage: Double,
physicalAttackMultiplier: Double,
physicalDefenseReferenceFloor: Double,
fixedSpellDamage: Double,
spellAttackMultiplier: Double,
spellDefenseReferenceFloor: Double,
fixedTrueDamage: Double,
trueDamageMultiplier: Double,
capabilities: Collection<CombatCapability>,
public val feedback: CombatFeedback,
) {
public val fixedPhysicalDamage: Double = requireNonNegativeFinite(fixedPhysicalDamage, "fixedPhysicalDamage")
public val physicalAttackMultiplier: Double =
requireNonNegativeFinite(physicalAttackMultiplier, "physicalAttackMultiplier")
public val physicalDefenseReferenceFloor: Double =
requireNonNegativeFinite(physicalDefenseReferenceFloor, "physicalDefenseReferenceFloor")
public val fixedSpellDamage: Double = requireNonNegativeFinite(fixedSpellDamage, "fixedSpellDamage")
public val spellAttackMultiplier: Double =
requireNonNegativeFinite(spellAttackMultiplier, "spellAttackMultiplier")
public val spellDefenseReferenceFloor: Double =
requireNonNegativeFinite(spellDefenseReferenceFloor, "spellDefenseReferenceFloor")
public val fixedTrueDamage: Double = requireNonNegativeFinite(fixedTrueDamage, "fixedTrueDamage")
public val trueDamageMultiplier: Double =
requireNonNegativeFinite(trueDamageMultiplier, "trueDamageMultiplier")
public val capabilities: Set<CombatCapability> = immutableSet(
capabilities,
CombatCapability.entries.size,
"combat plan capabilities",
)
public val capabilityBits: Int = CombatCapability.bitsOf(this.capabilities)
init {
if (this.fixedPhysicalDamage > 0.0 || this.physicalAttackMultiplier > 0.0) {
require(this.physicalDefenseReferenceFloor > 0.0) {
"active physical channels require a positive defense reference floor"
}
}
if (this.fixedSpellDamage > 0.0 || this.spellAttackMultiplier > 0.0) {
require(this.spellDefenseReferenceFloor > 0.0) {
"active spell channels require a positive defense reference floor"
}
}
}
/** Source-compatible V1 constructor; fixed true damage defaults to zero. */
public constructor(
id: CombatPlanId,
fixedPhysicalDamage: Double,
physicalAttackMultiplier: Double,
physicalDefenseReferenceFloor: Double,
fixedSpellDamage: Double,
spellAttackMultiplier: Double,
spellDefenseReferenceFloor: Double,
trueDamageMultiplier: Double,
capabilities: Collection<CombatCapability>,
feedback: CombatFeedback,
) : this(
id,
fixedPhysicalDamage,
physicalAttackMultiplier,
physicalDefenseReferenceFloor,
fixedSpellDamage,
spellAttackMultiplier,
spellDefenseReferenceFloor,
0.0,
trueDamageMultiplier,
capabilities,
feedback,
)
}
/** Platform-neutral feedback requested only after successful committed damage. */
public class CombatFeedback(
public val hurtAnimation: Boolean,
public val hurtSound: Boolean,
knockback: Double,
) {
public val knockback: Double = requireNonNegativeFinite(knockback, "knockback")
}CombatOrigin.kt
kotlin
package cn.sealplugins.attributes.api.combat
import cn.sealplugins.attributes.api.subject.SubjectKey
import java.util.Optional
import java.util.UUID
/** Immutable attribution and positive capability flags for one combat request. */
public class CombatOrigin(
directSourceId: Optional<UUID>,
public val actorSubject: SubjectKey,
ownerSubject: Optional<SubjectKey>,
public val type: CombatOriginType,
public val capabilityBits: Int,
) {
public val directSourceId: Optional<UUID> = directSourceId
public val ownerSubject: Optional<SubjectKey> = ownerSubject
init {
require(capabilityBits >= 0 && (capabilityBits and CombatCapability.ALL_BITS) == capabilityBits) {
"combat origin contains unknown capability bits"
}
}
public fun supports(capability: CombatCapability): Boolean = capabilityBits and capability.bit != 0
}
public enum class CombatOriginType {
MELEE,
PROJECTILE,
SUMMON,
EXPLOSION,
SKILL,
REFLECTION,
DAMAGE_OVER_TIME,
API,
}
/** Fixed positive capability bits; absence means the behavior is not allowed. */
public enum class CombatCapability(public val bit: Int) {
EVADABLE(1 shl 0),
CRITTABLE(1 shl 1),
AFFECTED_BY_DEFENSE(1 shl 2),
BLOCKABLE(1 shl 3),
TRIGGERS_LIFESTEAL(1 shl 4),
TRIGGERS_REFLECTION(1 shl 5),
;
public companion object {
public const val ALL_BITS: Int = (1 shl 6) - 1
@JvmStatic
public fun bitsOf(capabilities: Collection<CombatCapability>): Int =
capabilities.fold(0) { bits, capability -> bits or capability.bit }
}
}CombatGate.kt
kotlin
package cn.sealplugins.attributes.api.combat
import cn.sealplugins.attributes.api.internal.requireNamespacedId
import cn.sealplugins.attributes.api.internal.requirePriority
import cn.sealplugins.attributes.api.subject.AttributeSnapshot
import java.util.Optional
/**
* Explicit protection adapter evaluated before authoritative combat begins.
*
* Gates execute synchronously on the platform-approved target execution context, ordered by
* `priority`, provider namespace and gate ID. A gate returns only [GateDecision]; it cannot modify
* events, snapshots or entity health. A protection adapter may synchronously read its platform or
* protection-plugin API in that context, but must not perform blocking I/O or reentrant
* SealAttributes mutations. [GateOutcome.DENY] stops the full combat chain with
* [CombatStatus.EXTERNAL_BLOCKED]. An exception or null Java return is fail-closed with the same
* status plus [CombatDiagnosticCode.GATE_EXCEPTION]; later gates and combat stages do not run.
*/
public interface CombatGate {
public fun id(): String
public fun priority(): Int
public fun evaluate(context: CombatGateContext): GateDecision
}
public class CombatGateContext(
public val request: CombatRequest,
public val actorSnapshot: AttributeSnapshot,
public val targetSnapshot: AttributeSnapshot,
)
public class GateDecision private constructor(
public val outcome: GateOutcome,
reasonId: Optional<String>,
) {
public val reasonId: Optional<String> = reasonId.map { requireNamespacedId(it, "gate reasonId") }
init {
require((outcome == GateOutcome.DENY) == this.reasonId.isPresent) {
"DENY requires a reasonId and PASS cannot provide one"
}
}
public companion object {
private val PASS: GateDecision = GateDecision(GateOutcome.PASS, Optional.empty())
@JvmStatic
public fun pass(): GateDecision = PASS
@JvmStatic
public fun deny(reasonId: String): GateDecision =
GateDecision(GateOutcome.DENY, Optional.of(reasonId))
}
}
public enum class GateOutcome {
PASS,
DENY,
}
public object CombatGateContract {
@JvmStatic
public fun requireId(value: String): String = requireNamespacedId(value, "combat gate id")
@JvmStatic
public fun validatePriority(value: Int): Int = requirePriority(value)
}CombatExtension.kt
kotlin
package cn.sealplugins.attributes.api.combat
import cn.sealplugins.attributes.api.ApiLimits
import cn.sealplugins.attributes.api.internal.immutableList
import cn.sealplugins.attributes.api.internal.requireFinite
import cn.sealplugins.attributes.api.internal.requireNamespacedId
import cn.sealplugins.attributes.api.internal.requirePriority
import cn.sealplugins.attributes.api.subject.AttributeSnapshot
/**
* Deterministic startup-registered combat extension.
*
* Evaluation is synchronous on the platform-approved target execution context used by
* [CombatService.submit]. Stable execution order is `stage`, then `priority`, provider namespace
* and extension ID. Implementations must be deterministic and must not perform I/O, platform
* access, mutable global reads or reentrant SealAttributes mutations. The runtime discards this
* invocation's complete modifier batch, records a typed diagnostic and continues when the
* extension throws, returns null from Java or returns any invalid modifier.
*/
public interface CombatExtension {
public fun id(): String
public fun stage(): CombatStage
public fun priority(): Int
public fun apply(context: CombatExtensionContext): CombatExtensionResult
}
/** Fixed extension points inside the pure combat calculation. */
public enum class CombatStage {
BEFORE_HIT_CHECK,
AFTER_HIT_CHECK,
BEFORE_MITIGATION,
AFTER_MITIGATION,
FINALIZE,
}
/** Immutable view of the current calculation frame. */
public class CombatExtensionContext(
public val request: CombatRequest,
public val plan: CombatPlan,
public val actorSnapshot: AttributeSnapshot,
public val targetSnapshot: AttributeSnapshot,
public val stage: CombatStage,
public val frame: CombatFrame,
)
/** Fixed-field immutable calculation frame; no dynamic map is created on the combat hot path. */
public class CombatFrame(
accuracyRating: Double,
evasionRating: Double,
criticalChance: Double,
criticalMultiplier: Double,
physicalDamage: Double,
spellDamage: Double,
trueDamage: Double,
finalDamage: Double,
lifestealStrength: Double,
reflectionStrength: Double,
blockStrength: Double,
) {
public val accuracyRating: Double = requireFinite(accuracyRating, "accuracyRating")
public val evasionRating: Double = requireFinite(evasionRating, "evasionRating")
public val criticalChance: Double = requireFinite(criticalChance, "criticalChance")
public val criticalMultiplier: Double = requireFinite(criticalMultiplier, "criticalMultiplier")
public val physicalDamage: Double = requireFinite(physicalDamage, "physicalDamage")
public val spellDamage: Double = requireFinite(spellDamage, "spellDamage")
public val trueDamage: Double = requireFinite(trueDamage, "trueDamage")
public val finalDamage: Double = requireFinite(finalDamage, "finalDamage")
public val lifestealStrength: Double = requireFinite(lifestealStrength, "lifestealStrength")
public val reflectionStrength: Double = requireFinite(reflectionStrength, "reflectionStrength")
public val blockStrength: Double = requireFinite(blockStrength, "blockStrength")
public fun value(key: CombatValue): Double = when (key) {
CombatValue.ACCURACY_RATING -> accuracyRating
CombatValue.EVASION_RATING -> evasionRating
CombatValue.CRITICAL_CHANCE -> criticalChance
CombatValue.CRITICAL_MULTIPLIER -> criticalMultiplier
CombatValue.PHYSICAL_DAMAGE -> physicalDamage
CombatValue.SPELL_DAMAGE -> spellDamage
CombatValue.TRUE_DAMAGE -> trueDamage
CombatValue.FINAL_DAMAGE -> finalDamage
CombatValue.LIFESTEAL_STRENGTH -> lifestealStrength
CombatValue.REFLECTION_STRENGTH -> reflectionStrength
CombatValue.BLOCK_STRENGTH -> blockStrength
}
}
/** Atomic structured delta returned by one extension invocation. */
public class CombatExtensionResult(modifiers: Collection<CombatModifier>) {
public val modifiers: List<CombatModifier> = immutableList(
modifiers,
ApiLimits.MAX_COMBAT_MODIFIERS_PER_EXTENSION,
"combat extension modifiers",
)
public companion object {
private val NO_CHANGE: CombatExtensionResult = CombatExtensionResult(emptyList())
@JvmStatic
public fun noChange(): CombatExtensionResult = NO_CHANGE
}
}
public class CombatModifier(
public val value: CombatValue,
public val operation: CombatModifierOperation,
operand: Double,
) {
public val operand: Double = requireFinite(operand, "combat modifier operand")
init {
if (operation == CombatModifierOperation.MULTIPLY) {
require(this.operand >= 0.0) { "combat multipliers must not be negative" }
}
}
}
public enum class CombatModifierOperation {
ADD,
MULTIPLY,
}
public enum class CombatValue {
ACCURACY_RATING,
EVASION_RATING,
CRITICAL_CHANCE,
CRITICAL_MULTIPLIER,
PHYSICAL_DAMAGE,
SPELL_DAMAGE,
TRUE_DAMAGE,
FINAL_DAMAGE,
LIFESTEAL_STRENGTH,
REFLECTION_STRENGTH,
BLOCK_STRENGTH,
}
/** Shared validation helpers for registration implementations. */
public object CombatExtensionContract {
@JvmStatic
public fun requireId(value: String): String = requireNamespacedId(value, "combat extension id")
@JvmStatic
public fun validatePriority(value: Int): Int = requirePriority(value)
}