Zones¶
A zone is a charged area that grows around the user, holds for a while, and applies its effects to what is inside. Two classes share one parent:
| Class | For |
|---|---|
ZoneAbility |
a sphere that applies effects to entities inside it |
SpreadingBlockAbility |
a zone that also converts the ground, and restores it at the end |
DomainAbility |
their shared parent: the zone sounds and the "entity left the zone" tracking |
ZoneAbility¶
Lifecycle¶
- First press: the user must be on the ground (unless
onGroundOnlyisfalse). A sphere is created, the charge starts, and the user is rooted forchargeTimeticks. - During the charge: the sphere grows. Pressing again releases the zone early, at the size it has reached, but only once
minChargeTimeticks have passed. - Charge end: the zone holds for
zoneTimeticks, applying its effects every effect interval. - Pressing during the zone closes it.
- Close: effects listed for clearing are removed, the closing sound plays, and the cooldown starts in proportion to the size reached:
maxCooldown x (size / maxZoneSize), clamped betweenminCooldownandmaxCooldown.
A minimal zone¶
public class MyZoneAbility extends ZoneAbility {
public MyZoneAbility(AbilityCore<MyZoneAbility> core) {
super(core);
this.maxZoneSize = 20;
this.chargeTime = 100;
this.zoneTime = 400.0F;
this.zoneColor = new Color(80, 160, 255, 75);
this.setEffectsTickInterval(20);
}
@Override
protected void applyEffectToLivingEntityInZone(LivingEntity owner, LivingEntity target) {
this.applyOrRefresh(target, MobEffects.MOVEMENT_SLOWDOWN, 40, 1, false, true);
}
@Override
protected List<MobEffect> getZoneEffectsToClearOnEnd() {
return List.of(MobEffects.MOVEMENT_SLOWDOWN);
}
}
Settings¶
Set these from your constructor.
| Field | Default | Meaning |
|---|---|---|
maxZoneSize |
128 |
radius at a full charge |
minZoneSize |
0 |
smallest radius |
chargeTime |
240 |
ticks to reach maxZoneSize |
minChargeTime |
60 |
ticks before an early release is accepted |
zoneTime |
1200 |
ticks the zone holds after the charge |
minCooldown, maxCooldown |
2400 |
bounds of the proportional cooldown |
cancelCooldown |
10 |
length of the closing animation when the user closes the zone |
zoneColor |
white, alpha 50 |
the sphere's colour |
onGroundOnly |
true |
refuse the first press in mid-air |
applyEffectOnUser |
false |
include the user in the zone's targets |
allowAnimation |
true |
play the charging pose |
enableBlockOverlay |
false |
highlight the blocks inside the zone on clients |
overlayArgb |
0x50FFFFFF |
the overlay colour |
targetsCheck |
enemies | which living entities are affected |
Change the effect rate with setEffectsTickInterval(ticks): it is safe to call from the constructor.
Always give zoneColor an alpha
The colour goes to the sphere as is. new Color(r, g, b) has an alpha of 255, which paints an opaque dome nobody can see out of. Use the four-argument constructor: around 50 for a saturated colour, 75 for a mid tone, 110 to 120 for a dark one.
Hooks¶
| Override | Called |
|---|---|
applyEffectToLivingEntityInZone(owner, target) |
for each targeted living entity inside, every effect interval |
onEntityLeavesZone(owner, target) |
once, when an entity that was affected is no longer inside |
getZoneEffectsToClearOnEnd() |
at close, the effects to strip from everything that was affected |
endChargeBlocks, continuityTickBlocks, continuityEndBlocks |
block work at charge end, per tick, and at close |
applyEffectToNonLivingEntityInZone(owner, target) |
for items, arrows, orbs... only if affectsNonLivingEntities() returns true |
The non-living hook needs its switch: overriding applyEffectToNonLivingEntityInZone alone does nothing, because the scan never hands it a target. Override affectsNonLivingEntities() to return true as well. It widens every scan to every entity in the zone's box, so only do it when the zone really acts on them.
Useful helpers: applyOrRefresh(target, effect, duration, amplifier, ambient, particles), isEntityInZone(entity), isPositionInZone(pos), getZoneSize(), getCenterBlock().
Zone damage is an unguarded pulse
applyEffectToLivingEntityInZone runs on every effect interval, for every target. Deal damage there with hurtTarget, not hurtBurst, unless the interval is longer than 10 ticks. See Damage.
Interrupting a charge from outside¶
DomainAbility.interruptCharge(entity) cancels a zone's charge as if it had never started, for an ability that breaks
the caster's concentration. It returns whether a charge was running; nothing is put on cooldown.
AkumaLib calls it itself for every AbilityHelper.emergencyStopAbility - kairoseki, water, handcuffs, any
disableAbilities caller - through AkumaCharges.interrupt, which is also the call to make for an interruption of
your own when the ability may not be a domain. See Charges.
| Class | What it takes back |
|---|---|
ZoneAbility |
the sphere spawned at charge start, the charge's MOVEMENT_BLOCKED and pose. A closing zone (the owner shutting it) is left alone |
SpreadingBlockAbility |
every block the charge had already spread, through the same restore queue as a normal close (over restoreTicks), the lock and the pose |
any other DomainAbility |
nothing: the default returns false |
Neither charge-component call is safe on a domain
ChargeComponent.stopCharging runs the charge-end event, and a domain opens its zone there, at the size reached.
forceStopCharging skips the event but leaves what the charge put in the world: a sphere that is never discarded,
or blocks with no continuity left to restore them. Call AkumaCharges.interrupt, which picks interruptCharge
for a domain.
Why the spreading restore is queued
It used to be restoreImmediately, which at a large radius is tens of thousands of setBlock calls in one tick.
The zone stays in ACTIVE_ZONES until the queue is drained, so a disconnect in between still restores everything,
and a recast flushes whatever is left first.
Zone sounds: ZoneSoundSet¶
DomainAbility plays four sounds over a zone's life. Declare them as data from your constructor. Leaving setZoneSounds unset plays the Ope Ope no Mi's Room sounds: a deliberate fallback, not a default to ship with.
this.setZoneSounds(ZoneSoundSet.of(MySounds.ZONE_CREATE, MySounds.ZONE_CHARGE,
MySounds.ZONE_EXPAND, MySounds.ZONE_END)
.pitches(0.9F, 0.85F, 1.0F, 0.9F)
.jitters(0.0F, 0.2F, 0.0F, 0.0F));
| Slot | When | Default volume |
|---|---|---|
| create | the charge starts | 3.0 |
| interval | repeatedly while the zone charges | 1.5 |
| charge end | the zone reaches full size | 3.0 |
| end | the zone closes | 2.0 |
Pass the RegistryObjects directly, without .get(). .volumes(...), .pitches(...) and .jitters(...) take one value per slot, in that order. Give the interval slot a pitch jitter of about 0.2: a charge sound repeated for hundreds of ticks turns robotic, and the jitter keeps it natural.
Declare the interval clip's length
The interval slot is replayed every 18 ticks by default - the pace of the base mod's short Room sound. A longer clip restarted that often plays two or three copies of itself over each other for the whole charge. Give the set the clip's length and it replays it only once a play has finished:
this.setZoneSounds(ZoneSoundSet.of(MySounds.ZONE_CREATE, MySounds.ZONE_CHARGE,
MySounds.ZONE_EXPAND, MySounds.ZONE_END)
.pitches(1.0F, 0.8F, 1.0F, 1.0F)
.intervalClip(3.0F)); // seconds, at pitch 1
The cadence is worked out at the slot's base pitch, its slowest, since jitter only raises the pitch and a lower pitch plays a clip for longer: a 3 s clip at pitch 0.8 lasts 3.75 s, so it is replayed every 75 ticks. setZoneSounds sets playSoundInterval from it; assign that field yourself only before the call, or not at all.
A clip replayed back to back also wants a short fade at both ends, or each restart clicks.
The zone that also converts the ground has its own page: Spreading blocks.