Skip to content

Reactions

Reactions are still under active development. They are available for pack authors to try, but the feature is not fully stable yet; expect bugs and behaviour changes while it settles.

The Reactions system lets villagers respond to gestures, activities, surroundings, time, and nearby events with animations and optional sounds, particles, and relationship changes.

A reaction begins with:

{
"schema": "townstead:reaction/v2"
}

Files without schema use the legacy v1 format and remain supported.

A reaction answers four questions:

  1. What can start it? The entries in triggers.
  2. When is it allowed? The reaction and choice conditions.
  3. What can the villager perform? The entries in choices.
  4. How often can it happen? Chance, cooldown, and animation locking.

Pheno supplies conditions and follow-up actions. Reactions keeps its own responsibilities: event triggers, personality-weighted animation selection, movement locking, social mirroring, and relationship changes.

Reactions are loaded on the server from data packs. The currently available animation backend is Emotecraft, so clients need the corresponding emotes to see authored animations. See the Pheno reference for available when conditions and do actions.

Place reaction files in:

data/<namespace>/townstead/reactions/<path>.json

For example:

example_reactions/
├── pack.mcmeta
└── data/
└── example/
└── townstead/
└── reactions/
└── wave.json

This creates the reaction ID:

example:wave
{
"schema": "townstead:reaction/v2",
"display_name": "Wave",
"cooldown": "10s",
"triggers": [
{
"type": "gesture",
"emote": "emotecraft:waving"
}
],
"choices": [
{
"animation": {
"type": "emotecraft",
"id": "waving"
}
}
]
}

When a nearby player or villager waves, this reaction attempts to make the observing villager wave in return. A successful play places that reaction on a 200-tick cooldown for that villager.

When a trigger matches, Townstead:

  1. Rejects sleeping or animation-locked villagers.
  2. Checks the reaction cooldown.
  3. Rolls the reaction-level chance.
  4. Checks reaction-level conditions.
  5. Removes choices whose Pheno conditions, legacy tags, cooldowns, or chance rolls fail.
  6. Multiplies each remaining choice’s weight by its personality multiplier.
  7. Selects one choice by weighted random choice.
  8. Asks the animation backend to play it.
  9. Applies cooldowns, sound, particles, movement locking, hearts, and mirroring.
  10. Runs the selected choice’s Pheno do, followed by the reaction-level do.

Cooldowns are only claimed after an animation successfully starts. Failed attempts do not consume them.

Commands bypass sleeping and animation-lock checks, but other gates still apply.

FieldValuesDefaultDescription
display_nameStringHuman-readable name used by debugging tools.
tagsString array[]Descriptive reaction tags. Also used by nearby-reaction context tags.
schematownstead:reaction/v2RequiredSelects the current reaction authoring format.
cooldownDuration100tPer-villager cooldown for the whole reaction. Accepts ticks or values such as "10s".
chanceNumber or percentage1.0Chance that a matching trigger proceeds, such as 0.75 or "75%".
lockDuration0tFallback movement lock when the emote duration is unknown.
whenPheno conditionCondition that must pass before choices are considered.
doPheno action or arrayRuns after the selected animation starts successfully.
mirror_radiusInteger, 0+0Radius in which the played emote is broadcast as a gesture to other villagers.
mirror_chanceNumber, 0.0–1.00.0Chance for each nearby villager to receive the mirrored gesture.
heartsInteger0MCA relationship-heart change for the player who caused the reaction.
triggersObject array[]Events that can nominate this reaction.
choicesObject array[]Weighted performance candidates.
replacetrue, falsefalseReplaces lower-priority definitions instead of merging with them.

The current format also accepts the legacy aliases cooldown_ticks, lock_ticks, and bindings. Twenty ticks equal one second under normal server timing.

A reaction can contain any number of triggers. Multiple triggers are alternatives: any one of them can nominate the reaction.

Fires when the named emote plays near and roughly in front of a villager. This includes gestures from players and mirrored reactions from other villagers.

{
"type": "gesture",
"emote": "emotecraft:waving",
"max_distance": 6.0,
"min_dot": 0.6
}
FieldValuesDefaultDescription
emoteEmote reference or bare nameRequiredEmote to observe. Matching is case-insensitive and ignores the backend prefix.
max_distanceNumber6.0Parsed for the trigger, but not currently enforced. Gesture broadcasts currently use the broadcaster radius.
min_dotNumber0.6Parsed for the trigger, but not currently enforced.

Nominates the reaction when any listed context tag newly becomes present.

{
"type": "context_enter",
"tags": ["near_player_friend", "morning"]
}

Use this for transitions such as entering a building, approaching music, or becoming hungry. It does not continuously repeat while the context remains unchanged.

The trigger’s tags field is used to find candidate reactions. Put tags that must all be present in reaction-level conditions.required_tags, choice-level required_tags, or Pheno when.

Nominates the reaction during context scans when any listed context tag is present.

{
"type": "context_present",
"tags": ["near_music"]
}

Use cooldown to control how often the reaction can repeat. This is appropriate for ongoing situations such as dancing near music. Put tags that must all be present in reaction-level conditions.required_tags, choice-level required_tags, or Pheno when.

Fires when a Townstead villager task reports a named lifecycle phase.

{
"type": "task",
"task": "townstead:example_task",
"phase": "start"
}

Both values must exactly match the task event after case normalisation. Phases are task-defined and may include values such as start, transition:<state>, or stop:<result>.

Fires when a villager idles near a Townstead idle-spot point of interest.

{
"type": "idle_spot",
"spot": "example:bench"
}

The spot value must match the ID supplied by the idle-spot system.

Checks villagers at an interval while the named time phase is active.

{
"type": "time",
"phase": "evening",
"interval_ticks": 1200
}
FieldValuesDefaultDescription
phaseTime phaseRequiredPhase to match.
interval_ticksInteger, 20+1200Parsed, but not currently enforced. Use reaction cooldown to control repetition.

Common phases are day, night, dawn, and dusk.

Use when on the whole reaction:

{
"when": {
"type": "pheno:environment",
"time": "day",
"weather": "clear"
}
}

Or on an individual choice:

{
"when": {
"type": "pheno:health",
"min": 10
}
}

Reaction-level when is tested first. Choice-level when removes only that choice from weighted selection.

When a player gesture caused the reaction, Pheno receives the villager as self and the player as other. Other trigger sources normally have no counterpart.

The legacy conditions and choice required_tags fields remain available for compatibility. New packs should prefer Pheno when whenever the condition can be expressed through Pheno.

{
"conditions": {
"required_tags": ["near_player_friend", "under_open_sky"],
"time": "morning",
"weather": "clear"
}
}
FieldDescription
required_tagsEvery listed context tag must be present.
timeReserved time-phase condition. Parsed and merged, but not currently enforced.
weatherReserved weather condition. Parsed and merged, but not currently enforced.

Use required_tags for working time and weather gates:

{
"conditions": {
"required_tags": ["morning", "raining"]
}
}

Clear weather does not currently emit a clear tag.

Gesture, task, idle-spot, time, and command contexts do not currently carry the full context-tag set. required_tags conditions are therefore most useful with context_enter and context_present triggers.

Context tags are resolved around each villager during periodic scans.

TagsMeaning
hungry, peckishHunger is below Townstead’s emergency or adequate threshold.
thirsty, parchedHydration is below Townstead’s emergency or adequate threshold.
exhausted, drowsy, tiredFatigue has crossed the corresponding threshold.
unemployedThe villager has no profession.
at_workstationThe villager is near their workstation.
pregnantMCA reports the villager as pregnant.
aloneNo other villagers are nearby.
in_crowdAt least three other villagers are nearby.
near_baby_villagerA baby or toddler villager is nearby.
TagsMeaning
is_married, is_engaged, is_promisedThe villager has the corresponding MCA relationship state.
near_spouse, near_parent, near_familyA corresponding family member is nearby.
near_working_villagerA nearby villager is working.
near_resting_villagerA nearby villager is resting.
near_meeting_villagerA nearby villager is meeting.
TagsMeaning
near_player_friendA nearby player has at least 30 hearts with the villager.
near_player_strangerA nearby player’s hearts are between friend and disliked thresholds.
near_player_dislikedA nearby player has at most -10 hearts.
near_player_spouseThe villager’s player spouse is nearby.
being_watched_by_playerA nearby player is looking towards the villager.
player_holding:<item>A nearby player holds the named item path, such as player_holding:diamond.
player_holding_tag:<namespace>:<tag>A nearby player’s held item belongs to the named item tag.
TagsMeaning
near_zombie_villagerA zombie villager is nearby.
near_mob_threatA hostile mob is nearby.
outsider_presentAn illager is nearby.
raid_activeA raid is active at the villager’s position.
near_graveAn MCA tombstone is nearby.
in_dialogue_with_playerThe MCA dialogue screen is open for this villager.
dialogue_just_endedDialogue with the villager ended roughly within the last three seconds.
heart_increasedThe villager’s MCA relationship hearts with the interacting player went up recently.
heart_decreasedThe villager’s MCA relationship hearts with the interacting player went down recently.
Tag patternMeaning
near_reacting:<namespace>:<reaction>A nearby villager is currently locked by that exact reaction.
near_reacting_tag:<tag>A nearby villager is currently performing a reaction carrying that reaction tag.

For example, a reaction tagged dancing causes nearby villagers to receive near_reacting_tag:dancing while its lock is active.

Bindings with allow_movement: true do not create a reaction lock, so they do not contribute these nearby-reaction tags.

TagsMeaning
day, night, dawn, duskBroad Minecraft time phases.
early_morning, morning, noon, afternoon, evening, late_nightDisplay-hour periods.
hour:<0–23>Current display hour.
day:<number>Current world-day number.
on_shift:<name>Current named Townstead shift.
shift_customThe villager is using a custom shift.
TagsMeaning
raining, thunderingCurrent level weather.
under_open_sky, under_roofWhether the villager can see the sky.
in_darkBoth block and sky light are below 4.
near_water, near_lavaThe corresponding fluid is nearby.
biome:<namespace>:<biome>Current biome ID.
freezing, cold, temperate, hotCurrent biome temperature band.
dimension:<namespace>:<dimension>Current dimension ID.
in_building:<type>MCA building type containing the villager. The namespace is removed.
near_musicA registered music source, including a playing jukebox, is nearby.

A choice is one possible animation and its local rules.

{
"animation": {
"type": "emotecraft",
"id": "waving",
"allow_movement": true
},
"weight": 2.0,
"chance": "100%",
"personality_weights": {
"friendly": 2.0,
"introverted": 0.4,
"default": 1.0
}
}
FieldValuesDefaultDescription
animationObjectRequiredAnimation backend, ID or IDs, and playback settings.
weightNumber, 0+1.0Relative selection weight.
chanceNumber or percentage1.0Independent eligibility roll before weighted selection.
shotsInteger, 1+1Number of animation repetitions used when calculating the lock duration.
cooldownDuration0tPer-villager cooldown for this choice.
allow_movementtrue, falsefalseAllows vanilla movement to continue and prevents reaction locking.
parts_skipString arrayBody parts omitted from the animation.
personality_weightsObject{}Multipliers applied to this choice’s weight.
whenPheno conditionCondition required for this choice.
doPheno action or arrayRuns after this choice’s animation starts.
required_tagsString array[]Legacy context tags required for this choice.
soundObjectSound played after the animation starts.
particlesObjectParticle burst emitted after the animation starts.
speech_poolStringReserved speech-pool reference. Parsed but not currently emitted.

If allow_movement is true and parts_skip is omitted, Townstead defaults it to:

["legs", "torso"]

Legacy ref and args fields remain accepted. The legacy cooldown_ticks alias remains accepted as well.

{
"animation": {
"type": "emotecraft",
"ids": [
"club_penguin_dance",
"kazotsky_kick"
],
"shots": 2,
"speed": 1.0,
"allow_movement": false
}
}
FieldDefaultDescription
typeRequiredAnimation backend. Currently emotecraft.
idOne animation ID without the backend prefix.
idsAnimation ID array. One is chosen uniformly after the choice wins weighted selection.
shots1Repetitions used when calculating animation lock duration.
speed1.0Positive playback-speed multiplier.
loop_override-1Backend loop override.
allow_movementfalseAllows movement and suppresses reaction locking.
parts_skipBody-part names omitted from playback.

The final selection weight is:

choice weight × personality multiplier

Keys are matched case-insensitively against the MCA personality name. default is used when no exact key exists, and an omitted default acts as 1.0.

A multiplier of 0 makes that choice unavailable to the personality.

Emote names are normalised to lowercase for playback.

do accepts one Pheno action or an array of actions:

{
"do": [
{
"type": "pheno:spawn_particles",
"particle": "minecraft:happy_villager",
"count": 3
},
{
"type": "pheno:play_sound",
"sound": "minecraft:entity.villager.yes"
}
]
}

Choice actions run before the reaction-level action. Neither runs when animation playback fails.

The legacy sound and particles choice fields remain supported, particularly for MCA gendered voice suffixes, but general-purpose effects should use Pheno.

Use a direct sound event:

{
"sound": {
"id": "minecraft:entity.villager.yes",
"volume": 0.8,
"pitch": 1.0
}
}

Or an MCA gendered voice suffix:

{
"sound": {
"mca_voice": "greet",
"volume": 0.8,
"pitch_range": [0.95, 1.05]
}
}
FieldDefaultDescription
idSound-event ID.
mca_voiceMCA voice suffix resolved as mca:villager.<gender>.<suffix>.
volume1.0Non-negative playback volume.
pitch1.0Fixed pitch.
pitch_rangeTwo-number minimum and maximum pitch range.

At least id or mca_voice is required. When both are present, MCA villagers use mca_voice; id is the fallback for entities without MCA gender data. MCA voice pitch is also adjusted by the individual villager’s voice tone.

{
"particles": {
"id": "minecraft:note",
"count": 6,
"spread": [0.5, 1.0, 0.5],
"y_offset": 2.0
}
}
FieldDefaultDescription
idRequiredSimple particle type ID. Particle types requiring extra parameters are not supported here.
count1Number of particles.
spread[0.2, 0.2, 0.2]X, Y, and Z Gaussian spread in blocks.
y_offset1.0Height above the villager’s feet.

Unless allow_movement is true, a successful reaction can temporarily lock the villager against other reactions.

For known Emotecraft animations, Townstead calculates the lock from the selected emote’s duration and shots. If the duration is unknown, it uses the reaction’s lock. If neither supplies a duration, no lock is applied.

The lock is also what makes a reaction visible through near_reacting:* context tags.

Mirroring lets a reaction spread socially:

{
"mirror_radius": 8,
"mirror_chance": 0.5
}

After a successful animation, each nearby villager has mirror_chance probability of receiving a synthetic gesture event for the played emote. Any reaction with a matching gesture trigger may respond.

Mirroring is limited to one hop. A reaction caused by a mirrored gesture cannot mirror again.

{
"hearts": 1
}

When a player gesture causes the reaction, Townstead can add or remove MCA relationship hearts between that player and the reacting villager.

The same player, villager, and reaction combination can change hearts at most once per Minecraft day. Context, task, time, and mirrored triggers have no player cause and therefore do not change hearts.

Townstead reads every resource-pack contribution to the same reaction ID rather than only the highest-priority file.

Without replace: true:

  • tags, choices, and triggers are concatenated and deduplicated.
  • Condition required_tags are combined.
  • Higher-priority scalar values replace lower-priority values.
  • Higher-priority conditions.time and conditions.weather replace lower values when present.
  • A higher-priority hearts value of 0 leaves the lower non-zero value intact.

Use:

{
"schema": "townstead:reaction/v2",
"replace": true,
"choices": [
{
"animation": {
"type": "emotecraft",
"id": "waving"
}
}
]
}

when the higher-priority file should completely replace the earlier definition.

This merging model allows one pack to add triggers while another adds animations, but the final merged reaction must contain at least one usable choice.

{
"schema": "townstead:reaction/v2",
"display_name": "Friendly Wave",
"tags": ["social", "greeting"],
"cooldown": "10s",
"chance": "80%",
"mirror_radius": 6,
"mirror_chance": 0.25,
"hearts": 1,
"triggers": [
{
"type": "gesture",
"emote": "emotecraft:waving",
"max_distance": 6,
"min_dot": 0.6
}
],
"when": {
"type": "pheno:environment",
"weather": "clear"
},
"choices": [
{
"animation": {
"type": "emotecraft",
"id": "waving",
"allow_movement": true
},
"weight": 3,
"personality_weights": {
"friendly": 2.0,
"extroverted": 2.0,
"introverted": 0.3,
"default": 1.0
},
"sound": {
"mca_voice": "greet",
"volume": 0.8,
"pitch_range": [0.95, 1.05]
},
"do": {
"type": "pheno:spawn_particles",
"particle": "minecraft:happy_villager",
"count": 3,
"spread": 0.3
}
}
]
}

List loaded reactions:

/townstead reaction list

Play a reaction on the MCA villager under the player’s crosshair, or the nearest one within 16 blocks:

/townstead reaction play "example:friendly_wave"

Play it on an explicit target:

/townstead reaction play "example:friendly_wave" @e[type=mca:male_villager,limit=1,sort=nearest]

The command reports a failure when the reaction is unknown, on cooldown, rejected by chance, or has no eligible choice.

  • Confirm the file is under data/<namespace>/townstead/reactions/.
  • Confirm the JSON is valid.
  • Check the server log for a rejected contribution.
  • Reload data packs after changing the file.
  • Confirm at least one choice has a valid animation.id, animation.ids, or legacy ref.
  • Confirm the backend prefix is emotecraft.
  • Confirm all animation references in one choice use the same backend.
  • Check reaction and choice cooldowns.
  • Check both chance fields.
  • Check personality multipliers and required context tags.
  • Confirm the emote exists on the client.
  • Use context_enter for the transition into a state.
  • Use context_present for repetition while a state remains true.
  • Put all required tags in the trigger’s tags as well as any stricter reaction or choice gate.
  • Remember that cooldown controls repetition for context_present.
  • Set both mirror_radius and mirror_chance above zero.
  • Give the receiving reaction a gesture trigger matching the actual played emote.
  • Remember that mirroring stops after one hop.
  • Reactions are server-authored data-pack content.
  • Emotecraft is currently the only registered animation backend.
  • speech_pool is reserved but not currently played.
  • Context tags are resolved periodically rather than every tick.
  • Trigger and context vocabularies are code-defined and may expand as more Townstead systems expose events.