Map Playground Example
This example demonstrates how to interact with the World of Warcraft minimap using the IZI SDK. We'll build a plugin that lets you click on the map to place 3D markers in the game world, with support for adding, removing, and clearing markers.
The plugin allows clicking on the minimap to place green circle markers in the world. Cyan lines connect each marker to the player, and a HUD shows the marker count.
What You'll Learn
- How to detect map clicks with
izi.on_key_release() - Converting map cursor position to world coordinates with
izi.get_cursor_world_pos() - Checking if the map is open with
core.game_ui.is_map_open() - Drawing 3D circles and lines for marker visualization
- Managing a collection of world positions
- Finding the nearest marker for removal
Plugin Structure
header.lua
local plugin = {}
plugin.name = "Map Playground"
plugin.version = "1.0.0"
plugin.author = "Silvi"
plugin.load = true
local local_player = core.object_manager.get_local_player()
if not local_player or not local_player:is_valid() then
plugin.load = false
return plugin
end
return plugin
main.lua
--[[
Map Playground - Minimap interaction with IZI SDK
This plugin demonstrates how to:
- Detect mouse clicks on the minimap
- Convert map cursor position to world coordinates
- Place and manage 3D markers in the game world
- Draw visual feedback (circles, lines, HUD)
Controls:
- Left-click on minimap: Add a marker
- Right-click on minimap: Remove nearest marker
- Middle-click on minimap: Clear all markers
Author: Silvi
]]
-- ============================================================================
-- DEPENDENCIES
-- ============================================================================
local vec2 = require("common/geometry/vector_2")
local vec3 = require("common/geometry/vector_3")
local color = require("common/color")
local izi = require("common/izi_sdk")
-- ============================================================================
-- CONFIGURATION
-- ============================================================================
local CONFIG = {
marker_radius = 1.5, -- Size of marker circles
marker_thickness = 2.0, -- Line thickness for circles
marker_fade = 2.5, -- Fade factor for 3D rendering
marker_color = color.green(),
line_color = color.cyan(),
remove_threshold = 8.0, -- Distance threshold for marker removal
}
-- ============================================================================
-- KEY CODES
-- ============================================================================
local VK_LBUTTON = 0x01 -- Left mouse button
local VK_RBUTTON = 0x02 -- Right mouse button
local VK_MBUTTON = 0x04 -- Middle mouse button
-- ============================================================================
-- MARKER STORAGE
-- ============================================================================
local markers = {} -- Array of vec3 world positions
-- ============================================================================
-- MARKER FUNCTIONS
-- ============================================================================
local function add_marker(world_pos)
markers[#markers + 1] = world_pos
core.log("[MapPlayground] Marker added: (" ..
string.format("%.1f", world_pos.x) .. ", " ..
string.format("%.1f", world_pos.y) .. ", " ..
string.format("%.1f", world_pos.z) .. ")")
end
local function find_nearest_marker(world_pos)
local nearest_idx = nil
local nearest_dist = CONFIG.remove_threshold * CONFIG.remove_threshold
for i, marker in ipairs(markers) do
local dist = marker:squared_dist_to_ignore_z(world_pos)
if dist < nearest_dist then
nearest_dist = dist
nearest_idx = i
end
end
return nearest_idx
end
local function remove_marker(world_pos)
local idx = find_nearest_marker(world_pos)
if idx then
local removed = markers[idx]
table.remove(markers, idx)
core.log("[MapPlayground] Marker removed: (" ..
string.format("%.1f", removed.x) .. ", " ..
string.format("%.1f", removed.y) .. ")")
return true
end
return false
end
local function clear_markers()
local count = #markers
markers = {}
if count > 0 then
core.log("[MapPlayground] Cleared " .. count .. " markers")
end
end
-- ============================================================================
-- CLICK HANDLERS
-- ============================================================================
local function handle_left_click()
-- Don't process if menu is open
if core.graphics.is_menu_open() then return end
-- Only process if map is open
if not core.game_ui.is_map_open() then return end
-- Get world position from map cursor - one-liner via IZI!
local world_pos = izi.get_cursor_world_pos()
if world_pos then
add_marker(world_pos)
end
end
local function handle_right_click()
if core.graphics.is_menu_open() then return end
if not core.game_ui.is_map_open() then return end
local world_pos = izi.get_cursor_world_pos()
if world_pos then
remove_marker(world_pos)
end
end
local function handle_middle_click()
if core.graphics.is_menu_open() then return end
if not core.game_ui.is_map_open() then return end
clear_markers()
end
-- ============================================================================
-- REGISTER KEY CALLBACKS
-- ============================================================================
izi.on_key_release(VK_LBUTTON, handle_left_click)
izi.on_key_release(VK_RBUTTON, handle_right_click)
izi.on_key_release(VK_MBUTTON, handle_middle_click)
-- ============================================================================
-- RENDER CALLBACK
-- ============================================================================
local function on_render()
local player = core.object_manager.get_local_player()
if not player then return end
local player_pos = player:get_position()
-- Draw all markers
for _, marker in ipairs(markers) do
-- Draw circle at marker position
core.graphics.circle_3d(
marker,
CONFIG.marker_radius,
CONFIG.marker_color,
CONFIG.marker_thickness,
CONFIG.marker_fade
)
-- Draw line from player to marker
core.graphics.line_3d(
player_pos,
marker,
CONFIG.line_color,
3, -- thickness
1.5, -- fade factor
true -- has_volume
)
end
-- Draw HUD
if #markers > 0 then
local hud_text = "Markers: " .. #markers .. " | MMB to clear"
core.graphics.text_2d(hud_text, vec2.new(20, 20), 16, CONFIG.marker_color, false)
end
end
core.register_on_render_callback(on_render)
-- ============================================================================
-- STARTUP LOG
-- ============================================================================
core.log("[MapPlayground] Loaded! LMB=Add, RMB=Remove, MMB=Clear")
Code Breakdown
1. Key IZI Functions
This example showcases two powerful IZI SDK functions:
-- Get world position from where cursor is pointing on the map
local world_pos = izi.get_cursor_world_pos()
-- Register a callback for when a key is released
izi.on_key_release(VK_LBUTTON, handle_left_click)
2. Checking Map State
local function handle_left_click()
-- Don't process if our menu is open
if core.graphics.is_menu_open() then return end
-- Only process clicks when the game map is open
if not core.game_ui.is_map_open() then return end
-- Now safe to get map cursor position
local world_pos = izi.get_cursor_world_pos()
-- ...
end
Important checks before processing map clicks:
core.graphics.is_menu_open()- Skip if plugin menu is opencore.game_ui.is_map_open()- Only process when map is visible
3. Converting Map to World Coordinates
local world_pos = izi.get_cursor_world_pos()
This single function call:
- Gets the current map ID
- Gets the normalized cursor position on the map
- Converts map coordinates to world X/Y
- Calculates the terrain height (Z) at that position
- Returns a complete
vec3world position
Without IZI, you'd need to call multiple functions manually:
-- The hard way (what IZI does internally)
local map_id = core.game_ui.get_current_map_id()
local cursor = core.game_ui.get_normalized_cursor_position()
local world_2d = core.game_ui.get_world_pos_from_map_pos(map_id, cursor)
local height = core.get_height_for_position(vec3.new(world_2d.x, world_2d.y, 0))
local world_pos = vec3.new(world_2d.x, world_2d.y, height)
-- The easy way (IZI)
local world_pos = izi.get_cursor_world_pos()
4. Registering Key Callbacks
local VK_LBUTTON = 0x01 -- Left mouse
local VK_RBUTTON = 0x02 -- Right mouse
local VK_MBUTTON = 0x04 -- Middle mouse
izi.on_key_release(VK_LBUTTON, handle_left_click)
izi.on_key_release(VK_RBUTTON, handle_right_click)
izi.on_key_release(VK_MBUTTON, handle_middle_click)
izi.on_key_release() registers a callback that fires once when a key is released. This is better than checking is_key_pressed() every frame because:
- Fires exactly once per click
- No need to track previous key state
- Cleaner code without state management
5. Finding the Nearest Marker
local function find_nearest_marker(world_pos)
local nearest_idx = nil
local nearest_dist = CONFIG.remove_threshold * CONFIG.remove_threshold
for i, marker in ipairs(markers) do
-- Use squared distance for performance (avoids sqrt)
local dist = marker:squared_dist_to_ignore_z(world_pos)
if dist < nearest_dist then
nearest_dist = dist
nearest_idx = i
end
end
return nearest_idx
end
Performance tip: Use squared_dist_to_ignore_z() instead of dist_to() when comparing distances. Squared distance avoids the expensive square root calculation.
6. Drawing 3D Markers
-- Draw circle at marker position
core.graphics.circle_3d(
marker, -- vec3 position
CONFIG.marker_radius, -- radius in yards
CONFIG.marker_color, -- color
CONFIG.marker_thickness, -- line thickness
CONFIG.marker_fade -- fade factor (higher = faster fade at distance)
)
-- Draw line from player to marker
core.graphics.line_3d(
player_pos, -- start position
marker, -- end position
CONFIG.line_color, -- color
3, -- thickness
1.5, -- fade factor
true -- has_volume (3D depth)
)
IZI SDK Map API Reference
Functions
| Function | Description |
|---|---|
izi.get_cursor_world_pos() | Get world vec3 from map cursor position |
izi.on_key_release(key, callback) | Register callback for key release event |
izi.on_key_press(key, callback) | Register callback for key press event |
Related Core Functions
| Function | Description |
|---|---|
core.game_ui.is_map_open() | Check if world map is open |
core.graphics.is_menu_open() | Check if plugin menu is open |
core.game_ui.get_current_map_id() | Get current map ID |
core.game_ui.get_world_pos_from_map_pos() | Convert map coords to world |
Common Virtual Key Codes
| Code | Constant | Key |
|---|---|---|
0x01 | VK_LBUTTON | Left mouse button |
0x02 | VK_RBUTTON | Right mouse button |
0x04 | VK_MBUTTON | Middle mouse button |
0x70 | VK_F1 | F1 key |
0x74 | VK_F5 | F5 key |
0x1B | VK_ESCAPE | Escape key |
See Microsoft Virtual Key Codes for the complete list.
Customization
Changing Marker Appearance
local CONFIG = {
marker_radius = 2.0, -- Larger circles
marker_thickness = 3.0, -- Thicker lines
marker_color = color.yellow(), -- Yellow markers
line_color = color.red(), -- Red connection lines
}
Adding Marker Labels
local function on_render()
for i, marker in ipairs(markers) do
-- Draw circle
core.graphics.circle_3d(marker, CONFIG.marker_radius, CONFIG.marker_color, 2, 2.5)
-- Draw label above marker
local label_pos = vec3.new(marker.x, marker.y, marker.z + 2)
core.graphics.text_3d("Marker " .. i, label_pos, 14, color.white(), true)
end
end
Saving Markers to File
local function save_markers()
if #markers == 0 then return end
local data = {}
for i, m in ipairs(markers) do
data[i] = string.format('{"x":%.2f,"y":%.2f,"z":%.2f}', m.x, m.y, m.z)
end
local content = "[" .. table.concat(data, ",") .. "]"
core.create_data_file("markers.json")
core.write_data_file("markers.json", content)
core.log("Saved " .. #markers .. " markers")
end
local function load_markers()
local content = core.read_data_file("markers.json")
if not content or content == "" then return end
markers = {}
for x, y, z in content:gmatch('{"x":([%d%.%-]+),"y":([%d%.%-]+),"z":([%d%.%-]+)}') do
markers[#markers + 1] = vec3.new(tonumber(x), tonumber(y), tonumber(z))
end
core.log("Loaded " .. #markers .. " markers")
end
Using Markers for Navigation
local movement = require("common/utility/simple_movement")
local function navigate_to_markers()
if #markers == 0 then return end
movement:navigate(markers, false, true) -- waypoints, loop, start_from_beginning
end
local function on_update()
if is_navigating then
local reached = movement:process()
if reached then
is_navigating = false
core.log("Navigation complete!")
end
end
end
Related Documentation
- IZI SDK Reference - Complete IZI SDK documentation
- Game UI API - Map and UI functions
- Graphics API - Drawing functions
- Input API - Key detection
- Simple Movement - Automated navigation
Tips
izi.get_cursor_world_pos() only works when the world map is open. Always check core.game_ui.is_map_open() before calling it.
When the plugin menu is open, clicks might be intended for the menu, not the map. Check core.graphics.is_menu_open() to avoid unintended marker placement.
Use on_key_release() for click actions to ensure the callback fires exactly once per click. on_key_press() would fire on every frame while the key is held.
When comparing distances, use squared_dist_to_ignore_z() instead of dist_to(). It avoids the expensive square root calculation and is faster for comparisons.
Conclusion
This Map Playground example demonstrates powerful map interaction capabilities using the IZI SDK. By combining izi.get_cursor_world_pos() with izi.on_key_release(), you can create intuitive tools that let users interact with the game world through the minimap.
Key Takeaways:
izi.get_cursor_world_pos()- One-liner to get world position from map cursorizi.on_key_release()- Clean callback registration for mouse/key eventscore.game_ui.is_map_open()- Essential check before map interactions- Squared Distance - Use for performance when comparing distances
- 3D Visualization -
circle_3d()andline_3d()for marker display - State Checks - Always verify map/menu state before processing clicks
This example provides a foundation for building map-based tools like waypoint editors, farming route planners, or navigation systems. Feel free to extend it with saving/loading, navigation integration, or custom marker types!