Skip to content

Interactables System

Interactables are invisible entities that players can interact with by clicking. They execute custom MoLang scripts on interaction and can have per-player conditional visibility, making them perfect for invisible triggers, quest interactions, and dynamic world content.

Interactables provide:

  • Invisible Hitboxes: Customizable width and height for interaction zones
  • Click Actions: Different scripts for right-click (interact) and left-click (attack)
  • Per-Player Visibility: MoLang-based conditional visibility per player
  • MoLang Integration: Full access to Journey’s scripting capabilities
  • Persistent: Save with world data across server restarts

Common Use Cases:

  • Quest trigger zones that activate on click
  • Invisible NPCs or objects for story events
  • Conditional doors/portals requiring items or progress
  • Hidden collectibles that disappear after collection
  • Multi-stage puzzles using flags

Interactables are defined in JSON files located in config/journey/interactables/

{
"id": "journey:test",
"right_click_script": "q.player.tell('right click!');",
"left_click_script": [
"q.player.tell('left click!');",
"q.set_interaction_result('consume');"
],
"visibility_script": "q.player.has_flag('vis_test')",
"width": 1.25,
"height": 1.0
}
FieldTypeRequiredDefaultDescription
idStringYes-Unique identifier (e.g., "journey:quest_trigger")
right_click_scriptString/ArrayNo""MoLang script(s) executed on right-click
left_click_scriptString/ArrayNo""MoLang script(s) executed on left-click (attack)
visibility_scriptString/ArrayNo""MoLang expression determining player visibility (boolean)
widthFloatNo1.0Interaction hitbox width in blocks
heightFloatNo1.0Interaction hitbox height in blocks

Scripts can be single strings or arrays of strings. Each statement should end with a semicolon.

{
"right_click_script": "q.player.tell_minimessage('<green>Quest started!');"
}
{
"right_click_script": [
"q.player.tell_minimessage('<green>Quest started!');",
"q.player.start_task('journey:main_quest');",
"q.player.add_flag('quest_started');",
"q.set_interaction_result('success');"
]
}

Control how interactions behave using q.set_interaction_result():

q.set_interaction_result('consume') // Prevent item use
q.set_interaction_result('success') // Standard success
q.set_interaction_result('fail') // Fail interaction
q.set_interaction_result('pass') // Allow other interactions

Available Results:

  • "success" - Standard success
  • "success_no_item_used" - Success without consuming item
  • "consume" - Consume interaction (prevents item use)
  • "consume_partial" - Partial consumption
  • "pass" - Pass through to other interactions
  • "fail" - Fail the interaction

Available in all interactable scripts via q.player.*:

Messaging:

q.player.tell_minimessage(message) // Send formatted message
q.player.username // Get player username

Flags & State:

q.player.has_flag(flag) // Check if player has flag (returns 1.0/0.0)
q.player.add_flag(flag) // Add flag to player
q.player.remove_flag(flag) // Remove flag from player

Tasks/Quests:

q.player.has_completed_task(taskName) // Check task completion
q.player.has_completed_subtask(taskName, subtaskName) // Check subtask
q.player.start_task(taskName) // Start a task

Items:

q.player.has_item(itemId, count) // Check if player has items
q.player.give_item(itemId, count) // Give items
q.player.remove_item(itemId, count) // Remove items

Pokémon:

q.player.has_party_pokemon_matching(query) // Check for matching Pokémon
q.player.remove_party_pokemon(index) // Remove Pokémon at slot
q.player.party_slot_for_pokemon(query) // Get party slot index

Levelables:

q.player.has_levelable(name) // Check if player has levelable
q.player.give_levelable(name) // Give levelable
q.player.levelable_level(name) // Get current level
q.player.levelable_experience(name) // Get current XP
q.player.progress_levelable(name, amount) // Add XP
q.player.remove_levelable(name) // Remove levelable

Commands & Effects:

q.player.execute_command(command) // Execute server command
q.player.launch_timeline(timelineName) // Start timeline
q.player.snowstorm_particle(particle, x, y, z) // Spawn particle effect
q.player.push(x, y, z, force) // Push player

Zones:

q.player.is_in_zone(zoneUUID) // Check if player in zone

Paths:

q.player.toggle_path_visualization() // Toggle path preview
q.player.show_path_visualization(show) // Set path preview state
q.player.preview_path(pathId) // Preview a path

Available in left_click_script only:

q.damage_source() // Returns damage source type as string
q.damage_amount() // Returns damage amount

The visibility_script controls who can see and interact with the interactable:

{
"visibility_script": "q.player.has_flag('unlocked_area')"
}

How it Works:

  • Evaluated per-player periodically
  • Returns true (1.0) = player can see it
  • Returns false (0.0) = hidden from player
  • Cached for performance - updates only when state changes

Visible only to players without a flag:

{
"visibility_script": "!q.player.has_flag('collected_coin')"
}

Visible only to players who completed a task:

{
"visibility_script": "q.player.has_completed_task('journey:tutorial')"
}

Visible only to high-level players:

{
"visibility_script": "q.player.levelable_level('Combat') >= 25.0"
}

Visible only with specific item:

{
"visibility_script": "q.player.has_item('minecraft:diamond', 1)"
}

Always visible:

{
"visibility_script": "1.0 == 1.0"
}

Customize the interaction area with width and height:

{
"width": 2.0, // 2 blocks wide
"height": 3.0 // 3 blocks tall
}

Common Sizes:

  • Default (1x1): Standard button/trigger
  • Small (0.5x0.5): Collectible, precise trigger
  • Large (2x3): Door, portal, large interaction zone
  • Wide (3x1): Horizontal trigger area

File: config/journey/interactables/quest_starter.json

{
"id": "journey:quest_starter",
"right_click_script": [
"q.player.tell_minimessage('<green>Quest started!');",
"q.player.start_task('journey:main_quest');",
"q.player.add_flag('quest_started');",
"q.set_interaction_result('success');"
],
"visibility_script": "!q.player.has_flag('quest_started')",
"width": 1.0,
"height": 2.0
}

Usage: Invisible trigger that starts a quest, then becomes invisible after use.


File: config/journey/interactables/locked_portal.json

{
"id": "journey:locked_portal",
"right_click_script": [
"q.player.has_item('minecraft:diamond', 5) ? (",
" q.player.remove_item('minecraft:diamond', 5);",
" q.player.execute_command('tp {player} 100 64 200');",
" q.player.tell_minimessage('<aqua>Portal activated!');",
" q.set_interaction_result('success');",
") : (",
" q.player.tell_minimessage('<red>You need 5 diamonds to use this portal.');",
" q.set_interaction_result('fail');",
")"
],
"visibility_script": "q.player.has_completed_task('journey:unlock_portal')",
"width": 2.0,
"height": 3.0
}

Usage: Portal requiring quest completion and 5 diamonds to use.


File: config/journey/interactables/secret_coin.json

{
"id": "journey:secret_coin",
"right_click_script": [
"q.player.progress_levelable('Collector', 1);",
"q.player.tell_minimessage('<gold>+1 Secret Coin!');",
"q.player.add_flag('coin_' + q.this.uuid);",
"q.player.snowstorm_particle('cobblemon:sparkle', 0.0, 1.0, 0.0);",
"q.set_interaction_result('consume');"
],
"left_click_script": [
"q.player.tell_minimessage('<gray>Try right-clicking!');",
"q.set_interaction_result('fail');"
],
"visibility_script": "!q.player.has_flag('coin_' + q.this.uuid)",
"width": 0.5,
"height": 0.5
}

Usage: Small collectible that can only be collected once per player, with particle effect.


File: config/journey/interactables/trainer_battle.json

{
"id": "journey:trainer_battle",
"right_click_script": [
"q.player.execute_command('cobblemon battle {player} trainer:rival');",
"q.player.add_flag('fought_rival');",
"q.player.tell_minimessage('<red>Rival wants to battle!');",
"q.set_interaction_result('success');"
],
"visibility_script": "q.player.has_party_pokemon_matching('any') && !q.player.has_flag('fought_rival')",
"width": 1.0,
"height": 2.0
}

Usage: Triggers trainer battle, only visible to players with Pokémon who haven’t fought yet.


File: config/journey/interactables/puzzle_button.json

{
"id": "journey:puzzle_button",
"right_click_script": [
"!q.player.has_flag('button_1') ? q.player.add_flag('button_1') : 0;",
"q.player.has_flag('button_1') && q.player.has_flag('button_2') && q.player.has_flag('button_3') ? (",
" q.player.tell_minimessage('<green>Puzzle solved!');",
" q.player.start_task('journey:puzzle_reward');",
") : (",
" q.player.tell_minimessage('<yellow>Button pressed. Find the other buttons.');",
");",
"q.set_interaction_result('success');"
],
"visibility_script": "q.player.has_completed_task('journey:puzzle_quest')",
"width": 1.0,
"height": 1.0
}

Usage: Part of a 3-button puzzle tracked with flags.


File: config/journey/interactables/mysterious_voice.json

{
"id": "journey:mysterious_voice",
"right_click_script": [
"q.player.levelable_level('Wisdom') < 10.0 ? (",
" q.player.tell_minimessage('<gray><italic>You hear whispers... but cannot understand.');",
") : (",
" q.player.tell_minimessage('<gold>The voice speaks: \"Seek the ancient temple.\"');",
" q.player.add_flag('heard_prophecy');",
");",
"q.set_interaction_result('success');"
],
"visibility_script": "q.player.is_in_zone('temple-zone-uuid')",
"width": 2.0,
"height": 2.0
}

Usage: Voice that gives different messages based on player level.


Use the Journey command to spawn interactables in the world:

/journey interactable summon <type> <position>

Spawn at your location:

/journey interactable summon journey:quest_starter ~ ~ ~

Spawn 1 block above you:

/journey interactable summon journey:secret_coin ~ ~1 ~

Spawn at specific coordinates:

/journey interactable summon journey:locked_portal 100 64 -200

Permission Required: journey.command.interactable (default OP level 3)


Interactables automatically save with world data:

  • Persist across server restarts
  • Config changes apply to existing entities on reload
  • Entity UUID tracked for per-player flags
  • Use q.this.uuid in scripts to reference entity UUID

Always set interaction results:

q.set_interaction_result('success'); // Or 'consume', 'fail', etc.

Provide player feedback:

q.player.tell_minimessage('<green>Action completed!');

Use flags for one-time interactions:

!q.player.has_flag('collected') ? (...collect...) : (...already collected...)

Validate conditions before actions:

q.player.has_item('key', 1) ? (...unlock...) : (...locked...)

Hide completed content:

{
"visibility_script": "!q.player.has_flag('completed_puzzle')"
}

Show only to eligible players:

{
"visibility_script": "q.player.has_completed_task('journey:prerequisite')"
}

Combine multiple conditions:

{
"visibility_script": "q.player.has_flag('unlocked') && q.player.levelable_level('Mining') >= 10.0"
}

Match hitbox to purpose:

  • Small collectibles: 0.5 x 0.5
  • Standard buttons: 1.0 x 1.0
  • Doors/portals: 2.0 x 3.0
  • Large zones: 3.0 x 3.0

Test hitbox in-game:

  • Use F3+B to see entity hitboxes
  • Adjust based on player experience

Descriptive IDs:

journey:quest_trigger
journey:secret_coin_1
journey:portal_to_nether

One file per interactable type:

interactables/
quest_starter.json
secret_coin.json
locked_portal.json

Start tasks from interactables:

q.player.start_task('journey:epic_quest');

Check task completion for visibility:

q.player.has_completed_task('journey:prerequisite')

Track interaction state:

q.player.add_flag('button_pressed');
!q.player.has_flag('collected') // Visibility check

Require skill levels:

q.player.levelable_level('Mining') >= 25.0

Award XP:

q.player.progress_levelable('Exploration', 50.0);

Launch cinematic sequences:

q.player.launch_timeline('journey:cutscene_portal');

Show only in specific zones:

q.player.is_in_zone('secret-area-uuid')

Interactable Configs: config/journey/interactables/<name>.json

World Data: Saved automatically in world files


✅ Respond to right-click and left-click ✅ Execute MoLang scripts ✅ Have per-player conditional visibility ✅ Persist across server restarts ✅ Support customizable hitboxes

No visual model - Completely invisible (use particles for feedback) ❌ No animation - Static entities ❌ No movement - Fixed position ❌ No sound - Use MoLang to play sounds via commands

For visible NPCs: Use Cobblemon’s NPC system with TaskSource instead.


The Interactables System provides powerful invisible triggers and interactions for creating immersive, dynamic world content controlled entirely through MoLang scripting.