Architecture Overview¶
This page illustrates how Civillis decides whether a hostile mob is allowed to spawn, and how the major systems — civilization scoring, decay, and monster heads — interact.
Spawn Decision Workflow¶
Every time Minecraft's natural spawn cycle tries to place a hostile mob, Civillis intercepts and runs the following decision pipeline:
flowchart TD
SpawnAttempt["Hostile Mob Spawn Attempt"]
NaturalCheck{Natural spawn?}
HeadQuery["Query nearby skulls"]
HasNearby{Skulls in local zone?}
HeadNearby["HEAD_NEARBY: Allow spawn"]
ConvertCheck{3+ skulls?}
ConvertMob["Roll conversion"]
HeadAttract{Distant skull attraction?}
HeadSuppress["HEAD_SUPPRESS: Block spawn"]
ScoreQuery["Query civilization score"]
ApplyDecay["Apply decay factor"]
ThresholdCheck{Score vs thresholds}
Allow["Allow spawn"]
Probabilistic["Probabilistic block"]
Block["Block spawn"]
SpawnAttempt --> NaturalCheck
NaturalCheck -->|No: spawner / egg / summon| Allow
NaturalCheck -->|Yes| HeadQuery
HeadQuery --> HasNearby
HasNearby -->|Yes| HeadNearby
HeadNearby --> ConvertCheck
ConvertCheck -->|Yes| ConvertMob
ConvertCheck -->|No| Allow
ConvertMob --> Allow
HasNearby -->|No| HeadAttract
HeadAttract -->|Suppressed by distant skulls| HeadSuppress
HeadAttract -->|Not suppressed| ScoreQuery
ScoreQuery --> ApplyDecay
ApplyDecay --> ThresholdCheck
ThresholdCheck -->|Below low threshold| Allow
ThresholdCheck -->|Between thresholds| Probabilistic
ThresholdCheck -->|Above mid threshold| Block
Non-natural spawns (spawn eggs, spawners, /summon, reinforcements) bypass the pipeline entirely and always succeed.
Civilization Score¶
The score represents how "civilized" an area is. It is computed once per detection area and then kept up to date incrementally:
flowchart TD
Blocks["Recognized blocks in world"]
Palette{"Target blocks in chunk palette?"}
SkipChunk["Skip: score = 0"]
Weight["Scan section, look up weight per block"]
VCScore["Per-voxel-chunk score (capped at 1.0)"]
Aggregate["Distance-weighted aggregation<br/>across all chunks in detection range"]
Score["Cached civilization score"]
Change["Block placed or broken"]
Recompute["Recompute affected voxel chunk"]
Delta["Delta = new score - old score"]
DeltaZero{Delta = 0?}
Propagate["Apply delta × distance coefficient<br/>to each overlapping cached result"]
Blocks --> Palette
Palette -->|No| SkipChunk --> VCScore
Palette -->|Yes| Weight --> VCScore
VCScore --> Aggregate --> Score
Change --> Recompute --> Delta --> DeltaZero
DeltaZero -->|Yes| Score
DeltaZero -->|No| Propagate --> Score
- Each 16³ voxel chunk scores the weighted sum of recognized blocks inside it, capped at 1.0
- The detection range (default 240×240×48 blocks) defines how many chunks are aggregated
- After the initial computation, block changes only recompute the single affected chunk and propagate the difference — the cached score is never fully recomputed
Decay Integration¶
Civilization protection is not permanent. The decay system modulates the outer-zone contribution of the civilization score based on player presence:
flowchart TD
ScoreComputed["Raw score computed"]
SplitZones["Split: core zone + outer zone"]
CoreScore["Core: always full strength"]
OuterScore["Outer: modulated by decay"]
PresenceCheck["When was this area last visited?"]
GracePeriod{Within grace period?}
FullOuter["Outer: full strength"]
DecayApply["Outer: exponential decay applied"]
FloorCheck["Clamped to decay floor"]
CombinedScore["Effective score = core + decayed outer"]
ScoreComputed --> SplitZones
SplitZones --> CoreScore
SplitZones --> OuterScore
OuterScore --> PresenceCheck
PresenceCheck --> GracePeriod
GracePeriod -->|Yes| FullOuter
GracePeriod -->|No| DecayApply
DecayApply --> FloorCheck
FloorCheck --> CombinedScore
FullOuter --> CombinedScore
CoreScore --> CombinedScore
When a player returns, their presence gradually advances the recorded visit time, restoring the outer zone contribution step by step.
Monster Head Interaction¶
Monster heads operate on a separate pathway that runs before the civilization score is even consulted:
flowchart TD
SpawnPos["Spawn position"]
ScanHeads["Query indexed skull buckets near spawn"]
DimFilter["Filter by dimension whitelist"]
LocalZone{Skulls within local zone?}
AllowSpawn["HEAD_NEARBY: Allow spawn"]
CountHeads{"3+ skulls clustered?"}
NoConvert["Spawn original mob"]
RollConvert["Roll conversion probability"]
ConvertSuccess{Roll succeeds?}
SpawnConverted["Spawn as converted type"]
SpawnOriginal["Spawn original mob"]
DistantCalc["Evaluate heads within attraction window"]
SuppressRoll{Suppression roll succeeds?}
BlockSpawn["HEAD_SUPPRESS: Block spawn"]
ContinueToCiv["Continue to civilization scoring"]
SpawnPos --> ScanHeads --> DimFilter --> LocalZone
LocalZone -->|Yes| AllowSpawn --> CountHeads
CountHeads -->|No| NoConvert
CountHeads -->|Yes| RollConvert --> ConvertSuccess
ConvertSuccess -->|Yes| SpawnConverted
ConvertSuccess -->|No| SpawnOriginal
LocalZone -->|No| DistantCalc --> SuppressRoll
SuppressRoll -->|Yes| BlockSpawn
SuppressRoll -->|No| ContinueToCiv
- Skulls restricted to specific dimensions (e.g., wither skeleton skulls → Nether only) are filtered out before any mechanism activates
- Conversion probability scales with skull count; converted mobs bypass this pipeline on their own spawn to prevent recursion
- Distant suppression is range-bounded and index-backed: only heads in the local attraction window are considered
Mob Flee Behavior Layer¶
Mob Flee AI runs as a post-spawn behavior layer for existing hostile mobs. It does not replace spawn gating; it complements it.
- Spawn gating decides whether a new hostile mob is allowed to appear
- Mob Flee AI decides whether an already-existing hostile mob should retreat from civilization pressure
In dense city cores, flee logic may escalate to panic-like retreat behavior (including possible combat disengagement), while outer civilized zones usually produce softer outward drift.
flowchart TD
Tick["Periodic evaluation per mob"]
Enabled{"Mob Flee AI enabled?"}
HeadZone{"Inside local head force-allow zone?"}
Score["Read local civilization score"]
GreenLine["greenLine = spawnThresholdMid"]
AboveGreen{score >= greenLine?}
CombatLine["combatLine = greenLine + (1-greenLine) * combatFleeRatio"]
ModeCheck{"Current state"}
IdleBranch["IDLE mode (no target)"]
CombatBranch["COMBAT_PANIC mode (has target)"]
IdleZone{score < combatLine?}
IdleProb["P_idle = (score - greenLine) / (combatLine - greenLine)"]
IdleRoll{"Roll < P_idle ?"}
IdleForce["score >= combatLine -> guaranteed idle flee start"]
CombatGate{score >= combatLine?}
CombatProb["P_panic = ((score - combatLine)/(1-combatLine)) * (1-greenLine)"]
CombatRoll{"Roll < P_panic ?"}
FindTargetIdle["Choose flee target (idle)<br/>1) nearby head-zone target<br/>2) 8-direction gradient to lower score"]
FindTargetPanic["Choose flee target (panic)<br/>1) nearby head-zone target<br/>2) 8-direction gradient to lower score"]
StartIdle["Start IDLE flee"]
StartPanic["Start COMBAT panic<br/>clear target, panic burst"]
ContinueIdle["Continue normal AI (idle branch)"]
ContinueCombat["Continue normal AI (combat branch)"]
Continue["Continue normal AI"]
Tick --> Enabled
Enabled -->|No| Continue
Enabled -->|Yes| HeadZone
HeadZone -->|Yes| Continue
HeadZone -->|No| Score --> GreenLine --> AboveGreen
AboveGreen -->|No| Continue
AboveGreen -->|Yes| CombatLine --> ModeCheck
ModeCheck -->|No attack target| IdleBranch --> IdleZone
IdleZone -->|Yes| IdleProb --> IdleRoll
IdleRoll -->|No| ContinueIdle
IdleRoll -->|Yes| FindTargetIdle --> StartIdle
IdleZone -->|No| IdleForce --> FindTargetIdle
ModeCheck -->|Has attack target| CombatBranch --> CombatGate
CombatGate -->|No| ContinueCombat
CombatGate -->|Yes| CombatProb --> CombatRoll
CombatRoll -->|No| ContinueCombat
CombatRoll -->|Yes| FindTargetPanic --> StartPanic
ContinueIdle --> Continue
ContinueCombat --> Continue
Registry Loading & Injection¶
The mod's behavior is entirely data-driven. Two JSON registries are loaded at startup (and on /reload) and injected into the runtime systems:
flowchart TD
subgraph Load [Datapack Loading]
BW["civil_blocks/*.json"]
HT["civil_heads/*.json"]
Merge["Merge & resolve overrides<br/>(later-loaded datapacks override earlier ones)"]
BW --> Merge
HT --> Merge
end
Merge -->|block weights| ScoreEngine["Civilization Score Engine"]
Merge -->|head type definitions| HeadTracker["Head Tracker"]
ScoreEngine -->|"O(1) score lookup"| SpawnDecision["Spawn Decision"]
HeadTracker -->|"proximity + conversion"| SpawnDecision
SpawnDecision --> Outcome["allow / block / convert"]
- Block weight registry determines which blocks are recognized and how much each contributes to civilization score
- Head type registry determines which skull types are active, their dimension restrictions, and whether they participate in conversion
- Both registries support full datapack override: modpacks can add, modify, or replace entries without touching mod code