Skip to content

战斗、治疗与资源 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)
}