Skip to content

Attributes — System Boundaries

What Attributes deliberately does not do, and why. Each of these is a decision, not a gap waiting to be filled — knowing them is how you avoid designing around something that will never arrive.

It does not ship a stacking model

There is no default combiner, and there will not be one. The seams this framework already sold refuse to canonicalise combination — IDamageAffinity documents its semantics as consumer-defined, IStatusResistance is a 0..1 scale, IStatusPotency an absolute multiplier — and one published rule here would contradict at least one of them. Structurally, there is nowhere in Runtime/Systems/Attributes to put a formula: the combine step is an interface the container calls, and crossing that line would require adding a place for a formula to live, which is visible in review.

The moment this gets tested is the first support question — "what formula do you recommend?" The answer is a recipe to copy and own, not a framework default.

It does not write max health — or any other system's state

Health owns max health: its inspector field, its defaults asset, its snapshot. Attributes never writes it. The planned route runs the other way — Health publishing a contributor seam it asks, the way Crafting publishes ICraftingLevelSource — and that seam is an additive change to a shipped system, taken with its own assess-breakage pass when it lands. Until then, wiring an attribute to max health is not supported through any Attributes surface, and the absence is the boundary working rather than a hole in it.

The same rule holds generally: adapters answer questions other systems ask; they do not reach in and write. That is what keeps every adapter optional and deletable.

It does not own thresholds, ranks or named tiers

"Strength above 20 counts as Mighty" is a consumer's sentence, not a container's. Threshold semantics — above all hysteresis, the separate enter and exit boundaries that stop a value hovering on a line from firing every frame — belong to whatever consumes the value. A container that shipped tiers would hand every consumer one semantics and make the hovering bug everybody's default.

It does not name any attribute

No canonical strength, no level, no reputation. A shipped stat sheet is a policy statement about what kinds of characters every buyer's game has — and one shipped reputation attribute would pre-empt a social-standing design that deserves its own decisions. Even the Crafting adapter's level is only a default string on a component you configure.

It does not key by anything except the owner

An attribute is (owner, id). There is no (owner, context) axis — standing with a faction, reputation in a town — because a container that generalises to compound keys absorbs those whole domains as a side effect. Whether standing is ever a system is a real design question; it will not be answered by an accident of this container's shape.

It does not replace the seams that already ship

IAttackerDamageModifier and its siblings receive context a generic lookup cannot carry — the whole DamageContext, the status id and tags. An attribute stored as fire-resistance: 0.8 cannot answer "against this particular hit from this particular attacker". Attributes therefore feeds those seams through adapters and supersedes none of them; a project that wants contextual answers implements the contextual seam, with or without an attribute behind it.

It does not fire events for effective values

Computed reads have no "changed" moment the container can observe — providers are pull seams. If a HUD needs a live effective value, it re-reads at the moments the game changes something, exactly as every consumer of the framework's rule seams already does.

It does not configure an attribute it was not told about

Bounds are authored configuration. AttributeEntry carries an id, a base value and optional min/max together, and that list is the only place an attribute is configured — so an id created by SetBaseValue at runtime has no bounds, and no configured base value either. It is unconfigured in every respect rather than specially deprived of a range.

That is the boundary, not an omission: inventing a range for a value nobody configured would be the container deciding something your game owns. If a runtime id needs a range, author the id, or clamp in the code that decides what the value means — the second is the ordinary answer, and the recipes take it.

What the container will not do is let you find out in silence. Creating an id in a set that authors bounds on any row logs one warning naming that id, because that is the case where the inspector looks like it promised something. A set with no authored bounds stays quiet; it promised nothing.

It does not decide where per-item stat data lives

ItemDefinition has no stat fields, and this system adds none. The equipment-to-stat contract — whether item stats live on definitions, on a separate authored asset, or in ItemStack metadata — is a deliberately open decision recorded in the design findings, and it stays open until it is taken on purpose. An equipment provider you write today can read whatever source your project chooses.

It does not persist anything but base values

Not effective values, not contributions, not bounds. Every other fact is owned and persisted by whichever system grants it, and a save file where two participants write the same fact is a save file where one of them silently wins on load.

The shape of every boundary here

Attributes holds numbers and reports changes. The meaning of the numbers — their names, their stacking, their thresholds, their politics — belongs to the game that is being made with them.