-- Node Color Mapping System for Luanti -- Dynamically generates colors based on node properties, keywords, and groups local nodecolors = {} -- ============================================================================ -- COLOR PALETTE - 16 base colors for all terrain types -- ============================================================================ local PALETTE = { GRASS_GREEN = {95, 159, 53}, DIRT_BROWN = {134, 96, 67}, STONE_GRAY = {128, 128, 128}, SAND_TAN = {194, 178, 128}, DESERT_SAND = {210, 180, 140}, WATER_BLUE = {38, 92, 255}, SNOW_WHITE = {240, 240, 240}, ICE_BLUE = {175, 210, 255}, TREE_BROWN = {101, 84, 50}, LEAVES_GREEN = {60, 100, 30}, LAVA_ORANGE = {255, 100, 0}, COAL_BLACK = {64, 64, 64}, IRON_GRAY = {140, 120, 110}, GOLD_YELLOW = {220, 180, 50}, GRAVEL_GRAY = {136, 126, 126}, DEFAULT_GRAY = {100, 100, 100} } -- ============================================================================ -- MANUAL OVERRIDES - Highest priority, exact node name matches -- ============================================================================ local manual_overrides = { -- Special cases that don't fit auto-detection well ["default:apple"] = PALETTE.GOLD_YELLOW, ["mcl_core:obsidian"] = {20, 10, 30}, ["mcl_core:bedrock"] = {40, 40, 40}, ["mcl_nether:netherrack"] = {100, 50, 50}, ["mcl_nether:soul_sand"] = {80, 70, 60}, ["mcl_end:end_stone"] = {220, 220, 180}, ["nc_fire:ash"] = {80, 80, 80}, ["nc_concrete:concrete"] = {160, 160, 160}, } -- ============================================================================ -- KEYWORD DETECTION - Pattern matching in node names -- ============================================================================ local keyword_colors = { -- Grass and vegetation {pattern = "grass", color = PALETTE.GRASS_GREEN}, {pattern = "turf", color = PALETTE.GRASS_GREEN}, {pattern = "tallgrass", color = PALETTE.GRASS_GREEN}, {pattern = "sedge", color = PALETTE.GRASS_GREEN}, -- Dirt and soil {pattern = "dirt", color = PALETTE.DIRT_BROWN}, {pattern = "soil", color = PALETTE.DIRT_BROWN}, {pattern = "podzol", color = {90, 70, 50}}, {pattern = "coarse_dirt", color = {120, 85, 60}}, {pattern = "mycelium", color = {110, 90, 110}}, {pattern = "clay", color = {160, 160, 180}}, -- Stone types {pattern = "stone", color = PALETTE.STONE_GRAY}, {pattern = "cobble", color = PALETTE.DEFAULT_GRAY}, {pattern = "mossycobble", color = {80, 120, 80}}, {pattern = "granite", color = {150, 100, 80}}, {pattern = "andesite", color = {130, 130, 130}}, {pattern = "diorite", color = {180, 180, 180}}, {pattern = "annealed", color = {150, 150, 150}}, {pattern = "pumice", color = {200, 190, 180}}, {pattern = "lode", color = {110, 110, 110}}, -- Sand and desert {pattern = "sand", color = PALETTE.SAND_TAN}, {pattern = "redsand", color = PALETTE.DESERT_SAND}, {pattern = "desert", color = PALETTE.DESERT_SAND}, -- Water {pattern = "water", color = PALETTE.WATER_BLUE}, {pattern = "river_water", color = {20, 100, 200}}, -- Snow and ice {pattern = "snow", color = PALETTE.SNOW_WHITE}, {pattern = "ice", color = PALETTE.ICE_BLUE}, {pattern = "packed_ice", color = {160, 200, 255}}, -- Trees and wood {pattern = "tree", color = PALETTE.TREE_BROWN}, {pattern = "trunk", color = PALETTE.TREE_BROWN}, {pattern = "bark", color = {90, 70, 45}}, {pattern = "root", color = {80, 60, 40}}, {pattern = "log", color = PALETTE.TREE_BROWN}, {pattern = "wood", color = PALETTE.TREE_BROWN}, -- Specific tree types {pattern = "pine", color = {85, 70, 45}}, {pattern = "spruce", color = {85, 70, 45}}, {pattern = "jungle", color = {120, 90, 50}}, {pattern = "birch", color = {220, 220, 180}}, {pattern = "acacia", color = {160, 100, 60}}, {pattern = "dark", color = {60, 40, 20}}, -- Leaves {pattern = "leaves", color = PALETTE.LEAVES_GREEN}, {pattern = "needles", color = {45, 80, 25}}, {pattern = "jungleleaves", color = {50, 90, 30}}, {pattern = "birchleaves", color = {80, 120, 50}}, {pattern = "acacialeaves", color = {90, 110, 40}}, {pattern = "darkleaves", color = {40, 70, 20}}, -- Ores {pattern = "coal", color = PALETTE.COAL_BLACK}, {pattern = "iron", color = PALETTE.IRON_GRAY}, {pattern = "copper", color = {180, 120, 80}}, {pattern = "gold", color = PALETTE.GOLD_YELLOW}, {pattern = "diamond", color = {100, 200, 255}}, {pattern = "emerald", color = {50, 200, 100}}, {pattern = "lapis", color = {50, 100, 200}}, {pattern = "redstone", color = {200, 50, 50}}, -- Lava and gravel {pattern = "lava", color = PALETTE.LAVA_ORANGE}, {pattern = "gravel", color = PALETTE.GRAVEL_GRAY}, -- Flora {pattern = "flower", color = {200, 100, 150}}, {pattern = "junglegrass", color = {40, 80, 20}}, -- Farmland {pattern = "farmland", color = {120, 80, 50}}, {pattern = "grass_path", color = {140, 120, 80}}, } -- ============================================================================ -- GROUP-BASED DETECTION - Fallback based on node groups -- ============================================================================ local group_colors = { soil = PALETTE.DIRT_BROWN, sand = PALETTE.SAND_TAN, stone = PALETTE.STONE_GRAY, tree = PALETTE.TREE_BROWN, leaves = PALETTE.LEAVES_GREEN, water = PALETTE.WATER_BLUE, lava = PALETTE.LAVA_ORANGE, ice = PALETTE.ICE_BLUE, snow = PALETTE.SNOW_WHITE, } -- ============================================================================ -- COLOR CACHE - Store computed colors to avoid recalculation -- ============================================================================ local color_cache = {} -- ============================================================================ -- HELPER FUNCTIONS -- ============================================================================ -- Check if a string contains a pattern (case-insensitive) local function contains_pattern(str, pattern) return str:lower():find(pattern:lower(), 1, true) ~= nil end -- Detect color based on keywords in node name local function detect_color_from_keywords(nodename) for _, entry in ipairs(keyword_colors) do if contains_pattern(nodename, entry.pattern) then return entry.color end end return nil end -- Detect color based on node groups local function detect_color_from_groups(nodename) local node_def = minetest.registered_nodes[nodename] if not node_def or not node_def.groups then return nil end -- Check each group that has a color mapping for group, color in pairs(group_colors) do if node_def.groups[group] and node_def.groups[group] > 0 then return color end end return nil end -- ============================================================================ -- PUBLIC API -- ============================================================================ -- Get color for a node (main function) function nodecolors.get_color(nodename) -- Return from cache if available if color_cache[nodename] then return color_cache[nodename] end local color = nil -- Priority 1: Manual overrides if manual_overrides[nodename] then color = manual_overrides[nodename] end -- Priority 2: Keyword detection if not color then color = detect_color_from_keywords(nodename) end -- Priority 3: Group-based detection if not color then color = detect_color_from_groups(nodename) end -- Priority 4: Default color if not color then color = PALETTE.DEFAULT_GRAY end -- Cache the result color_cache[nodename] = color return color end -- Set a manual override color for a specific node function nodecolors.set_color(nodename, color) manual_overrides[nodename] = color color_cache[nodename] = color end -- Check if a node has a defined color (not default) function nodecolors.has_color(nodename) local color = nodecolors.get_color(nodename) return color ~= PALETTE.DEFAULT_GRAY end -- Add a new keyword pattern and color function nodecolors.add_keyword(pattern, color) table.insert(keyword_colors, {pattern = pattern, color = color}) end -- Add a new group color mapping function nodecolors.add_group_color(group, color) group_colors[group] = color end -- Get the color palette (for external customization) function nodecolors.get_palette() return PALETTE end -- Clear the color cache (useful after bulk changes) function nodecolors.clear_cache() color_cache = {} end -- Get statistics about color detection function nodecolors.get_stats() local stats = { manual_overrides = 0, keyword_patterns = 0, group_mappings = 0, cached_colors = 0 } for _ in pairs(manual_overrides) do stats.manual_overrides = stats.manual_overrides + 1 end stats.keyword_patterns = #keyword_colors for _ in pairs(group_colors) do stats.group_mappings = stats.group_mappings + 1 end for _ in pairs(color_cache) do stats.cached_colors = stats.cached_colors + 1 end return stats end -- Debug function: show how a color was determined function nodecolors.debug_color(nodename) local info = { nodename = nodename, color = nil, method = "none", details = "" } -- Check manual override if manual_overrides[nodename] then info.color = manual_overrides[nodename] info.method = "manual_override" return info end -- Check keywords local keyword_color = detect_color_from_keywords(nodename) if keyword_color then info.color = keyword_color info.method = "keyword" for _, entry in ipairs(keyword_colors) do if contains_pattern(nodename, entry.pattern) then info.details = "matched pattern: " .. entry.pattern break end end return info end -- Check groups local group_color = detect_color_from_groups(nodename) if group_color then info.color = group_color info.method = "group" local node_def = minetest.registered_nodes[nodename] if node_def and node_def.groups then for group, color in pairs(group_colors) do if node_def.groups[group] and node_def.groups[group] > 0 then info.details = "matched group: " .. group break end end end return info end -- Default info.color = PALETTE.DEFAULT_GRAY info.method = "default" return info end return nodecolors