diff --git a/assets/config/manifest.json b/assets/config/manifest.json index d558e25..d2e0df7 100644 --- a/assets/config/manifest.json +++ b/assets/config/manifest.json @@ -327,6 +327,186 @@ "filter":"nearest", "tileHeight":16, "tileWidth":16 + }, + "tiles.beach":{ + "path":"assets/Cute_Fantasy/Tiles/Beach/Beach_Tiles.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "trees.small_birch":{ + "path":"assets/Cute_Fantasy/Trees/Small_Birch_Tree.png", + "filter":"nearest", + "tileHeight":64, + "tileWidth":32 + }, + "trees.medium_birch":{ + "path":"assets/Cute_Fantasy/Trees/Medium_Birch_Tree.png", + "filter":"nearest", + "tileHeight":48, + "tileWidth":32 + }, + "trees.small_fruit":{ + "path":"assets/Cute_Fantasy/Trees/Small_Fruit_Tree.png", + "filter":"nearest", + "tileHeight":64, + "tileWidth":32 + }, + "props.flower_1":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Flower_Animations/Not_Potted/Flowers_1_Anim.png", + "filter":"nearest", + "tileHeight":32, + "tileWidth":16 + }, + "props.flower_2":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Flower_Animations/Not_Potted/Flowers_2_Anim.png", + "filter":"nearest", + "tileHeight":32, + "tileWidth":16 + }, + "props.flower_3":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Flower_Animations/Not_Potted/Flowers_3_Anim.png", + "filter":"nearest", + "tileHeight":32, + "tileWidth":16 + }, + "props.flower_4":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Flower_Animations/Not_Potted/Flowers_4_Anim.png", + "filter":"nearest", + "tileHeight":32, + "tileWidth":16 + }, + "props.flower_5":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Flower_Animations/Not_Potted/Flowers_5_Anim.png", + "filter":"nearest", + "tileHeight":32, + "tileWidth":16 + }, + "props.grass_anim_1":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Grass_Animations/Grass_1_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.grass_anim_2":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Grass_Animations/Grass_2_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.grass_anim_3":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Grass_Animations/Grass_3_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.mushroom_1":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Muschroom_Animations/muschroom_1_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.mushroom_2":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Muschroom_Animations/muschroom_2_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.mushroom_3":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Muschroom_Animations/muschroom_3_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.mushroom_4":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Muschroom_Animations/muschroom_4_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.mushroom_5":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Muschroom_Animations/muschroom_5_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.mushroom_6":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Muschroom_Animations/muschroom_6_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.mushroom_7":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Muschroom_Animations/muschroom_7_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.mushroom_8":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Muschroom_Animations/muschroom_8_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.cattail_1":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Water_Decor_Animations/Water_Plants/Cattail_1_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.cattail_2":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Water_Decor_Animations/Water_Plants/Cattail_2_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.cattail_3":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Water_Decor_Animations/Water_Plants/Cattail_3_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.cattail_4":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Water_Decor_Animations/Water_Plants/Cattail_4_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.cattail_5":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Water_Decor_Animations/Water_Plants/Cattail_5_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.lillypad_green_1":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Water_Decor_Animations/Water_Plants/Lillypad_Green_1_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.lillypad_green_2":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Water_Decor_Animations/Water_Plants/Lillypad_Green_2_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.lillypad_green_3":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Water_Decor_Animations/Water_Plants/Lillypad_Green_3_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.lillypad_red_1":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Water_Decor_Animations/Water_Plants/Lillypad_Red_1_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 + }, + "props.lillypad_purple_1":{ + "path":"assets/Cute_Fantasy/Outdoor decoration/Outdoor_Decor_Animations/Water_Decor_Animations/Water_Plants/Lillypad_Purple_1_Anim.png", + "filter":"nearest", + "tileHeight":16, + "tileWidth":16 } } } diff --git a/assets/config/tiles.json b/assets/config/tiles.json index af33bad..6ad73db 100644 --- a/assets/config/tiles.json +++ b/assets/config/tiles.json @@ -1,9 +1,7 @@ { "tiles":{ "water":{ - "collision":{ - "shape":"none" - }, + "collision":{ "shape":"none" }, "name":"Water", "overlay":[], "ground":{ @@ -15,15 +13,13 @@ "walkable":false }, "sand":{ - "collision":{ - "shape":"none" - }, + "collision":{ "shape":"none" }, "name":"Sand", "overlay":[], "ground":{ "tint":[0.96,0.88,0.62], "tileIds":[0], - "atlases":["tiles.path","tiles.grass_2"] + "atlas":"tiles.path" }, "color":[0.85,0.8,0.5], "walkable":true @@ -46,7 +42,7 @@ "ground":{ "tint":[0.78,0.8,0.84], "tileIds":[0], - "atlases":["tiles.cave_floor","tiles.path"] + "atlas":"tiles.cave_floor" }, "color":[0.4,0.4,0.4], "walkable":false @@ -68,52 +64,182 @@ "atlases":["trees.small_oak","trees.medium_oak","trees.small_spruce","trees.medium_spruce"] }, "ground":{ - "atlases":["tiles.grass_1","tiles.grass_2","tiles.grass_3","tiles.grass_4"], + "atlas":"tiles.grass_1", "tileIds":[0] }, "color":[0.15,0.45,0.15], "walkable":false }, "dirt":{ - "collision":{ - "shape":"none" - }, + "collision":{ "shape":"none" }, "name":"Dirt", "overlay":[], "ground":{ "tint":[0.84,0.66,0.43], "tileIds":[0], - "atlases":["tiles.path","tiles.cave_floor"] + "atlas":"tiles.path" }, "color":[0.6,0.45,0.25], "walkable":true }, "grass":{ - "collision":{ - "shape":"none" - }, + "collision":{ "shape":"none" }, "name":"Grass", "overlay":[], "ground":{ - "atlases":["tiles.grass_1","tiles.grass_2","tiles.grass_3","tiles.grass_4"], + "atlas":"tiles.grass_1", "tileIds":[0] }, "color":[0.35,0.65,0.25], "walkable":true }, "stone":{ - "collision":{ - "shape":"none" - }, + "collision":{ "shape":"none" }, "name":"Stone", "overlay":[], "ground":{ "tint":[0.78,0.8,0.84], "tileIds":[0], - "atlases":["tiles.cave_floor","tiles.path"] + "atlas":"tiles.cave_floor" }, "color":[0.55,0.55,0.55], "walkable":true + }, + "beach":{ + "collision":{ "shape":"none" }, + "name":"Beach", + "overlay":[], + "ground":{ + "atlas":"tiles.beach", + "tileIds":[0,1,2] + }, + "color":[0.90,0.82,0.60], + "walkable":true + }, + "flower":{ + "collision":{ "shape":"none" }, + "name":"Flower", + "overlay":{ + "atlases":["props.flower_1","props.flower_2","props.flower_3","props.flower_4","props.flower_5"], + "tileIds":[0], + "scale":1.0, + "anchorX":8, + "anchorY":32, + "oy":-2, + "tint":[1,1,1] + }, + "ground":{ + "atlas":"tiles.grass_1", + "tileIds":[0] + }, + "color":[0.40,0.70,0.30], + "walkable":true + }, + "tall_grass":{ + "collision":{ "shape":"none" }, + "name":"Tall Grass", + "overlay":{ + "atlases":["props.grass_anim_1","props.grass_anim_2","props.grass_anim_3"], + "frames":[0,1,2,3,4,5,6,7], + "frameDuration":0.13, + "scale":1.5, + "anchorX":8, + "anchorY":16, + "oy":-2, + "tint":[1,1,1] + }, + "ground":{ + "atlas":"tiles.grass_1", + "tileIds":[0] + }, + "color":[0.30,0.60,0.20], + "walkable":true + }, + "mushroom":{ + "collision":{ "shape":"none" }, + "name":"Mushroom", + "overlay":{ + "atlases":["props.mushroom_1","props.mushroom_2","props.mushroom_3","props.mushroom_4","props.mushroom_5","props.mushroom_6","props.mushroom_7","props.mushroom_8"], + "tileIds":[0], + "scale":1.4, + "anchorX":8, + "anchorY":16, + "oy":-2, + "tint":[1,1,1] + }, + "ground":{ + "tint":[0.84,0.66,0.43], + "atlas":"tiles.path", + "tileIds":[0] + }, + "color":[0.55,0.42,0.25], + "walkable":true + }, + "lily_pad":{ + "collision":{ "shape":"none" }, + "name":"Lily Pad", + "overlay":{ + "atlases":["props.lillypad_green_1","props.lillypad_green_2","props.lillypad_green_3","props.lillypad_red_1","props.lillypad_purple_1"], + "frames":[0,1,2,3,4,5,6,7], + "frameDuration":0.20, + "scale":1.2, + "anchorX":8, + "anchorY":16, + "oy":0, + "tint":[1,1,1] + }, + "ground":{ + "atlas":"tiles.water", + "frames":[0,1,2,3,4,5,6,7], + "frameDuration":0.18 + }, + "color":[0.20,0.50,0.30], + "walkable":false + }, + "cattail":{ + "collision":{ "shape":"none" }, + "name":"Cattail", + "overlay":{ + "atlases":["props.cattail_1","props.cattail_2","props.cattail_3","props.cattail_4","props.cattail_5"], + "frames":[0,1,2,3,4,5,6,7], + "frameDuration":0.15, + "scale":1.4, + "anchorX":8, + "anchorY":16, + "oy":-4, + "tint":[1,1,1] + }, + "ground":{ + "atlas":"tiles.water", + "frames":[0,1,2,3,4,5,6,7], + "frameDuration":0.18 + }, + "color":[0.15,0.40,0.25], + "walkable":false + }, + "birch":{ + "collision":{ + "shape":"circle", + "radius":0.12, + "offsetX":0, + "offsetY":-0.51 + }, + "name":"Birch Tree", + "overlay":{ + "atlases":["trees.small_birch","trees.medium_birch","trees.small_fruit"], + "tileIds":[1,2], + "scale":1.6, + "anchorX":16, + "anchorY":58, + "oy":2, + "tint":[1,1,1] + }, + "ground":{ + "atlas":"tiles.grass_1", + "tileIds":[0] + }, + "color":[0.70,0.85,0.55], + "walkable":false } } } diff --git a/src/core/assetmanager.lua b/src/core/assetmanager.lua index 88940ed..490f29c 100644 --- a/src/core/assetmanager.lua +++ b/src/core/assetmanager.lua @@ -62,7 +62,12 @@ local function mergeTables(dst, src) end local function chooseVariant(variants, tx, ty) - local hash = math.abs(tx * 73856093 + ty * 19349663) + -- Non-linear hash to avoid the regular checkerboard caused by linear + -- formulas with even-count variant lists (parity alternates per tile). + local hash = math.abs( + tx * 374761393 + ty * 1234567891 + + tx * ty * 6271 + tx * tx * 1301 + ty * ty * 7919 + ) return variants[(hash % #variants) + 1] end @@ -259,16 +264,22 @@ function AssetManager:_resolveGroundData(def, tx, ty, timeSeconds) } end -function AssetManager:_resolveOverlayImageData(def, tx, ty) +function AssetManager:_resolveOverlayImageData(def, tx, ty, timeSeconds) if def.overlay.atlas or def.overlay.atlases then local atlasId = def.overlay.atlas if def.overlay.atlases then atlasId = chooseVariant(def.overlay.atlases, tx, ty) end - local tileId = def.overlay.tileId or 0 - if def.overlay.tileIds then + local tileId + if def.overlay.frames then + local frameDuration = def.overlay.frameDuration or 0.2 + local frameIndex = (math.floor((timeSeconds or 0) / frameDuration) % #def.overlay.frames) + 1 + tileId = def.overlay.frames[frameIndex] + elseif def.overlay.tileIds then tileId = chooseVariant(def.overlay.tileIds, tx, ty) + else + tileId = def.overlay.tileId or 0 end local atlas = self:getAtlas(atlasId) @@ -304,7 +315,7 @@ function AssetManager:getTileRenderData(tileType, tx, ty, timeSeconds) anchorY = def.overlay.anchorY, } - overlay.image, overlay.quad = self:_resolveOverlayImageData(def, tx, ty) + overlay.image, overlay.quad = self:_resolveOverlayImageData(def, tx, ty, timeSeconds) result.overlay = overlay end diff --git a/src/rendering/colors.lua b/src/rendering/colors.lua new file mode 100644 index 0000000..276883a --- /dev/null +++ b/src/rendering/colors.lua @@ -0,0 +1,126 @@ +--- Color Palette Module +-- Centralized color definitions used across the codebase +-- Eliminates hardcoded color values and provides consistent theming + +local Colors = {} + +-- Color definitions +Colors.WHITE = {1, 1, 1, 1} +Colors.BLACK = {0, 0, 0, 1} +Colors.RED = {1, 0, 0, 1} +Colors.GREEN = {0, 1, 0, 1} +Colors.BLUE = {0, 0, 1, 1} +Colors.YELLOW = {1, 1, 0, 1} +Colors.CYAN = {0, 1, 1, 1} +Colors.MAGENTA = {1, 0, 1, 1} +Colors.ORANGE = {1, 0.5, 0, 1} +Colors.PURPLE = {0.5, 0, 1, 1} +Colors.PINK = {1, 0.75, 0.8, 1} +Colors.BROWN = {0.6, 0.3, 0, 1} +Colors.GRAY = {0.5, 0.5, 0.5, 1} +Colors.LIGHT_GRAY = {0.75, 0.75, 0.75, 1} +Colors.DARK_GRAY = {0.25, 0.25, 0.25, 1} + +-- Game-specific colors +Colors.GRASS = {0.2, 0.6, 0.2, 1} +Colors.DIRT = {0.4, 0.3, 0.1, 1} +Colors.STONE = {0.5, 0.5, 0.5, 1} +Colors.WATER = {0.2, 0.4, 0.8, 1} +Colors.SAND = {0.9, 0.8, 0.5, 1} + +-- UI colors +Colors.UI_BACKGROUND = {0.1, 0.1, 0.1, 0.8} +Colors.UI_BORDER = {0.3, 0.3, 0.3, 1} +Colors.UI_TEXT = {0.9, 0.9, 0.9, 1} +Colors.UI_HIGHLIGHT = {0.4, 0.6, 1, 1} +Colors.UI_SUCCESS = {0.2, 0.8, 0.2, 1} +Colors.UI_WARNING = {1, 0.8, 0, 1} +Colors.UI_ERROR = {1, 0.2, 0.2, 1} + +-- Health bar colors +Colors.HEALTH_FULL = {0.2, 0.8, 0.2, 1} +Colors.HEALTH_MEDIUM = {1, 0.8, 0, 1} +Colors.HEALTH_LOW = {1, 0.2, 0.2, 1} +Colors.HEALTH_BACKGROUND = {0.2, 0.2, 0.2, 0.8} + +-- Resource colors +Colors.WOOD = {0.6, 0.3, 0.1, 1} +Colors.IRON = {0.6, 0.6, 0.7, 1} +Colors.ROPE = {0.8, 0.7, 0.4, 1} +Colors.FOOD = {1, 0.4, 0.2, 1} +Colors.CLOTH = {0.9, 0.9, 0.9, 1} + +-- Building colors +Colors.BUILDING_VALID = {0.2, 1, 0.2, 0.5} +Colors.BUILDING_INVALID = {1, 0.2, 0.2, 0.5} +Colors.BUILDING_WOOD = {0.6, 0.4, 0.2, 1} +Colors.BUILDING_STONE = {0.5, 0.5, 0.5, 1} +Colors.BUILDING_REINFORCED = {0.3, 0.3, 0.4, 1} + +-- Enemy colors +Colors.ENEMY_FLY = {0.4, 0.4, 0.2, 1} +Colors.ENEMY_SHAMBLER = {0.3, 0.5, 0.3, 1} +Colors.ENEMY_HOUND = {0.6, 0.3, 0.2, 1} +Colors.ENEMY_BRUTE = {0.7, 0.2, 0.2, 1} +Colors.ENEMY_SPITTER = {0.4, 0.7, 0.4, 1} +Colors.ENEMY_WRAITH = {0.5, 0.3, 0.7, 0.7} +Colors.ENEMY_SIEGE = {0.8, 0.1, 0.1, 1} + +-- Class colors +Colors.WARRIOR = {0.8, 0.2, 0.2, 1} +Colors.ARCHER = {0.2, 0.8, 0.2, 1} +Colors.ENGINEER = {1, 0.8, 0.2, 1} +Colors.SCOUT = {0.4, 0.6, 1, 1} + +-- Debug colors +Colors.DEBUG_HITBOX = {1, 0, 0, 0.3} +Colors.DEBUG_PATH = {0, 1, 0, 0.5} +Colors.DEBUG_GRID = {0.3, 0.3, 0.3, 0.3} +Colors.DEBUG_TEXT = {1, 1, 0, 1} + +--- Set the current drawing color +-- @param color Color table {r, g, b, a} (values 0-1) +function Colors.set(color) + if color and #color >= 3 then + love.graphics.setColor(color[1], color[2], color[3], color[4] or 1) + else + love.graphics.setColor(1, 1, 1, 1) + end +end + +--- Create a new color with modified alpha +-- @param color Base color table +-- @param alpha New alpha value (0-1) +-- @return New color table with modified alpha +function Colors.withAlpha(color, alpha) + return {color[1], color[2], color[3], alpha} +end + +--- Blend two colors +-- @param color1 First color +-- @param color2 Second color +-- @param t Blend factor (0-1, 0=full color1, 1=full color2) +-- @return Blended color +function Colors.blend(color1, color2, t) + return { + color1[1] + (color2[1] - color1[1]) * t, + color1[2] + (color2[2] - color1[2]) * t, + color1[3] + (color2[3] - color1[3]) * t, + color1[4] + (color2[4] - color1[4]) * t + } +end + +--- Get health color based on percentage +-- @param healthPct Health percentage (0-1) +-- @return Color table for health bar +function Colors.getHealthColor(healthPct) + if healthPct > 0.6 then + return Colors.HEALTH_FULL + elseif healthPct > 0.3 then + return Colors.HEALTH_MEDIUM + else + return Colors.HEALTH_LOW + end +end + +return Colors diff --git a/src/rendering/spriteutils.lua b/src/rendering/spriteutils.lua new file mode 100644 index 0000000..dd26741 --- /dev/null +++ b/src/rendering/spriteutils.lua @@ -0,0 +1,193 @@ +--- Sprite Utility Module +-- Provides common sprite loading and rendering operations + +local SpriteUtils = {} + +--- Load an image from a sprite definition +-- @param spriteDef Sprite definition table with path field +-- @return Love2D Image object or nil if failed +function SpriteUtils.loadImage(spriteDef) + if not spriteDef or not spriteDef.path then + return nil + end + + local success, result = pcall(function() + return love.graphics.newImage(spriteDef.path) + end) + + if success then + return result + else + print("Warning: Failed to load image: " .. tostring(spriteDef.path)) + return nil + end +end + +--- Draw an isometric diamond shape +-- Used for building footprints and placeholders +-- @param sx Screen x coordinate (center) +-- @param sy Screen y coordinate (center) +-- @param width Diamond width +-- @param height Diamond height +-- @param fillColor Fill color table {r, g, b, a} +-- @param outlineColor Optional outline color table +function SpriteUtils.drawIsoDiamond(sx, sy, width, height, fillColor, outlineColor) + width = width or 32 + height = height or 16 + + local halfW = width / 2 + local halfH = height / 2 + + -- Diamond vertices (top, right, bottom, left) + local vertices = { + sx, sy - halfH, -- Top + sx + halfW, sy, -- Right + sx, sy + halfH, -- Bottom + sx - halfW, sy -- Left + } + + -- Draw fill + if fillColor then + love.graphics.setColor(fillColor[1], fillColor[2], fillColor[3], fillColor[4] or 1) + love.graphics.polygon('fill', vertices) + end + + -- Draw outline + if outlineColor then + love.graphics.setColor(outlineColor[1], outlineColor[2], outlineColor[3], outlineColor[4] or 1) + love.graphics.polygon('line', vertices) + end + + -- Reset color + love.graphics.setColor(1, 1, 1, 1) +end + +--- Draw an isometric rectangle +-- Used for wall segments and rectangular buildings +-- @param sx Screen x coordinate (center) +-- @param sy Screen y coordinate (top) +-- @param width Rectangle width +-- @param depth Isometric depth +-- @param height Rectangle height +-- @param fillColor Fill color table +-- @param outlineColor Optional outline color table +function SpriteUtils.drawIsoRect(sx, sy, width, depth, height, fillColor, outlineColor) + width = width or 32 + depth = depth or 16 + height = height or 32 + + local halfW = width / 2 + local halfD = depth / 2 + + -- Top face (diamond) + local topVertices = { + sx, sy, -- Top + sx + halfW, sy + halfD, -- Right + sx, sy + depth, -- Bottom + sx - halfW, sy + halfD -- Left + } + + -- Front face (left side) + local frontVertices = { + sx - halfW, sy + halfD, + sx, sy + depth, + sx, sy + depth + height, + sx - halfW, sy + halfD + height + } + + -- Right face + local rightVertices = { + sx, sy + depth, + sx + halfW, sy + halfD, + sx + halfW, sy + halfD + height, + sx, sy + depth + height + } + + -- Draw faces + if fillColor then + love.graphics.setColor(fillColor[1], fillColor[2], fillColor[3], fillColor[4] or 1) + + -- Draw darker shades for depth + local frontShade = {fillColor[1] * 0.7, fillColor[2] * 0.7, fillColor[3] * 0.7, fillColor[4] or 1} + local rightShade = {fillColor[1] * 0.85, fillColor[2] * 0.85, fillColor[3] * 0.85, fillColor[4] or 1} + + love.graphics.setColor(frontShade) + love.graphics.polygon('fill', frontVertices) + + love.graphics.setColor(rightShade) + love.graphics.polygon('fill', rightVertices) + + love.graphics.setColor(fillColor) + love.graphics.polygon('fill', topVertices) + end + + -- Draw outlines + if outlineColor then + love.graphics.setColor(outlineColor[1], outlineColor[2], outlineColor[3], outlineColor[4] or 1) + love.graphics.polygon('line', topVertices) + love.graphics.polygon('line', frontVertices) + love.graphics.polygon('line', rightVertices) + end + + -- Reset color + love.graphics.setColor(1, 1, 1, 1) +end + +--- Draw a sprite with optional offset and scale +-- @param image Love2D Image object +-- @param x Screen x coordinate +-- @param y Screen y coordinate +-- @param offsetX Optional x offset (default: center) +-- @param offsetY Optional y offset (default: center) +-- @param scaleX Optional x scale (default: 1) +-- @param scaleY Optional y scale (default: 1) +-- @param rotation Optional rotation in radians (default: 0) +function SpriteUtils.drawSprite(image, x, y, offsetX, offsetY, scaleX, scaleY, rotation) + if not image then return end + + local width = image:getWidth() + local height = image:getHeight() + + offsetX = offsetX or (width / 2) + offsetY = offsetY or (height / 2) + scaleX = scaleX or 1 + scaleY = scaleY or 1 + rotation = rotation or 0 + + love.graphics.draw(image, x, y, rotation, scaleX, scaleY, offsetX, offsetY) +end + +--- Create a simple colored rectangle image +-- Useful for placeholder sprites +-- @param width Rectangle width +-- @param height Rectangle height +-- @param color Color table {r, g, b, a} +-- @return Love2D Canvas that can be used as an image +function SpriteUtils.createColoredRect(width, height, color) + local canvas = love.graphics.newCanvas(width, height) + + love.graphics.setCanvas(canvas) + love.graphics.clear() + love.graphics.setColor(color[1], color[2], color[3], color[4] or 1) + love.graphics.rectangle('fill', 0, 0, width, height) + love.graphics.setCanvas() + + love.graphics.setColor(1, 1, 1, 1) + + return canvas +end + +--- Batch load multiple sprites from definitions +-- @param spriteDefs Table of sprite definitions {name = {path = "..."}, ...} +-- @return Table of loaded images {name = Image, ...} +function SpriteUtils.loadSprites(spriteDefs) + local sprites = {} + + for name, def in pairs(spriteDefs) do + sprites[name] = SpriteUtils.loadImage(def) + end + + return sprites +end + +return SpriteUtils diff --git a/src/states/daystate.lua b/src/states/daystate.lua index 2eeccce..7110133 100644 --- a/src/states/daystate.lua +++ b/src/states/daystate.lua @@ -803,15 +803,8 @@ function DayState:_queueTileOverlay(entry, renderData, dim, entityDrawList) } end -function DayState:_drawVisibleTileOutline(entry) - if not entry.visible then return end - local sx, sy = Iso.tileToScreen(entry.tx, entry.ty) - love.graphics.setColor(0, 0, 0, 0.12) - love.graphics.polygon("line", - sx, sy, - sx + Iso.TILE_W * 0.5, sy + Iso.TILE_H * 0.5, - sx, sy + Iso.TILE_H, - sx - Iso.TILE_W * 0.5, sy + Iso.TILE_H * 0.5) +function DayState:_drawVisibleTileOutline(_entry) + -- Tile grid outline disabled — too prominent at full map visibility. end function DayState:_drawTerrain(drawList, worldTime, entityDrawList) diff --git a/src/states/menustate.lua b/src/states/menustate.lua index bb158b5..65940f1 100644 --- a/src/states/menustate.lua +++ b/src/states/menustate.lua @@ -430,7 +430,6 @@ function MenuState:mousepressed(x, y, button) self:selectOption() end end - function MenuState:gamepadPressed(joystick, button) if button == "dpup" then self.selectedOption = self.selectedOption - 1 diff --git a/src/states/worldstate.lua b/src/states/worldstate.lua index 52d489f..0ae53dd 100644 --- a/src/states/worldstate.lua +++ b/src/states/worldstate.lua @@ -583,7 +583,7 @@ function WorldState:draw() self.dayPhase.harvestManager:draw() if self.dayPhase.buildGhost then - self.dayPhase.buildGhost:draw(self.dayPhase.buildManager, self.dayPhase.depot) + self.dayPhase.buildGhost:draw(self.dayPhase.buildManager, self.dayPhase.depot, self.inventory) end end diff --git a/src/ui/uiutils.lua b/src/ui/uiutils.lua new file mode 100644 index 0000000..798d9b9 --- /dev/null +++ b/src/ui/uiutils.lua @@ -0,0 +1,257 @@ +--- UI Utility Module +-- Provides common UI rendering operations (progress bars, fonts, etc.) + +local UIUtils = {} + +-- Font cache to prevent creating duplicate fonts +local fontCache = {} + +--- Draw a progress bar +-- @param x Bar x coordinate (top-left) +-- @param y Bar y coordinate (top-left) +-- @param width Bar width +-- @param height Bar height +-- @param progress Progress value (0-1) +-- @param fillColor Fill color table {r, g, b, a} +-- @param backgroundColor Background color table (optional) +-- @param borderColor Border color table (optional) +-- @param label Optional text label to display in center +function UIUtils.drawProgressBar(x, y, width, height, progress, fillColor, backgroundColor, borderColor, label) + progress = math.max(0, math.min(1, progress)) + + -- Draw background + if backgroundColor then + love.graphics.setColor(backgroundColor[1], backgroundColor[2], backgroundColor[3], backgroundColor[4] or 1) + love.graphics.rectangle('fill', x, y, width, height) + end + + -- Draw fill + if progress > 0 and fillColor then + love.graphics.setColor(fillColor[1], fillColor[2], fillColor[3], fillColor[4] or 1) + love.graphics.rectangle('fill', x, y, width * progress, height) + end + + -- Draw border + if borderColor then + love.graphics.setColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1) + love.graphics.rectangle('line', x, y, width, height) + end + + -- Draw label + if label then + local font = love.graphics.getFont() + local textWidth = font:getWidth(label) + local textHeight = font:getHeight() + + love.graphics.setColor(1, 1, 1, 1) + love.graphics.print(label, + x + (width - textWidth) / 2, + y + (height - textHeight) / 2) + end + + -- Reset color + love.graphics.setColor(1, 1, 1, 1) +end + +--- Draw a vertical progress bar +-- @param x Bar x coordinate (top-left) +-- @param y Bar y coordinate (top-left) +-- @param width Bar width +-- @param height Bar height +-- @param progress Progress value (0-1) +-- @param fillColor Fill color table +-- @param backgroundColor Background color table (optional) +-- @param borderColor Border color table (optional) +function UIUtils.drawProgressBarVertical(x, y, width, height, progress, fillColor, backgroundColor, borderColor) + progress = math.max(0, math.min(1, progress)) + + -- Draw background + if backgroundColor then + love.graphics.setColor(backgroundColor[1], backgroundColor[2], backgroundColor[3], backgroundColor[4] or 1) + love.graphics.rectangle('fill', x, y, width, height) + end + + -- Draw fill (from bottom up) + if progress > 0 and fillColor then + local fillHeight = height * progress + love.graphics.setColor(fillColor[1], fillColor[2], fillColor[3], fillColor[4] or 1) + love.graphics.rectangle('fill', x, y + height - fillHeight, width, fillHeight) + end + + -- Draw border + if borderColor then + love.graphics.setColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1) + love.graphics.rectangle('line', x, y, width, height) + end + + -- Reset color + love.graphics.setColor(1, 1, 1, 1) +end + +--- Get or create a font (cached) +-- @param size Font size +-- @param fontPath Optional path to font file (default: Love2D default font) +-- @return Love2D Font object +function UIUtils.getFont(size, fontPath) + local key = (fontPath or "default") .. "_" .. size + + if not fontCache[key] then + if fontPath then + local success, font = pcall(function() + return love.graphics.newFont(fontPath, size) + end) + if success then + fontCache[key] = font + else + print("Warning: Failed to load font: " .. fontPath) + fontCache[key] = love.graphics.newFont(size) + end + else + fontCache[key] = love.graphics.newFont(size) + end + end + + return fontCache[key] +end + +--- Lazy load fonts for a state +-- Initializes fonts only when first accessed +-- @param state State object to add lazy font loading to +-- @param fontDefinitions Table of font definitions {name = {size=12, path="..."}, ...} +function UIUtils.lazyLoadFonts(state, fontDefinitions) + state._fonts = state._fonts or {} + state._fontDefs = fontDefinitions + + -- Create metatable for lazy loading + local fontProxy = setmetatable({}, { + __index = function(_, key) + if not state._fonts[key] and state._fontDefs[key] then + local def = state._fontDefs[key] + state._fonts[key] = UIUtils.getFont(def.size, def.path) + end + return state._fonts[key] + end + }) + + state.fonts = fontProxy +end + +--- Draw a text with shadow +-- @param text Text to draw +-- @param x Text x coordinate +-- @param y Text y coordinate +-- @param color Text color table (optional, default: white) +-- @param shadowColor Shadow color table (optional, default: black) +-- @param shadowOffset Shadow offset in pixels (optional, default: 1) +function UIUtils.drawTextWithShadow(text, x, y, color, shadowColor, shadowOffset) + color = color or {1, 1, 1, 1} + shadowColor = shadowColor or {0, 0, 0, 0.5} + shadowOffset = shadowOffset or 1 + + -- Draw shadow + love.graphics.setColor(shadowColor[1], shadowColor[2], shadowColor[3], shadowColor[4] or 1) + love.graphics.print(text, x + shadowOffset, y + shadowOffset) + + -- Draw text + love.graphics.setColor(color[1], color[2], color[3], color[4] or 1) + love.graphics.print(text, x, y) + + -- Reset color + love.graphics.setColor(1, 1, 1, 1) +end + +--- Draw a simple panel/box +-- @param x Panel x coordinate +-- @param y Panel y coordinate +-- @param width Panel width +-- @param height Panel height +-- @param backgroundColor Background color (optional) +-- @param borderColor Border color (optional) +-- @param borderWidth Border width (optional, default: 1) +function UIUtils.drawPanel(x, y, width, height, backgroundColor, borderColor, borderWidth) + borderWidth = borderWidth or 1 + + -- Draw background + if backgroundColor then + love.graphics.setColor(backgroundColor[1], backgroundColor[2], backgroundColor[3], backgroundColor[4] or 1) + love.graphics.rectangle('fill', x, y, width, height) + end + + -- Draw border + if borderColor then + love.graphics.setColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1) + love.graphics.setLineWidth(borderWidth) + love.graphics.rectangle('line', x, y, width, height) + love.graphics.setLineWidth(1) + end + + -- Reset color + love.graphics.setColor(1, 1, 1, 1) +end + +--- Draw centered text +-- @param text Text to draw +-- @param x Center x coordinate +-- @param y Center y coordinate +-- @param color Text color (optional) +function UIUtils.drawCenteredText(text, x, y, color) + local font = love.graphics.getFont() + local textWidth = font:getWidth(text) + local textHeight = font:getHeight() + + if color then + love.graphics.setColor(color[1], color[2], color[3], color[4] or 1) + end + + love.graphics.print(text, x - textWidth / 2, y - textHeight / 2) + + love.graphics.setColor(1, 1, 1, 1) +end + +--- Draw a tooltip +-- @param text Tooltip text +-- @param x Mouse x coordinate +-- @param y Mouse y coordinate +-- @param backgroundColor Background color (optional) +-- @param textColor Text color (optional) +-- @param padding Padding around text (optional, default: 5) +function UIUtils.drawTooltip(text, x, y, backgroundColor, textColor, padding) + backgroundColor = backgroundColor or {0.1, 0.1, 0.1, 0.9} + textColor = textColor or {1, 1, 1, 1} + padding = padding or 5 + + local font = love.graphics.getFont() + local textWidth = font:getWidth(text) + local textHeight = font:getHeight() + + local boxWidth = textWidth + padding * 2 + local boxHeight = textHeight + padding * 2 + + -- Adjust position to keep on screen + local screenWidth = love.graphics.getWidth() + local screenHeight = love.graphics.getHeight() + + if x + boxWidth > screenWidth then + x = screenWidth - boxWidth + end + if y + boxHeight > screenHeight then + y = screenHeight - boxHeight + end + + -- Draw background + love.graphics.setColor(backgroundColor[1], backgroundColor[2], backgroundColor[3], backgroundColor[4] or 1) + love.graphics.rectangle('fill', x, y, boxWidth, boxHeight) + + -- Draw border + love.graphics.setColor(0.3, 0.3, 0.3, 1) + love.graphics.rectangle('line', x, y, boxWidth, boxHeight) + + -- Draw text + love.graphics.setColor(textColor[1], textColor[2], textColor[3], textColor[4] or 1) + love.graphics.print(text, x + padding, y + padding) + + -- Reset color + love.graphics.setColor(1, 1, 1, 1) +end + +return UIUtils diff --git a/src/utils/mathutils.lua b/src/utils/mathutils.lua new file mode 100644 index 0000000..7e0b018 --- /dev/null +++ b/src/utils/mathutils.lua @@ -0,0 +1,109 @@ +--- Math Utility Module +-- Provides common mathematical operations and safe float comparisons + +local MathUtils = {} + +-- Epsilon for float comparison +local EPSILON = 0.0001 + +--- Clamp a value between min and max +-- @param value The value to clamp +-- @param min Minimum value +-- @param max Maximum value +-- @return Clamped value +function MathUtils.clamp(value, min, max) + if value < min then return min end + if value > max then return max end + return value +end + +--- Linear interpolation +-- @param a Start value +-- @param b End value +-- @param t Interpolation factor (0-1) +-- @return Interpolated value +function MathUtils.lerp(a, b, t) + return a + (b - a) * t +end + +--- Get the sign of a number +-- @param x The number +-- @return 1 if positive, -1 if negative, 0 if zero +function MathUtils.sign(x) + if x > 0 then return 1 end + if x < 0 then return -1 end + return 0 +end + +--- Check if two floats are approximately equal +-- @param a First value +-- @param b Second value +-- @param epsilon Optional epsilon value (default: 0.0001) +-- @return true if values are within epsilon of each other +function MathUtils.approximately(a, b, epsilon) + epsilon = epsilon or EPSILON + return math.abs(a - b) < epsilon +end + +--- Check if a float is approximately zero +-- @param x The value to check +-- @param epsilon Optional epsilon value (default: 0.0001) +-- @return true if value is within epsilon of zero +function MathUtils.approximatelyZero(x, epsilon) + epsilon = epsilon or EPSILON + return math.abs(x) < epsilon +end + +--- Round a number to the nearest integer +-- @param x The number to round +-- @return Rounded value +function MathUtils.round(x) + return math.floor(x + 0.5) +end + +--- Round a number to a specific number of decimal places +-- @param x The number to round +-- @param decimals Number of decimal places +-- @return Rounded value +function MathUtils.roundTo(x, decimals) + local mult = 10 ^ decimals + return math.floor(x * mult + 0.5) / mult +end + +--- Map a value from one range to another +-- @param value The value to map +-- @param inMin Input range minimum +-- @param inMax Input range maximum +-- @param outMin Output range minimum +-- @param outMax Output range maximum +-- @return Mapped value +function MathUtils.map(value, inMin, inMax, outMin, outMax) + return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin +end + +--- Wrap a value to a range +-- @param value The value to wrap +-- @param min Range minimum +-- @param max Range maximum +-- @return Wrapped value +function MathUtils.wrap(value, min, max) + local range = max - min + while value < min do + value = value + range + end + while value >= max do + value = value - range + end + return value +end + +--- Check if a value is within a range (inclusive) +-- @param value The value to check +-- @param min Range minimum +-- @param max Range maximum +-- @return true if value is in range +function MathUtils.inRange(value, min, max) + return value >= min and value <= max +end + +return MathUtils diff --git a/src/utils/vector.lua b/src/utils/vector.lua new file mode 100644 index 0000000..da7bbe6 --- /dev/null +++ b/src/utils/vector.lua @@ -0,0 +1,131 @@ +--- Vector Math Utility Module +-- Provides common 2D vector operations to eliminate duplicate math code +-- across the codebase. + +local Vector = {} + +--- Calculate the distance between two points +-- @param x1 First point x coordinate +-- @param y1 First point y coordinate +-- @param x2 Second point x coordinate +-- @param y2 Second point y coordinate +-- @return The Euclidean distance between the two points +function Vector.distance(x1, y1, x2, y2) + local dx = x2 - x1 + local dy = y2 - y1 + return math.sqrt(dx * dx + dy * dy) +end + +--- Calculate the magnitude (length) of a vector +-- @param x Vector x component +-- @param y Vector y component +-- @return The magnitude of the vector +function Vector.magnitude(x, y) + return math.sqrt(x * x + y * y) +end + +--- Normalize a vector to unit length +-- @param x Vector x component +-- @param y Vector y component +-- @return Normalized x and y components (0, 0 if zero vector) +function Vector.normalize(x, y) + local mag = Vector.magnitude(x, y) + if mag == 0 then + return 0, 0 + end + return x / mag, y / mag +end + +--- Calculate the dot product of two vectors +-- @param x1 First vector x component +-- @param y1 First vector y component +-- @param x2 Second vector x component +-- @param y2 Second vector y component +-- @return The dot product +function Vector.dot(x1, y1, x2, y2) + return x1 * x2 + y1 * y2 +end + +--- Calculate the angle between two points +-- @param x1 First point x coordinate +-- @param y1 First point y coordinate +-- @param x2 Second point x coordinate +-- @param y2 Second point y coordinate +-- @return The angle in radians +function Vector.angle(x1, y1, x2, y2) + return math.atan2(y2 - y1, x2 - x1) +end + +--- Calculate the angle of a vector +-- @param x Vector x component +-- @param y Vector y component +-- @return The angle in radians +function Vector.angleOf(x, y) + return math.atan2(y, x) +end + +--- Rotate a vector by an angle +-- @param x Vector x component +-- @param y Vector y component +-- @param angle Rotation angle in radians +-- @return Rotated x and y components +function Vector.rotate(x, y, angle) + local cos = math.cos(angle) + local sin = math.sin(angle) + return x * cos - y * sin, x * sin + y * cos +end + +--- Linear interpolation between two vectors +-- @param x1 Start vector x component +-- @param y1 Start vector y component +-- @param x2 End vector x component +-- @param y2 End vector y component +-- @param t Interpolation factor (0-1) +-- @return Interpolated x and y components +function Vector.lerp(x1, y1, x2, y2, t) + return x1 + (x2 - x1) * t, y1 + (y2 - y1) * t +end + +--- Scale a vector by a scalar +-- @param x Vector x component +-- @param y Vector y component +-- @param scale Scaling factor +-- @return Scaled x and y components +function Vector.scale(x, y, scale) + return x * scale, y * scale +end + +--- Add two vectors +-- @param x1 First vector x component +-- @param y1 First vector y component +-- @param x2 Second vector x component +-- @param y2 Second vector y component +-- @return Sum x and y components +function Vector.add(x1, y1, x2, y2) + return x1 + x2, y1 + y2 +end + +--- Subtract two vectors +-- @param x1 First vector x component +-- @param y1 First vector y component +-- @param x2 Second vector x component +-- @param y2 Second vector y component +-- @return Difference x and y components +function Vector.subtract(x1, y1, x2, y2) + return x1 - x2, y1 - y2 +end + +--- Limit the magnitude of a vector +-- @param x Vector x component +-- @param y Vector y component +-- @param maxMag Maximum magnitude +-- @return Limited x and y components +function Vector.limit(x, y, maxMag) + local mag = Vector.magnitude(x, y) + if mag > maxMag then + return Vector.scale(x, y, maxMag / mag) + end + return x, y +end + +return Vector diff --git a/src/world/dayphase.lua b/src/world/dayphase.lua index aeaf196..5894af3 100644 --- a/src/world/dayphase.lua +++ b/src/world/dayphase.lua @@ -116,7 +116,7 @@ function DayPhase:keypressed(key, worldState) if key == "r" or key == "a" then if self.buildGhost and self.buildGhost:isActive() and self.buildManager and self.depot then local tx, ty = worldState:_ghostTile() - self.buildManager:place(self.buildGhost:currentType(), tx, ty, self.depot) + self.buildManager:place(self.buildGhost:currentType(), tx, ty, self.depot, worldState.inventory) end return end diff --git a/src/world/fogofwar.lua b/src/world/fogofwar.lua index 42304fc..701e9fa 100644 --- a/src/world/fogofwar.lua +++ b/src/world/fogofwar.lua @@ -57,19 +57,17 @@ function FogOfWar:getCachedType(tx, ty) end -- Returns "visible" | "explored" | "hidden" -function FogOfWar:getState(tx, ty) - local key = tileKey(tx, ty) - if self._visible[key] then return "visible" end - if self._explored[key] then return "explored" end - return "hidden" +-- Fog of war disabled: all tiles are always visible. +function FogOfWar:getState(tx, ty) -- luacheck: ignore tx ty + return "visible" end -function FogOfWar:isVisible(tx, ty) - return self._visible[tileKey(tx, ty)] == true +function FogOfWar:isVisible(tx, ty) -- luacheck: ignore tx ty + return true end -function FogOfWar:isExplored(tx, ty) - return self._explored[tileKey(tx, ty)] ~= nil +function FogOfWar:isExplored(tx, ty) -- luacheck: ignore tx ty + return true end FogOfWar.VISION_RADIUS = VISION_RADIUS diff --git a/src/world/worldgen.lua b/src/world/worldgen.lua index 11a2287..f9c81a1 100644 --- a/src/world/worldgen.lua +++ b/src/world/worldgen.lua @@ -36,54 +36,85 @@ end -- ─── Tile selection ────────────────────────────────────────────────────────── -- Returns the tile type string for a given world tile coordinate. --- Two noise layers: --- elevation – large features (water, plains, hills) --- detail – small features (trees, rocks, dirt patches) +-- Uses two separate elevation signals: +-- shore – very smooth (2 octaves, low frequency) for clean water/beach borders +-- elev – full detail (4 octaves) for land biome selection only +-- moist – medium-scale vegetation moisture +-- temp – large-scale temperature (tree species) function WorldGen.getTileAt(tx, ty) - local scale = 0.04 -- lower = larger features + local scale = 0.04 - local elev = octaveNoise(tx * scale, ty * scale, 4, 0.5, 2.0) - local detail = octaveNoise(tx * scale * 3 + 100, ty * scale * 3 + 100, 2, 0.5, 2.0) + -- Smooth, low-frequency boundary used only for water/shore/sand thresholds. + -- Fewer octaves = no small dips that create water islands inside beach zones. + local shore = octaveNoise(tx * scale * 0.7, ty * scale * 0.7, 2, 0.5, 2.0) - -- Water at low elevation - if elev < -0.35 then return "water" end + -- Full-detail elevation for land biome height (highland/rock detection). + local elev = octaveNoise(tx * scale, ty * scale, 4, 0.5, 2.0) - -- Sandy shores - if elev < -0.20 then return "sand" end + local moist = octaveNoise(tx * scale * 0.75 + 200, ty * scale * 0.75 + 200, 3, 0.5, 2.0) + local temp = octaveNoise(tx * scale * 0.5 + 400, ty * scale * 0.5 + 400, 2, 0.6, 2.0) - -- Stone outcrops at high elevation + high detail - if elev > 0.35 and detail > 0.2 then return "rock" end + -- ── Water (decided by smooth shore signal) ──────────────────────────────── + if shore < -0.42 then return "water" end - -- Dense forest in mid-high elevation - if elev > 0.10 and detail > 0.30 then return "tree" end + -- Shore fringe: use temp (slow-varying) for plant patches vs plain water + if shore < -0.28 then + if temp > 0.40 then return "lily_pad" end + if temp < -0.40 then return "cattail" end + return "water" + end + + -- ── Shore ───────────────────────────────────────────────────────────────── + if shore < -0.14 then return "beach" end + if shore < -0.02 then return "sand" end + + -- ── Land (decided by full-detail elev + moist + temp) ──────────────────── + + -- Rocky highlands + if elev > 0.52 then return "rock" end + if elev > 0.42 and moist < 0.0 then return "rock" end + + -- Forest zones driven by moisture bands + if moist > 0.45 then + if elev > 0.05 then + if temp < -0.20 then return "birch" end + return "tree" + end + if temp > 0.30 then return "mushroom" end + return "flower" + end - -- Dirt patches - if detail < -0.25 then return "dirt" end + if moist > 0.15 then + if elev > 0.18 then + if temp < -0.15 then return "birch" end + return "tree" + end + if temp > 0.20 then return "flower" end + return "tall_grass" + end - -- Default: grass + if moist < -0.30 then return "dirt" end return "grass" end -- Returns a resource type string if a resource node should spawn at (tx, ty), -- or nil if no node. Called once when a chunk is first generated. function WorldGen.getResourceAt(tx, ty, tileType) - -- Resources only spawn on specific tile types local resourceMap = { - tree = "wood", - rock = "iron", -- ore underneath rock outcrops + tree = "wood", + birch = "wood", + rock = "iron", } if resourceMap[tileType] then - -- Sparse placement: use a simple hash so it's deterministic local h = (tx * 374761393 + ty * 1234567891) % 100 - if h < 60 then -- 60% chance on eligible tile - return resourceMap[tileType] - end + if h < 60 then return resourceMap[tileType] end end - -- Occasional stone nodes on plain grass/dirt - if tileType == "grass" or tileType == "dirt" then + -- Occasional stone nodes on open ground + if tileType == "grass" or tileType == "dirt" or + tileType == "flower" or tileType == "tall_grass" then local h = (tx * 987654321 + ty * 123456789) % 100 - if h < 3 then return "stone" end -- 3% chance + if h < 3 then return "stone" end end return nil