├── .luacheckrc ├── image.png ├── README.md ├── types.lua ├── game.lua ├── camera.lua ├── tile.lua ├── notification.lua ├── settlement.lua ├── .cursor └── rules │ └── project.mdc ├── main.lua ├── minimap.lua ├── tilemenu.lua ├── resources.lua ├── renderer.lua ├── grid.lua └── player.lua /.luacheckrc: -------------------------------------------------------------------------------- 1 | new_globals = { '_', 'love' } 2 | exclude_files = { '**/.*' } 3 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Civilization-Love2D/main/image.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Civilization lite 2 | 3 | This is a LÖVE-based grid-based civilization game prototype. 4 | Mainly made with Cursor Agent + Vibe coding 5 | 6 | ![Game Screenshot](image.png) 7 | -------------------------------------------------------------------------------- /types.lua: -------------------------------------------------------------------------------- 1 | -- types.lua - Shared type definitions for the game 2 | 3 | -- Tile type enum 4 | TileType = { 5 | PLAINS = "plains", 6 | FOREST = "forest", 7 | MOUNTAIN = "mountain", 8 | WATER = "water" 9 | } 10 | 11 | -- Building type enum 12 | BuildingType = { 13 | FARM = "farm", 14 | MINE = "mine", 15 | MARKET = "market", 16 | FISHERY = "fishery" 17 | } 18 | 19 | -- Resource type enum 20 | ResourceType = { 21 | NONE = "none", 22 | IRON = "iron", 23 | GOLD_ORE = "gold_ore", 24 | HORSES = "horses", 25 | FISH = "fish", 26 | WHEAT = "wheat" 27 | } 28 | 29 | return { 30 | TileType = TileType, 31 | BuildingType = BuildingType, 32 | ResourceType = ResourceType 33 | } 34 | -------------------------------------------------------------------------------- /game.lua: -------------------------------------------------------------------------------- 1 | local love = require("love") 2 | 3 | -- game.lua - Main game state and initialization 4 | 5 | Game = {} 6 | Game.__index = Game 7 | 8 | function Game.new() 9 | local self = setmetatable({}, Game) 10 | self.tileSize = 30 11 | self.gridWidth = 300 12 | self.gridHeight = 300 13 | self.turn = 1 -- Initialize turn counter 14 | 15 | -- Initialize components 16 | self.grid = Grid.new(self.gridWidth, self.gridHeight, self.tileSize) 17 | self.camera = Camera.new(self.gridWidth, self.gridHeight, self.tileSize) 18 | self.ui = { 19 | tileMenu = TileMenu.new() 20 | } 21 | 22 | -- Initialize player 23 | self.player = Player.new(self.grid) 24 | 25 | -- Initialize minimap 26 | self.minimap = Minimap.new(self.grid, self.tileSize) 27 | 28 | -- Set camera to player position 29 | if self.player.gridX and self.player.gridY then 30 | local centerX = (self.player.gridX - 1) * self.tileSize 31 | local centerY = (self.player.gridY - 1) * self.tileSize 32 | self:centerCameraOn(centerX, centerY) 33 | end 34 | 35 | return self 36 | end 37 | 38 | function Game:getTileAtMouse(x, y) 39 | -- Convert screen coordinates to grid coordinates 40 | local gridX, gridY = self.camera:worldToGrid(x + self.camera.x, y + self.camera.y, self.tileSize) 41 | return self.grid:getTileAt(gridX, gridY) 42 | end 43 | 44 | function Game:centerCameraOn(x, y) 45 | -- Center the camera on the given world coordinates 46 | local screenWidth = love.graphics.getWidth() 47 | local screenHeight = love.graphics.getHeight() 48 | 49 | self.camera.x = x - screenWidth / 2 50 | self.camera.y = y - screenHeight / 2 51 | 52 | -- Calculate effective grid size with zoom 53 | local effectiveGridWidth = self.gridWidth * self.tileSize * self.camera.zoomLevel 54 | local effectiveGridHeight = self.gridHeight * self.tileSize * self.camera.zoomLevel 55 | 56 | -- Clamp camera position to grid boundaries 57 | self.camera.x = math.max(0, math.min(self.camera.x, effectiveGridWidth - screenWidth)) 58 | self.camera.y = math.max(0, math.min(self.camera.y, effectiveGridHeight - screenHeight)) 59 | end 60 | 61 | -- Increment the turn counter and handle turn-based updates 62 | function Game:nextTurn() 63 | self.turn = self.turn + 1 64 | 65 | -- Show turn notification 66 | self:showNotification(NotificationType.TURN, "Turn " .. self.turn) 67 | 68 | -- Update player for the new turn 69 | self.player:onTurnEnd() 70 | end 71 | 72 | -- Helper function to show different types of game notifications 73 | function Game:showNotification(notificationType, text, additionalOptions) 74 | local options = additionalOptions or {} 75 | 76 | -- Set default options based on notification type 77 | if notificationType == NotificationType.TURN then 78 | options.color = options.color or {0.7, 0.7, 1.0} -- Light blue for turn notifications 79 | options.position = options.position or NotificationPosition.TOP 80 | elseif notificationType == NotificationType.RESOURCE then 81 | options.color = options.color or {0.2, 0.8, 0.2} -- Green for resource notifications 82 | options.position = options.position or NotificationPosition.MIDDLE 83 | elseif notificationType == NotificationType.WARNING then 84 | options.color = options.color or {0.8, 0.2, 0.2} -- Red for warnings 85 | options.position = options.position or NotificationPosition.TOP 86 | elseif notificationType == NotificationType.ACHIEVEMENT then 87 | options.color = options.color or {1.0, 0.8, 0.2} -- Gold for achievements 88 | options.position = options.position or NotificationPosition.BOTTOM 89 | options.duration = options.duration or 3.0 -- Longer duration for achievements 90 | end 91 | 92 | -- Show the notification using the global notification system 93 | return notificationSystem:show(text, options) 94 | end 95 | 96 | return Game 97 | -------------------------------------------------------------------------------- /camera.lua: -------------------------------------------------------------------------------- 1 | local love = require("love") 2 | 3 | -- camera.lua - Handles camera movement and viewport calculations 4 | 5 | Camera = {} 6 | Camera.__index = Camera 7 | 8 | function Camera.new(gridWidth, gridHeight, tileSize) 9 | local self = setmetatable({}, Camera) 10 | self.x = 0 11 | self.y = 0 12 | self.speed = 500 -- pixels per second 13 | self.gridWidth = gridWidth 14 | self.gridHeight = gridHeight 15 | self.tileSize = tileSize 16 | self.zoomLevel = 1.0 -- Default zoom level (1.0 = 100%) 17 | self.minZoom = 0.5 -- Minimum zoom level (50%) 18 | self.maxZoom = 2.0 -- Maximum zoom level (200%) 19 | self.zoomSpeed = 0.1 -- How much to zoom per mouse wheel movement 20 | return self 21 | end 22 | 23 | function Camera:update(dt) 24 | -- Camera movement with bounds checking 25 | local newX = self.x 26 | local newY = self.y 27 | 28 | if love.keyboard.isDown('left') then 29 | newX = newX - self.speed * dt 30 | end 31 | if love.keyboard.isDown('right') then 32 | newX = newX + self.speed * dt 33 | end 34 | if love.keyboard.isDown('up') then 35 | newY = newY - self.speed * dt 36 | end 37 | if love.keyboard.isDown('down') then 38 | newY = newY + self.speed * dt 39 | end 40 | 41 | -- Calculate effective grid size with zoom 42 | local effectiveGridWidth = self.gridWidth * self.tileSize * self.zoomLevel 43 | local effectiveGridHeight = self.gridHeight * self.tileSize * self.zoomLevel 44 | 45 | -- Clamp camera position to grid boundaries 46 | self.x = math.max(0, math.min(newX, effectiveGridWidth - love.graphics.getWidth())) 47 | self.y = math.max(0, math.min(newY, effectiveGridHeight - love.graphics.getHeight())) 48 | end 49 | 50 | function Camera:zoom(amount) 51 | -- Calculate new zoom level 52 | local newZoom = self.zoomLevel + amount * self.zoomSpeed 53 | 54 | -- Clamp zoom level to min/max values 55 | newZoom = math.max(self.minZoom, math.min(newZoom, self.maxZoom)) 56 | 57 | -- Get mouse position for zoom centering 58 | local mx, my = love.mouse.getPosition() 59 | 60 | -- Calculate world position of mouse before zoom 61 | local worldX = mx + self.x 62 | local worldY = my + self.y 63 | 64 | -- Calculate grid position (stays constant during zoom) 65 | local gridX = worldX / (self.tileSize * self.zoomLevel) 66 | local gridY = worldY / (self.tileSize * self.zoomLevel) 67 | 68 | -- Apply new zoom level 69 | self.zoomLevel = newZoom 70 | 71 | -- Calculate new world position after zoom 72 | local newWorldX = gridX * (self.tileSize * self.zoomLevel) 73 | local newWorldY = gridY * (self.tileSize * self.zoomLevel) 74 | 75 | -- Adjust camera position to keep mouse position fixed on same grid cell 76 | self.x = self.x + (newWorldX - worldX) 77 | self.y = self.y + (newWorldY - worldY) 78 | 79 | -- Ensure camera stays within bounds after zooming 80 | local effectiveGridWidth = self.gridWidth * self.tileSize * self.zoomLevel 81 | local effectiveGridHeight = self.gridHeight * self.tileSize * self.zoomLevel 82 | 83 | self.x = math.max(0, math.min(self.x, effectiveGridWidth - love.graphics.getWidth())) 84 | self.y = math.max(0, math.min(self.y, effectiveGridHeight - love.graphics.getHeight())) 85 | end 86 | 87 | function Camera:getEffectiveTileSize() 88 | return self.tileSize * self.zoomLevel 89 | end 90 | 91 | function Camera:isOnScreen(x, y, width, height) 92 | return x + width > self.x and 93 | x < self.x + love.graphics.getWidth() and 94 | y + height > self.y and 95 | y < self.y + love.graphics.getHeight() 96 | end 97 | 98 | function Camera:worldToGrid(x, y, tileSize) 99 | -- Convert screen coordinates to world coordinates 100 | -- Note: x and y are already in world coordinates if they include camera.x and camera.y 101 | local effectiveTileSize = tileSize * self.zoomLevel 102 | local gridX = math.floor(x / effectiveTileSize) + 1 103 | local gridY = math.floor(y / effectiveTileSize) + 1 104 | return gridX, gridY 105 | end 106 | 107 | return Camera 108 | -------------------------------------------------------------------------------- /tile.lua: -------------------------------------------------------------------------------- 1 | require("types") 2 | require("buildings") 3 | require("resources") 4 | 5 | -- Tile definitions 6 | TileDefinitions = { 7 | types = { 8 | [TileType.PLAINS] = { 9 | name = "Plains", 10 | color = {0.7, 0.9, 0.4}, 11 | bonuses = {food = 2, production = 1, gold = 0}, 12 | maxBuildings = 2, 13 | allowedBuildings = {BuildingType.FARM, BuildingType.MARKET} 14 | }, 15 | [TileType.FOREST] = { 16 | name = "Forest", 17 | color = {0.2, 0.5, 0.2}, 18 | bonuses = {food = 1, production = 2, gold = 0}, 19 | maxBuildings = 1, 20 | allowedBuildings = {BuildingType.MINE, BuildingType.MARKET} 21 | }, 22 | [TileType.MOUNTAIN] = { 23 | name = "Mountain", 24 | color = {0.5, 0.5, 0.5}, 25 | bonuses = {food = 0, production = 3, gold = 0}, 26 | maxBuildings = 1, 27 | allowedBuildings = {BuildingType.MINE} 28 | }, 29 | [TileType.WATER] = { 30 | name = "Water", 31 | color = {0.3, 0.6, 0.9}, 32 | bonuses = {food = 2, production = 0, gold = 1}, 33 | maxBuildings = 1, 34 | allowedBuildings = {BuildingType.FISHERY, BuildingType.MARKET} 35 | } 36 | } 37 | } 38 | 39 | -- Resource definitions 40 | ResourceDefinitions = { 41 | types = { 42 | [ResourceType.IRON] = { 43 | name = "Iron", 44 | bonuses = {food = 0, production = 2, gold = 1}, 45 | validTiles = {TileType.MOUNTAIN, TileType.PLAINS}, 46 | rarity = 0.1 -- 10% chance on valid tiles 47 | }, 48 | [ResourceType.GOLD_ORE] = { 49 | name = "Gold Ore", 50 | bonuses = {food = 0, production = 1, gold = 3}, 51 | validTiles = {TileType.MOUNTAIN}, 52 | rarity = 0.05 -- 5% chance on valid tiles 53 | }, 54 | [ResourceType.HORSES] = { 55 | name = "Horses", 56 | bonuses = {food = 1, production = 1, gold = 1}, 57 | validTiles = {TileType.PLAINS}, 58 | rarity = 0.15 -- 15% chance on valid tiles 59 | }, 60 | [ResourceType.FISH] = { 61 | name = "Fish", 62 | bonuses = {food = 3, production = 0, gold = 1}, 63 | validTiles = {TileType.WATER}, 64 | rarity = 0.2 -- 20% chance on valid tiles 65 | }, 66 | [ResourceType.WHEAT] = { 67 | name = "Wheat", 68 | bonuses = {food = 3, production = 0, gold = 1}, 69 | validTiles = {TileType.PLAINS}, 70 | rarity = 0.2 -- 20% chance on valid tiles 71 | } 72 | } 73 | } 74 | 75 | -- Tile class 76 | Tile = {} 77 | Tile.__index = Tile 78 | 79 | function Tile.new(type) 80 | local self = setmetatable({}, Tile) 81 | self.type = type 82 | self.buildings = {} 83 | self.resource = ResourceType.NONE 84 | return self 85 | end 86 | 87 | function Tile:canAddBuilding(buildingType) 88 | local buildings = Buildings.new() 89 | return buildings:canAddToTile(self, buildingType) 90 | end 91 | 92 | function Tile:addBuilding(buildingType) 93 | local buildings = Buildings.new() 94 | return buildings:addToTile(self, buildingType) 95 | end 96 | 97 | function Tile:setResource(resourceType) 98 | self.resource = resourceType 99 | end 100 | 101 | function Tile:getTotalYield() 102 | local tileDef = TileDefinitions.types[self.type] 103 | local yield = {food = tileDef.bonuses.food, production = tileDef.bonuses.production, gold = tileDef.bonuses.gold} 104 | 105 | -- Add resource bonuses if present 106 | if self.resource ~= ResourceType.NONE then 107 | local resources = Resources.new() 108 | local resourceBonuses = resources:getBonuses(self.resource) 109 | yield.food = yield.food + resourceBonuses.food 110 | yield.production = yield.production + resourceBonuses.production 111 | yield.gold = yield.gold + resourceBonuses.gold 112 | end 113 | 114 | -- Add building bonuses 115 | local buildings = Buildings.new() 116 | local buildingBonuses = buildings:getBonuses(self) 117 | yield.food = yield.food + buildingBonuses.food 118 | yield.production = yield.production + buildingBonuses.production 119 | yield.gold = yield.gold + buildingBonuses.gold 120 | 121 | return yield 122 | end 123 | -------------------------------------------------------------------------------- /notification.lua: -------------------------------------------------------------------------------- 1 | local love = require("love") 2 | 3 | -- notification.lua - Handles game notifications 4 | 5 | -- Enum for notification positions 6 | NotificationPosition = { 7 | TOP = "top", 8 | MIDDLE = "middle", 9 | BOTTOM = "bottom" 10 | } 11 | 12 | -- Enum for notification types 13 | NotificationType = { 14 | TURN = "turn", 15 | RESOURCE = "resource", 16 | WARNING = "warning", 17 | ACHIEVEMENT = "achievement" 18 | } 19 | 20 | Notification = {} 21 | Notification.__index = Notification 22 | 23 | function Notification.new() 24 | local self = setmetatable({}, Notification) 25 | 26 | -- Queue of active notifications 27 | self.notifications = {} 28 | 29 | -- Default settings 30 | self.defaultDuration = 1.5 31 | self.maxNotifications = 3 -- Maximum number of notifications to show at once 32 | self.padding = 16 -- Padding around text (in pixels) 33 | self.minWidth = 120 -- Minimum width for notifications 34 | 35 | return self 36 | end 37 | 38 | -- Add a new notification to the queue 39 | function Notification:show(text, options) 40 | options = options or {} 41 | 42 | -- Calculate width based on text length 43 | local font = love.graphics.getFont() 44 | local textWidth = font:getWidth(text) 45 | local textHeight = font:getHeight() 46 | 47 | -- Width is text width plus padding on both sides, with a minimum width 48 | local width = math.max(self.minWidth, textWidth + (self.padding * 2)) 49 | -- Height is text height plus padding on both sides 50 | local height = textHeight + (self.padding * 2) 51 | 52 | local notification = { 53 | text = text, 54 | timer = options.duration or self.defaultDuration, 55 | duration = options.duration or self.defaultDuration, 56 | color = options.color or {0.7, 0.7, 1.0}, -- Default to light blue 57 | backgroundColor = options.backgroundColor or {0, 0, 0, 0.7}, 58 | position = options.position or NotificationPosition.TOP, 59 | width = options.width or width, 60 | height = options.height or height, 61 | active = true, 62 | textWidth = textWidth, -- Store text dimensions for centering 63 | textHeight = textHeight 64 | } 65 | 66 | -- Add to the queue 67 | table.insert(self.notifications, notification) 68 | 69 | -- Limit the number of notifications 70 | while #self.notifications > self.maxNotifications do 71 | table.remove(self.notifications, 1) 72 | end 73 | 74 | return notification 75 | end 76 | 77 | -- Update all active notifications 78 | function Notification:update(dt) 79 | local i = 1 80 | while i <= #self.notifications do 81 | local notification = self.notifications[i] 82 | 83 | notification.timer = notification.timer - dt 84 | 85 | if notification.timer <= 0 then 86 | table.remove(self.notifications, i) 87 | else 88 | i = i + 1 89 | end 90 | end 91 | end 92 | 93 | -- Draw all active notifications 94 | function Notification:draw() 95 | local screenWidth = love.graphics.getWidth() 96 | local screenHeight = love.graphics.getHeight() 97 | 98 | for i, notification in ipairs(self.notifications) do 99 | -- Calculate position based on notification settings 100 | local x = screenWidth / 2 - notification.width / 2 101 | local y 102 | 103 | if notification.position == NotificationPosition.TOP then 104 | y = 50 + (i - 1) * (notification.height + 10) 105 | elseif notification.position == NotificationPosition.BOTTOM then 106 | y = screenHeight - 50 - notification.height - (i - 1) * (notification.height + 10) 107 | else -- middle 108 | y = screenHeight / 2 - notification.height / 2 109 | end 110 | 111 | -- Calculate alpha based on remaining time 112 | local alpha = math.min(1, notification.timer / (notification.duration / 2)) 113 | if notification.timer < notification.duration / 2 then 114 | alpha = notification.timer / (notification.duration / 2) 115 | end 116 | 117 | -- Draw background 118 | love.graphics.setColor( 119 | notification.backgroundColor[1], 120 | notification.backgroundColor[2], 121 | notification.backgroundColor[3], 122 | (notification.backgroundColor[4] or 1) * alpha 123 | ) 124 | love.graphics.rectangle("fill", x, y, notification.width, notification.height) 125 | 126 | -- Draw border 127 | love.graphics.setColor( 128 | notification.color[1], 129 | notification.color[2], 130 | notification.color[3], 131 | alpha 132 | ) 133 | love.graphics.rectangle("line", x, y, notification.width, notification.height) 134 | 135 | -- Use stored text dimensions for perfect centering 136 | local textX = x + notification.width/2 - notification.textWidth/2 137 | local textY = y + notification.height/2 - notification.textHeight/2 138 | 139 | -- Draw text shadow 140 | love.graphics.setColor(0, 0, 0, 0.5 * alpha) 141 | love.graphics.print(notification.text, textX + 1, textY + 1) 142 | 143 | -- Draw text 144 | love.graphics.setColor(1, 1, 1, alpha) 145 | love.graphics.print(notification.text, textX, textY) 146 | end 147 | 148 | -- Reset color 149 | love.graphics.setColor(1, 1, 1, 1) 150 | end 151 | 152 | return Notification 153 | -------------------------------------------------------------------------------- /settlement.lua: -------------------------------------------------------------------------------- 1 | -- settlement.lua - Handles player settlements and their borders 2 | 3 | require("tile") 4 | local love = require("love") 5 | 6 | Settlement = {} 7 | Settlement.__index = Settlement 8 | 9 | function Settlement.new(x, y, owner) 10 | local self = setmetatable({}, Settlement) 11 | 12 | -- Settlement position (center tile) 13 | self.centerX = x 14 | self.centerY = y 15 | 16 | -- Settlement owner 17 | self.owner = owner 18 | 19 | -- Settlement name (could be randomly generated in the future) 20 | self.name = "Settlement " .. math.random(1, 1000) 21 | 22 | -- Settlement border radius (initially 1 for a 2x2 area) 23 | self.borderRadius = 1 24 | 25 | -- Settlement tiles (will be populated in calculateBorder) 26 | self.tiles = {} 27 | 28 | -- Calculate initial border 29 | self:calculateBorder() 30 | 31 | return self 32 | end 33 | 34 | -- Calculate which tiles are within the settlement's border 35 | function Settlement:calculateBorder() 36 | self.tiles = {} 37 | 38 | -- Calculate the range of tiles within the border 39 | local startX = self.centerX - self.borderRadius 40 | local endX = self.centerX + self.borderRadius 41 | local startY = self.centerY - self.borderRadius 42 | local endY = self.centerY + self.borderRadius 43 | 44 | -- Add all tiles within the border to the settlement 45 | for y = startY, endY do 46 | for x = startX, endX do 47 | table.insert(self.tiles, {x = x, y = y}) 48 | end 49 | end 50 | end 51 | 52 | -- Check if a tile is within the settlement's border 53 | function Settlement:containsTile(x, y) 54 | for _, tile in ipairs(self.tiles) do 55 | if tile.x == x and tile.y == y then 56 | return true 57 | end 58 | end 59 | return false 60 | end 61 | 62 | -- Calculate the total yields of this settlement 63 | function Settlement:calculateYields(grid) 64 | local yields = { 65 | food = 0, 66 | production = 0, 67 | gold = 0 68 | } 69 | 70 | for _, tilePos in ipairs(self.tiles) do 71 | -- Make sure the tile position is valid 72 | if tilePos.x >= 1 and tilePos.x <= grid.width and 73 | tilePos.y >= 1 and tilePos.y <= grid.height then 74 | 75 | local tile = grid.tiles[tilePos.y][tilePos.x] 76 | 77 | -- Get the total yield from this tile 78 | local tileYield = tile:getTotalYield() 79 | 80 | -- Add tile yields to settlement yields 81 | yields.food = yields.food + tileYield.food 82 | yields.production = yields.production + tileYield.production 83 | yields.gold = yields.gold + tileYield.gold 84 | end 85 | end 86 | 87 | return yields 88 | end 89 | 90 | -- Draw the settlement border 91 | function Settlement:draw(tileSize, grid) 92 | -- Draw border for each tile in the settlement 93 | for _, tile in ipairs(self.tiles) do 94 | local screenX = (tile.x - 1) * tileSize 95 | local screenY = (tile.y - 1) * tileSize 96 | 97 | -- Draw a border around the tile 98 | love.graphics.setColor(1, 1, 1, 0.5) -- White semi-transparent 99 | love.graphics.setLineWidth(3) 100 | love.graphics.rectangle("line", screenX, screenY, tileSize, tileSize) 101 | 102 | -- Draw corner markers to make the border more visible 103 | local markerSize = math.max(3, tileSize / 6) -- Scale marker size with tile size 104 | love.graphics.setColor(1, 1, 1, 0.8) -- Brighter white for corners 105 | 106 | -- Top-left corner 107 | love.graphics.rectangle("fill", screenX, screenY, markerSize, markerSize) 108 | -- Top-right corner 109 | love.graphics.rectangle("fill", screenX + tileSize - markerSize, screenY, markerSize, markerSize) 110 | -- Bottom-left corner 111 | love.graphics.rectangle("fill", screenX, screenY + tileSize - markerSize, markerSize, markerSize) 112 | -- Bottom-right corner 113 | love.graphics.rectangle("fill", screenX + tileSize - markerSize, screenY + tileSize - markerSize, markerSize, markerSize) 114 | end 115 | 116 | -- Draw settlement center marker 117 | local centerScreenX = (self.centerX - 1) * tileSize 118 | local centerScreenY = (self.centerY - 1) * tileSize 119 | 120 | love.graphics.setColor(1, 0.8, 0.2, 0.8) -- Gold color for settlement center 121 | love.graphics.circle("fill", centerScreenX + tileSize/2, centerScreenY + tileSize/2, tileSize/4) 122 | 123 | -- Draw settlement name 124 | love.graphics.setColor(1, 1, 1, 0.9) 125 | local fontSize = math.max(10, tileSize / 3) -- Scale font size with tile size 126 | love.graphics.print(self.name, centerScreenX, centerScreenY - fontSize/2, 0, fontSize/12, fontSize/12) 127 | 128 | -- Calculate and display settlement yields if grid is provided 129 | if grid then 130 | local yields = self:calculateYields(grid) 131 | local yieldText = string.format("F:%d P:%d G:%d", yields.food, yields.production, yields.gold) 132 | love.graphics.setColor(1, 1, 1, 0.9) 133 | love.graphics.print(yieldText, centerScreenX, centerScreenY - fontSize, 0, fontSize/12, fontSize/12) 134 | 135 | -- Display tile count 136 | local tileCountText = string.format("Tiles: %d", #self.tiles) 137 | love.graphics.print(tileCountText, centerScreenX, centerScreenY - fontSize*1.5, 0, fontSize/12, fontSize/12) 138 | end 139 | 140 | -- Reset color and line width 141 | love.graphics.setColor(1, 1, 1) 142 | love.graphics.setLineWidth(1) 143 | end 144 | 145 | return Settlement 146 | -------------------------------------------------------------------------------- /.cursor/rules/project.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: This is how the project is structured and what the game is about 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # Civilization Grid Prototype - Project Overview 7 | 8 | ## Project Structure 9 | This is a LÖVE-based grid-based civilization game prototype with the following key components: 10 | 11 | 1. **Main Game Files**: 12 | - `main.lua`: Entry point, handles game loop, input, and rendering 13 | - `game.lua`: Main game state and initialization 14 | - `player.lua`: Player entity, movement, and UI 15 | 16 | 2. **Grid System**: 17 | - `grid.lua`: Grid generation and management 18 | - `tile.lua`: Tile types, properties, and yields 19 | 20 | 3. **Game Mechanics**: 21 | - `buildings.lua`: Building types and effects 22 | - `resources.lua`: Resource types and bonuses 23 | - `types.lua`: Enums and type definitions 24 | - `notification.lua`: Notification system for game events 25 | 26 | 4. **UI and Rendering**: 27 | - `renderer.lua`: Handles drawing of game elements 28 | - `tilemenu.lua`: UI for tile interaction 29 | - `camera.lua`: Camera movement and viewport calculations 30 | - `settlement.lua`: Settlement UI and management 31 | 32 | ## Key Concepts 33 | 34 | ### Grid System 35 | - The game world is a grid of tiles (300x300) 36 | - Each tile has a type (Plains, Forest, Mountain, Water) 37 | - Tiles can contain resources and buildings 38 | - Tiles provide yields (Food, Production, Gold) 39 | 40 | ### Buildings and Resources 41 | - Buildings can be placed on tiles with restrictions based on tile type 42 | - Resources spawn on tiles based on rarity and valid tile types 43 | - Both buildings and resources provide yield bonuses 44 | 45 | ### UI System 46 | - Top bar shows player yields 47 | - Tile menu allows interaction with tiles 48 | - Tooltips show information about tiles 49 | - End Turn button allows manual turn advancement 50 | - Notification system provides feedback for game events 51 | 52 | ### Notification System 53 | - Modular system for displaying game events to the player 54 | - Different notification types (Turn, Resource, Warning, Achievement) with distinct styling 55 | - Notifications can be positioned at top, middle, or bottom of screen 56 | - Dynamic sizing based on text content with minimum width 57 | - Fade-in and fade-out animations with configurable duration 58 | - Text shadow effect for improved readability 59 | - Notification queue with maximum limit to prevent screen clutter 60 | - Test mode available by pressing 'T' key 61 | 62 | ## Code Patterns 63 | - Classes use the Lua metatables pattern with `Class.new()` constructors 64 | - Game state is centralized in the `game` global variable 65 | - LÖVE callbacks (`love.load`, `love.update`, `love.draw`, etc.) handle the game loop 66 | - Rendering is done with a camera translation system 67 | - Enums used for type safety (NotificationType, NotificationPosition) 68 | 69 | ### Turn System 70 | - Game progresses through turns, tracked by a turn counter 71 | - Player movement triggers turn advancement 72 | - End Turn button allows manual turn advancement 73 | - Turn changes trigger notifications 74 | - Player resources and yields update at the end of each turn 75 | 76 | ### Player System 77 | - Player has a position on the grid 78 | - Player can move between non-water tiles using WASD keys 79 | - Player has yields (Food, Production, Gold) displayed in a UI bar 80 | - Camera centers on player position 81 | - Player has a sight range (4 tiles in each direction) 82 | - Player maintains their own list of discovered tiles 83 | - Each player has their own fog of war 84 | - Undiscovered tiles appear as dark gray (fog of war) 85 | - Player can create settlements on non-water tiles 86 | - Settlements have a 2x2 tile border initially 87 | - Buildings can only be constructed within settlement borders 88 | - Player receives notifications for important events 89 | 90 | ### Settlements System 91 | - Players can found settlements on non-water tiles 92 | - Settlements have a center position and a border radius 93 | - Initial settlements have a border radius of 1 (covering a 2x2 area) 94 | - Settlements cannot be founded within 4 tiles of an existing settlement 95 | - Each settlement has a unique name (currently randomly generated) 96 | - Settlements provide visual borders around controlled tiles 97 | - Only tiles within settlement borders contribute to player yields 98 | - Buildings can only be constructed on tiles within settlement bordersx 99 | - Each settlement displays its name, yields, and tile count 100 | - Settlement centers are marked with a gold circle 101 | - Settlement borders are displayed with white semi-transparent lines 102 | - The UI shows the total number of settlements and controlled tiles 103 | - Founding a new settlement triggers an achievement notification 104 | 105 | ### Minimap System 106 | - Located in the bottom-left corner of the screen 107 | - Shows a scaled-down view of the discovered world 108 | - Uses the same color scheme as the main map for consistency 109 | - Displays player position with a red marker 110 | - Shows settlements with gold markers 111 | - Indicates current viewport with a white rectangle 112 | - Allows clicking to navigate to different areas of the map 113 | - Only shows tiles that have been discovered by the player 114 | - Can be toggled on/off by pressing the 'M' key 115 | - Provides navigation notifications when clicking on the minimap 116 | 117 | ## Development Notes 118 | - The game automatically runs when files are saved 119 | - No need to run from CLI, just save the files to see changes 120 | - Player movement is handled with WASD keys 121 | - Camera movement is handled with arrow keys 122 | - Escape key quits the game or closes menus 123 | - Press 'T' to test the notification system with various message lengths -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | -- Import modules 2 | require("types") 3 | require("tile") 4 | require("grid") 5 | require("tilemenu") 6 | require("camera") 7 | require("renderer") 8 | require("game") 9 | require("buildings") 10 | require("resources") 11 | require("player") 12 | require("settlement") 13 | require("notification") 14 | require("minimap") 15 | local love = require("love") 16 | 17 | -- LÖVE callbacks 18 | function love.load() 19 | love.window.setTitle("Civilization Grid Prototype") 20 | love.window.setMode(800, 600, { 21 | resizable = true, -- Allow window resizing 22 | minwidth = 400, -- Minimum window width 23 | minheight = 300 -- Minimum window height 24 | }) 25 | 26 | -- Initialize notification system 27 | notificationSystem = Notification.new() 28 | 29 | game = Game.new() 30 | 31 | -- Show a welcome notification 32 | game:showNotification(NotificationType.ACHIEVEMENT, "Welcome to Civilization Grid Prototype!") 33 | 34 | -- Test notifications with different text lengths (for debugging) 35 | -- testNotifications() 36 | end 37 | 38 | function love.update(dt) 39 | game.camera:update(dt) 40 | 41 | -- Update player animation 42 | game.player:update(dt, game.grid) 43 | 44 | -- Update notifications 45 | notificationSystem:update(dt) 46 | end 47 | 48 | function love.draw() 49 | love.graphics.push() 50 | love.graphics.translate(-game.camera.x, -game.camera.y) 51 | 52 | local renderer = Renderer.new() 53 | renderer:drawGrid(game.grid, game.camera, game.tileSize, game.player) 54 | 55 | -- Draw player settlements 56 | game.player:drawSettlements(game.tileSize, game.grid) 57 | 58 | -- Draw player on map 59 | game.player:drawOnMap(game.tileSize) 60 | 61 | love.graphics.pop() 62 | 63 | -- Only show tooltip if menu is not visible 64 | if not game.ui.tileMenu.visible then 65 | local mx, my = love.mouse.getPosition() 66 | local tile = game:getTileAtMouse(mx, my) 67 | if tile then 68 | -- Get the grid coordinates 69 | local gridX, gridY = game.camera:worldToGrid(mx + game.camera.x, my + game.camera.y, game.tileSize) 70 | 71 | if game.player:isTileDiscovered(gridX, gridY) then 72 | renderer:drawTooltip(tile, mx, my, game.player, gridX, gridY) 73 | end 74 | end 75 | end 76 | 77 | game.ui.tileMenu:draw() 78 | 79 | -- Draw player UI (should be drawn last to appear on top) 80 | game.player:draw() 81 | 82 | -- Draw minimap 83 | game.minimap:draw(game.player, game.camera) 84 | 85 | -- Draw notifications 86 | notificationSystem:draw() 87 | end 88 | 89 | function love.mousepressed(x, y, button) 90 | if button == 1 then -- Left mouse button 91 | -- Check if the click was handled by the minimap 92 | if game.minimap:handleMousePress(x, y, game) then 93 | return 94 | end 95 | 96 | -- Check if the click was handled by the player UI (End Turn button) 97 | if game.player:handleMousePress(x, y) then 98 | return 99 | end 100 | 101 | -- Check if the click was handled by the tile menu 102 | if game.ui.tileMenu:handleMousePress(x, y) then 103 | return 104 | end 105 | 106 | -- Otherwise, show the menu for the clicked tile 107 | local tile = game:getTileAtMouse(x, y) 108 | if tile then 109 | local gridX, gridY = game.camera:worldToGrid(x + game.camera.x, y + game.camera.y, game.tileSize) 110 | if game.player:isTileDiscovered(gridX, gridY) then 111 | game.ui.tileMenu:showForTile(tile, x, y) 112 | end 113 | end 114 | end 115 | end 116 | 117 | function love.keypressed(key) 118 | if key == "escape" then 119 | -- Check if the key was handled by the tile menu 120 | if game.ui.tileMenu:handleEscape() then 121 | return 122 | end 123 | 124 | -- Otherwise, quit the game 125 | love.event.quit() 126 | elseif key == "t" then 127 | -- Test notifications when pressing T 128 | testNotifications() 129 | elseif key == "m" then 130 | -- Toggle minimap visibility when pressing M 131 | game.minimap.visible = not game.minimap.visible 132 | 133 | -- Show notification about minimap toggle 134 | if game.minimap.visible then 135 | game:showNotification(NotificationType.RESOURCE, "Minimap enabled") 136 | else 137 | game:showNotification(NotificationType.RESOURCE, "Minimap disabled") 138 | end 139 | end 140 | 141 | -- Player movement with WASD keys 142 | local playerMoved = false 143 | 144 | if key == "z" then -- Move up 145 | playerMoved = game.player:moveTo(game.player.gridX, game.player.gridY - 1, game.grid) 146 | elseif key == "s" then -- Move down 147 | playerMoved = game.player:moveTo(game.player.gridX, game.player.gridY + 1, game.grid) 148 | elseif key == "q" then -- Move left 149 | playerMoved = game.player:moveTo(game.player.gridX - 1, game.player.gridY, game.grid) 150 | elseif key == "d" then -- Move right 151 | playerMoved = game.player:moveTo(game.player.gridX + 1, game.player.gridY, game.grid) 152 | end 153 | 154 | -- Camera centering is now handled in the player's update function 155 | end 156 | 157 | -- Add window resize callback 158 | function love.resize(w, h) 159 | -- You can add any additional resize logic here if needed 160 | -- For example, adjusting UI elements or camera bounds 161 | end 162 | 163 | -- Add mouse wheel callback for zooming 164 | function love.wheelmoved(x, y) 165 | if y ~= 0 then 166 | game.camera:zoom(y) 167 | end 168 | end 169 | 170 | -- Function to test notifications with different text lengths 171 | function testNotifications() 172 | -- Short text 173 | game:showNotification(NotificationType.TURN, "Turn 1") 174 | 175 | -- Medium text 176 | game:showNotification(NotificationType.RESOURCE, "Found gold near your settlement!") 177 | 178 | -- Long text 179 | game:showNotification(NotificationType.WARNING, "Enemy units approaching from the north! Prepare your defenses immediately.") 180 | end 181 | -------------------------------------------------------------------------------- /minimap.lua: -------------------------------------------------------------------------------- 1 | local love = require("love") 2 | 3 | Minimap = {} 4 | Minimap.__index = Minimap 5 | 6 | function Minimap.new(grid, tileSize) 7 | local self = setmetatable({}, Minimap) 8 | 9 | -- Configuration 10 | self.width = 150 -- Width of minimap in pixels 11 | self.height = 150 -- Height of minimap in pixels 12 | self.padding = 10 -- Padding from screen edge 13 | self.borderWidth = 2 -- Border thickness 14 | self.grid = grid 15 | self.originalTileSize = tileSize 16 | self.visible = true -- Whether the minimap is visible 17 | 18 | -- Calculate the scale factor for the minimap 19 | self.scaleFactor = math.min( 20 | self.width / (grid.width * tileSize), 21 | self.height / (grid.height * tileSize) 22 | ) 23 | 24 | -- Size of each tile on the minimap 25 | self.minimapTileSize = tileSize * self.scaleFactor 26 | 27 | return self 28 | end 29 | 30 | function Minimap:draw(player, camera) 31 | -- Skip drawing if not visible 32 | if not self.visible then 33 | return 34 | end 35 | 36 | local screenWidth = love.graphics.getWidth() 37 | local screenHeight = love.graphics.getHeight() 38 | 39 | -- Position in bottom left 40 | local x = self.padding 41 | local y = screenHeight - self.height - self.padding 42 | 43 | -- Draw background and border 44 | love.graphics.setColor(0, 0, 0, 0.7) 45 | love.graphics.rectangle("fill", x, y, self.width, self.height) 46 | love.graphics.setColor(0.5, 0.5, 0.5, 1) 47 | love.graphics.rectangle("line", x, y, self.width, self.height) 48 | 49 | -- Draw the grid tiles 50 | for gridY = 1, self.grid.height do 51 | for gridX = 1, self.grid.width do 52 | -- Only draw if the tile is discovered by the player 53 | if player:isTileDiscovered(gridX, gridY) then 54 | local tile = self.grid.tiles[gridY][gridX] 55 | 56 | -- Calculate position on minimap 57 | local tileX = x + (gridX - 1) * self.minimapTileSize 58 | local tileY = y + (gridY - 1) * self.minimapTileSize 59 | 60 | -- Set color based on tile type 61 | if tile.type == TileType.WATER then 62 | love.graphics.setColor(0.2, 0.4, 0.8, 0.8) 63 | elseif tile.type == TileType.PLAINS then 64 | love.graphics.setColor(0.5, 0.8, 0.2, 0.8) 65 | elseif tile.type == TileType.FOREST then 66 | love.graphics.setColor(0.1, 0.6, 0.1, 0.8) 67 | elseif tile.type == TileType.MOUNTAIN then 68 | love.graphics.setColor(0.6, 0.6, 0.6, 0.8) 69 | end 70 | 71 | -- Draw the tile 72 | love.graphics.rectangle("fill", tileX, tileY, self.minimapTileSize, self.minimapTileSize) 73 | end 74 | end 75 | end 76 | 77 | -- Draw settlements 78 | for _, settlement in ipairs(player.settlements) do 79 | local settlementX = x + (settlement.centerX - 1) * self.minimapTileSize 80 | local settlementY = y + (settlement.centerY - 1) * self.minimapTileSize 81 | 82 | -- Draw settlement marker 83 | love.graphics.setColor(1, 0.8, 0.2, 1) -- Gold color 84 | love.graphics.circle("fill", 85 | settlementX + self.minimapTileSize/2, 86 | settlementY + self.minimapTileSize/2, 87 | self.minimapTileSize * 1.5) 88 | end 89 | 90 | -- Draw player position 91 | local playerX = x + (player.visualX - 1) * self.minimapTileSize 92 | local playerY = y + (player.visualY - 1) * self.minimapTileSize 93 | 94 | love.graphics.setColor(1, 0, 0, 1) -- Red color 95 | love.graphics.circle("fill", 96 | playerX + self.minimapTileSize/2, 97 | playerY + self.minimapTileSize/2, 98 | self.minimapTileSize) 99 | 100 | -- Draw viewport rectangle - FIXED to properly handle zoom 101 | local effectiveTileSize = self.originalTileSize * camera.zoomLevel 102 | 103 | -- Calculate the viewport in grid coordinates 104 | local viewportStartGridX = camera.x / effectiveTileSize 105 | local viewportStartGridY = camera.y / effectiveTileSize 106 | local viewportWidthInTiles = love.graphics.getWidth() / effectiveTileSize 107 | local viewportHeightInTiles = love.graphics.getHeight() / effectiveTileSize 108 | 109 | -- Convert to minimap coordinates 110 | local viewportX = x + viewportStartGridX * self.minimapTileSize 111 | local viewportY = y + viewportStartGridY * self.minimapTileSize 112 | local viewportWidth = viewportWidthInTiles * self.minimapTileSize 113 | local viewportHeight = viewportHeightInTiles * self.minimapTileSize 114 | 115 | love.graphics.setColor(1, 1, 1, 0.5) 116 | love.graphics.rectangle("line", viewportX, viewportY, viewportWidth, viewportHeight) 117 | 118 | -- Reset color 119 | love.graphics.setColor(1, 1, 1, 1) 120 | end 121 | 122 | -- Handle mouse clicks on the minimap 123 | function Minimap:handleMousePress(x, y, game) 124 | -- Skip if not visible 125 | if not self.visible then 126 | return false 127 | end 128 | 129 | local screenHeight = love.graphics.getHeight() 130 | local minimapX = self.padding 131 | local minimapY = screenHeight - self.height - self.padding 132 | 133 | -- Check if click is within minimap bounds 134 | if x >= minimapX and x <= minimapX + self.width and 135 | y >= minimapY and y <= minimapY + self.height then 136 | 137 | -- Convert click position to grid coordinates 138 | local gridX = math.floor((x - minimapX) / self.minimapTileSize) + 1 139 | local gridY = math.floor((y - minimapY) / self.minimapTileSize) + 1 140 | 141 | -- Ensure coordinates are within grid bounds 142 | gridX = math.max(1, math.min(gridX, game.grid.width)) 143 | gridY = math.max(1, math.min(gridY, game.grid.height)) 144 | 145 | -- Center camera on clicked position, accounting for zoom 146 | local worldX = (gridX - 0.5) * game.tileSize 147 | local worldY = (gridY - 0.5) * game.tileSize 148 | game:centerCameraOn(worldX, worldY) 149 | 150 | -- Show a notification about the navigation 151 | game:showNotification(NotificationType.RESOURCE, "Navigating to position (" .. gridX .. ", " .. gridY .. ")") 152 | 153 | return true 154 | end 155 | 156 | return false 157 | end 158 | 159 | return Minimap 160 | -------------------------------------------------------------------------------- /tilemenu.lua: -------------------------------------------------------------------------------- 1 | local love = require("love") 2 | 3 | -- tilemenu.lua - Handles the tile interaction menu system 4 | 5 | -- Menu level enum 6 | MenuLevel = { 7 | MAIN = "main", 8 | BUILD = "build" 9 | } 10 | 11 | -- TileMenu class 12 | TileMenu = {} 13 | TileMenu.__index = TileMenu 14 | 15 | -- Initialize the tile menu system 16 | function TileMenu.new() 17 | local self = setmetatable({}, TileMenu) 18 | self.visible = false 19 | self.options = {} 20 | self.x = 0 21 | self.y = 0 22 | self.width = 200 23 | self.optionHeight = 25 24 | self.currentLevel = MenuLevel.MAIN 25 | self.buildOptions = {} 26 | self.selectedTile = nil 27 | self.selectedGridX = nil 28 | self.selectedGridY = nil 29 | return self 30 | end 31 | 32 | -- Draw the menu 33 | function TileMenu:draw() 34 | if not self.visible then 35 | return 36 | end 37 | 38 | local options = self.currentLevel == MenuLevel.MAIN and self.options or self.buildOptions 39 | local totalHeight = #options * self.optionHeight 40 | 41 | -- Draw menu background 42 | love.graphics.setColor(0, 0, 0, 0.9) 43 | love.graphics.rectangle("fill", self.x, self.y, self.width, totalHeight) 44 | 45 | -- Draw menu border 46 | love.graphics.setColor(1, 1, 1, 0.8) 47 | love.graphics.rectangle("line", self.x, self.y, self.width, totalHeight) 48 | 49 | -- Draw menu options 50 | for i, option in ipairs(options) do 51 | local y = self.y + (i-1) * self.optionHeight 52 | 53 | -- Highlight on hover 54 | if self:isMouseOverOption(i) then 55 | love.graphics.setColor(0.3, 0.3, 0.5, 0.8) 56 | love.graphics.rectangle("fill", self.x, y, self.width, self.optionHeight) 57 | end 58 | 59 | -- Draw option text 60 | love.graphics.setColor(1, 1, 1) 61 | love.graphics.print(option.text, self.x + 10, y + 5) 62 | end 63 | end 64 | 65 | -- Check if mouse is over a menu option 66 | function TileMenu:isMouseOverOption(optionIndex) 67 | if not self.visible then return false end 68 | 69 | local mx, my = love.mouse.getPosition() 70 | local options = self.currentLevel == MenuLevel.MAIN and self.options or self.buildOptions 71 | 72 | if optionIndex < 1 or optionIndex > #options then return false end 73 | 74 | local optionY = self.y + (optionIndex-1) * self.optionHeight 75 | 76 | return mx >= self.x and mx <= self.x + self.width and 77 | my >= optionY and my <= optionY + self.optionHeight 78 | end 79 | 80 | -- Handle mouse press on menu 81 | function TileMenu:handleMousePress(x, y) 82 | if not self.visible then return false end 83 | 84 | local options = self.currentLevel == MenuLevel.MAIN and self.options or self.buildOptions 85 | 86 | for i, option in ipairs(options) do 87 | if self:isMouseOverOption(i) then 88 | option.action() 89 | return true 90 | end 91 | end 92 | 93 | -- If click is outside menu, hide it 94 | self.visible = false 95 | return true 96 | end 97 | 98 | -- Handle escape key 99 | function TileMenu:handleEscape() 100 | if not self.visible then return false end 101 | 102 | if self.currentLevel == MenuLevel.BUILD then 103 | -- Go back to main menu 104 | self.currentLevel = MenuLevel.MAIN 105 | else 106 | -- Hide menu 107 | self.visible = false 108 | end 109 | 110 | return true 111 | end 112 | 113 | -- Show menu for a tile 114 | function TileMenu:showForTile(tile, x, y) 115 | -- The discovery check is now handled in main.lua before calling this function 116 | 117 | self.selectedTile = tile 118 | self.x = x 119 | self.y = y 120 | self.visible = true 121 | self.currentLevel = MenuLevel.MAIN 122 | 123 | -- Create main menu options 124 | self.options = {} 125 | 126 | -- Get the grid coordinates of the selected tile 127 | local gridX, gridY = game.camera:worldToGrid(x + game.camera.x, y + game.camera.y, game.tileSize) 128 | 129 | -- Store the grid coordinates for later use 130 | self.selectedGridX = gridX 131 | self.selectedGridY = gridY 132 | 133 | -- Add "Move to" option for non-water tiles 134 | if tile.type ~= TileType.WATER then 135 | table.insert(self.options, { 136 | text = "Move to", 137 | action = function() 138 | if game.player:moveTo(gridX, gridY, game.grid) then 139 | -- Only center camera when movement is complete 140 | -- This will be handled in the player's update function 141 | self.visible = false 142 | end 143 | end 144 | }) 145 | end 146 | 147 | -- Check if the tile is within a settlement 148 | local inSettlement, settlement = game.player:isTileInSettlement(gridX, gridY) 149 | 150 | if inSettlement then 151 | -- Inside settlement - show building options 152 | local tileDef = TileDefinitions.types[tile.type] 153 | if #tile.buildings < tileDef.maxBuildings and #tileDef.allowedBuildings > 0 then 154 | table.insert(self.options, { 155 | text = "Build", 156 | action = function() self:showBuildMenu() end 157 | }) 158 | end 159 | else 160 | -- Outside settlement - only allow settlement creation 161 | -- Check if the tile is suitable for a settlement (not water) 162 | if tile.type ~= TileType.WATER then 163 | table.insert(self.options, { 164 | text = "Found Settlement", 165 | action = function() 166 | if game.player:createSettlement(gridX, gridY) then 167 | self.visible = false 168 | end 169 | end 170 | }) 171 | end 172 | end 173 | 174 | -- Add "Close" option 175 | table.insert(self.options, { 176 | text = "Close", 177 | action = function() self.visible = false end 178 | }) 179 | 180 | -- Create build menu options 181 | self:createBuildOptions() 182 | end 183 | 184 | -- Show build submenu 185 | function TileMenu:showBuildMenu() 186 | self.currentLevel = MenuLevel.BUILD 187 | end 188 | 189 | -- Create build options 190 | function TileMenu:createBuildOptions() 191 | self.buildOptions = {} 192 | 193 | local tile = self.selectedTile 194 | if not tile then return end 195 | 196 | local tileDef = TileDefinitions.types[tile.type] 197 | 198 | -- Add building options 199 | for _, buildingType in ipairs(tileDef.allowedBuildings) do 200 | local buildings = Buildings.new() 201 | if buildings:canAddToTile(tile, buildingType) then 202 | local buildingDef = buildings.definitions[buildingType] 203 | local yieldStr = buildings:getYieldString(buildingType) 204 | 205 | table.insert(self.buildOptions, { 206 | text = buildingDef.name .. " (" .. yieldStr .. ")", 207 | action = function() 208 | buildings:addToTile(tile, buildingType) 209 | -- Update player yields after adding a building 210 | game.player:updateYields(game.grid) 211 | self.visible = false 212 | end 213 | }) 214 | end 215 | end 216 | 217 | -- Add "Back" option 218 | table.insert(self.buildOptions, { 219 | text = "Back", 220 | action = function() self.currentLevel = MenuLevel.MAIN end 221 | }) 222 | end 223 | 224 | return TileMenu 225 | -------------------------------------------------------------------------------- /resources.lua: -------------------------------------------------------------------------------- 1 | -- resources.lua - Resource definitions and rendering 2 | require("types") 3 | local love = require("love") 4 | 5 | -- Resource type enum 6 | ResourceType = { 7 | NONE = "none", 8 | IRON = "iron", 9 | GOLD_ORE = "gold_ore", 10 | HORSES = "horses", 11 | FISH = "fish", 12 | WHEAT = "wheat" 13 | } 14 | 15 | -- Resources class 16 | Resources = {} 17 | Resources.__index = Resources 18 | 19 | function Resources.new() 20 | local self = setmetatable({}, Resources) 21 | return self 22 | end 23 | 24 | -- Resource definitions 25 | Resources.definitions = { 26 | [ResourceType.IRON] = { 27 | name = "Iron", 28 | bonuses = {food = 0, production = 2, gold = 1}, 29 | validTiles = {TileType.MOUNTAIN, TileType.PLAINS}, 30 | rarity = 0.1, -- 10% chance on valid tiles 31 | description = "Used for tools and weapons." 32 | }, 33 | [ResourceType.GOLD_ORE] = { 34 | name = "Gold Ore", 35 | bonuses = {food = 0, production = 1, gold = 3}, 36 | validTiles = {TileType.MOUNTAIN}, 37 | rarity = 0.05, -- 5% chance on valid tiles 38 | description = "Highly valuable precious metal." 39 | }, 40 | [ResourceType.HORSES] = { 41 | name = "Horses", 42 | bonuses = {food = 1, production = 1, gold = 1}, 43 | validTiles = {TileType.PLAINS}, 44 | rarity = 0.15, -- 15% chance on valid tiles 45 | description = "Enables cavalry units and improves mobility." 46 | }, 47 | [ResourceType.FISH] = { 48 | name = "Fish", 49 | bonuses = {food = 3, production = 0, gold = 1}, 50 | validTiles = {TileType.WATER}, 51 | rarity = 0.2, -- 20% chance on valid tiles 52 | description = "Abundant source of food from the sea." 53 | }, 54 | [ResourceType.WHEAT] = { 55 | name = "Wheat", 56 | bonuses = {food = 3, production = 0, gold = 1}, 57 | validTiles = {TileType.PLAINS}, 58 | rarity = 0.2, -- 20% chance on valid tiles 59 | description = "Staple crop for growing populations." 60 | } 61 | } 62 | 63 | -- Check if a resource can be placed on a tile 64 | function Resources:canPlaceOnTile(resourceType, tileType) 65 | local resourceDef = self.definitions[resourceType] 66 | if not resourceDef then return false end 67 | 68 | for _, validTile in ipairs(resourceDef.validTiles) do 69 | if validTile == tileType then 70 | return true 71 | end 72 | end 73 | 74 | return false 75 | end 76 | 77 | -- Get resource bonuses 78 | function Resources:getBonuses(resourceType) 79 | if resourceType == ResourceType.NONE then 80 | return {food = 0, production = 0, gold = 0} 81 | end 82 | 83 | local resourceDef = self.definitions[resourceType] 84 | return resourceDef.bonuses 85 | end 86 | 87 | -- Render a resource on a tile 88 | function Resources:render(resourceType, x, y, tileSize, opacity) 89 | if resourceType == ResourceType.NONE then return end 90 | 91 | -- Default opacity to 1 if not provided 92 | opacity = opacity or 1 93 | 94 | -- Center of the tile 95 | local centerX = x + tileSize / 2 96 | local centerY = y + tileSize / 2 97 | local size = tileSize / 5 98 | 99 | -- Draw resource indicator based on type 100 | if resourceType == ResourceType.IRON then 101 | -- Iron: Gray hexagon 102 | love.graphics.setColor(0.6, 0.6, 0.6, 0.9 * opacity) 103 | self:drawHexagon(centerX, centerY, size) 104 | elseif resourceType == ResourceType.GOLD_ORE then 105 | -- Gold: Yellow star 106 | love.graphics.setColor(1, 0.9, 0.2, 0.9 * opacity) 107 | self:drawStar(centerX, centerY, size) 108 | elseif resourceType == ResourceType.HORSES then 109 | -- Horses: Brown horseshoe 110 | love.graphics.setColor(0.6, 0.4, 0.2, 0.9 * opacity) 111 | self:drawHorseshoe(centerX, centerY, size) 112 | elseif resourceType == ResourceType.FISH then 113 | -- Fish: Blue fish shape 114 | love.graphics.setColor(0.2, 0.5, 0.9, 0.9 * opacity) 115 | self:drawFish(centerX, centerY, size) 116 | elseif resourceType == ResourceType.WHEAT then 117 | -- Wheat: Yellow/brown wheat shape 118 | love.graphics.setColor(0.9, 0.8, 0.3, 0.9 * opacity) 119 | self:drawWheat(centerX, centerY, size) 120 | end 121 | end 122 | 123 | -- Helper function to draw a hexagon 124 | function Resources:drawHexagon(x, y, size) 125 | local vertices = {} 126 | for i = 0, 5 do 127 | local angle = (i * math.pi / 3) - math.pi / 6 128 | table.insert(vertices, x + size * math.cos(angle)) 129 | table.insert(vertices, y + size * math.sin(angle)) 130 | end 131 | love.graphics.polygon("fill", vertices) 132 | love.graphics.setColor(0, 0, 0, 0.7) 133 | love.graphics.polygon("line", vertices) 134 | end 135 | 136 | -- Helper function to draw a star 137 | function Resources:drawStar(x, y, size) 138 | local vertices = {} 139 | 140 | for i = 0, 4 do 141 | local outerAngle = (i * 2 * math.pi / 5) - math.pi / 2 142 | local innerAngle = outerAngle + math.pi / 5 143 | 144 | -- Outer point 145 | table.insert(vertices, x + size * math.cos(outerAngle)) 146 | table.insert(vertices, y + size * math.sin(outerAngle)) 147 | 148 | -- Inner point 149 | table.insert(vertices, x + (size/2) * math.cos(innerAngle)) 150 | table.insert(vertices, y + (size/2) * math.sin(innerAngle)) 151 | end 152 | 153 | love.graphics.polygon("fill", vertices) 154 | love.graphics.setColor(0, 0, 0, 0.7) 155 | love.graphics.polygon("line", vertices) 156 | end 157 | 158 | -- Helper function to draw a horseshoe 159 | function Resources:drawHorseshoe(x, y, size) 160 | -- Simple horseshoe representation 161 | love.graphics.setLineWidth(size/3) 162 | love.graphics.arc("line", x, y, size, math.pi, 0) 163 | love.graphics.line(x - size, y, x - size, y + size/2) 164 | love.graphics.line(x + size, y, x + size, y + size/2) 165 | love.graphics.setLineWidth(1) 166 | end 167 | 168 | -- Helper function to draw a fish 169 | function Resources:drawFish(x, y, size) 170 | -- Simple fish shape 171 | local vertices = { 172 | x - size, y, 173 | x - size/2, y - size/2, 174 | x + size/2, y - size/2, 175 | x + size, y, 176 | x + size/2, y + size/2, 177 | x - size/2, y + size/2 178 | } 179 | love.graphics.polygon("fill", vertices) 180 | love.graphics.setColor(0, 0, 0, 0.7) 181 | love.graphics.polygon("line", vertices) 182 | 183 | -- Eye 184 | love.graphics.setColor(1, 1, 1, 0.9) 185 | love.graphics.circle("fill", x + size/4, y - size/6, size/6) 186 | love.graphics.setColor(0, 0, 0, 0.9) 187 | love.graphics.circle("fill", x + size/4, y - size/6, size/10) 188 | end 189 | 190 | -- Helper function to draw wheat 191 | function Resources:drawWheat(x, y, size) 192 | -- Simple wheat representation 193 | love.graphics.setLineWidth(size/6) 194 | 195 | -- Stem 196 | love.graphics.line(x, y, x, y + size) 197 | 198 | -- Grains 199 | local grainLength = size * 0.4 200 | local grainSpacing = size * 0.2 201 | 202 | for i = 0, 2 do 203 | local yPos = y + i * grainSpacing 204 | -- Right side grains 205 | love.graphics.line(x, yPos, x + grainLength, yPos - grainLength/2) 206 | -- Left side grains 207 | love.graphics.line(x, yPos, x - grainLength, yPos - grainLength/2) 208 | end 209 | 210 | love.graphics.setLineWidth(1) 211 | end 212 | 213 | -- Get formatted yield string for a resource 214 | function Resources:getYieldString(resourceType) 215 | if resourceType == ResourceType.NONE then 216 | return "No yield" 217 | end 218 | 219 | local bonuses = self.definitions[resourceType].bonuses 220 | local yieldStr = "" 221 | 222 | if bonuses.food > 0 then 223 | yieldStr = yieldStr .. "+" .. bonuses.food .. " Food " 224 | end 225 | if bonuses.production > 0 then 226 | yieldStr = yieldStr .. "+" .. bonuses.production .. " Prod " 227 | end 228 | if bonuses.gold > 0 then 229 | yieldStr = yieldStr .. "+" .. bonuses.gold .. " Gold" 230 | end 231 | 232 | -- If there are no bonuses, show "No yield" 233 | if yieldStr == "" then 234 | yieldStr = "No yield" 235 | end 236 | 237 | return yieldStr 238 | end 239 | 240 | return Resources 241 | -------------------------------------------------------------------------------- /renderer.lua: -------------------------------------------------------------------------------- 1 | require("camera") 2 | require("tile") 3 | require("buildings") 4 | require("resources") 5 | local love = require("love") 6 | 7 | -- renderer.lua - Handles rendering of game elements 8 | 9 | Renderer = {} 10 | Renderer.__index = Renderer 11 | 12 | function Renderer.new() 13 | local self = setmetatable({}, Renderer) 14 | return self 15 | end 16 | 17 | function Renderer:drawGrid(grid, camera, tileSize, player) 18 | -- Get the effective tile size based on zoom level 19 | local effectiveTileSize = camera:getEffectiveTileSize() 20 | 21 | -- Single pass through the grid 22 | for y = 1, grid.height do 23 | for x = 1, grid.width do 24 | local screenX = (x-1) * effectiveTileSize 25 | local screenY = (y-1) * effectiveTileSize 26 | 27 | -- Only draw tiles that are visible on screen 28 | if camera:isOnScreen(screenX, screenY, effectiveTileSize, effectiveTileSize) then 29 | local tile = grid.tiles[y][x] 30 | 31 | -- Check if the tile is being discovered (animating) 32 | local discoveryProgress = player:getTileDiscoveryProgress(x, y) 33 | 34 | -- Draw the tile based on discovery status 35 | if player:isTileDiscovered(x, y) then 36 | -- Draw discovered tile with full details 37 | local tileDef = TileDefinitions.types[tile.type] 38 | 39 | -- Draw base tile 40 | if discoveryProgress >= 0 and discoveryProgress < 1 then 41 | -- Tile is being discovered - animate the reveal 42 | -- Draw fog of war underneath 43 | love.graphics.setColor(0.1, 0.1, 0.1) 44 | love.graphics.rectangle("fill", screenX, screenY, effectiveTileSize, effectiveTileSize) 45 | 46 | -- Draw tile with increasing opacity 47 | love.graphics.setColor( 48 | tileDef.color[1], 49 | tileDef.color[2], 50 | tileDef.color[3], 51 | math.min(1, discoveryProgress * 1.5) -- Faster fade-in 52 | ) 53 | love.graphics.rectangle("fill", screenX, screenY, effectiveTileSize, effectiveTileSize) 54 | 55 | -- Draw border with increasing opacity 56 | love.graphics.setColor(0, 0, 0, 0.3 * math.min(1, discoveryProgress * 1.5)) 57 | love.graphics.rectangle("line", screenX, screenY, effectiveTileSize, effectiveTileSize) 58 | 59 | -- Draw a reveal effect (expanding circle) 60 | love.graphics.setColor(1, 1, 1, 0.7 * (1 - discoveryProgress)) 61 | local radius = effectiveTileSize * 0.6 * discoveryProgress -- Larger circle 62 | love.graphics.circle("line", screenX + effectiveTileSize/2, screenY + effectiveTileSize/2, radius) 63 | else 64 | -- Fully discovered tile 65 | love.graphics.setColor(tileDef.color) 66 | love.graphics.rectangle("fill", screenX, screenY, effectiveTileSize, effectiveTileSize) 67 | love.graphics.setColor(0, 0, 0, 0.3) 68 | love.graphics.rectangle("line", screenX, screenY, effectiveTileSize, effectiveTileSize) 69 | end 70 | 71 | -- Only draw resources and buildings if fully visible or nearly visible 72 | -- Show resources and buildings earlier in the animation 73 | if discoveryProgress < 0 or discoveryProgress > 0.5 then 74 | -- Draw resource indicator if present 75 | if tile.resource ~= ResourceType.NONE then 76 | local resources = Resources.new() 77 | -- If still animating, draw with partial opacity 78 | local opacity = 1 79 | if discoveryProgress >= 0 then 80 | opacity = math.min(1, (discoveryProgress - 0.5) * 2) -- Faster fade-in 81 | end 82 | resources:render(tile.resource, screenX, screenY, effectiveTileSize, opacity) 83 | end 84 | 85 | -- Draw building indicators 86 | if #tile.buildings > 0 then 87 | local buildings = Buildings.new() 88 | -- If still animating, draw with partial opacity 89 | local opacity = 1 90 | if discoveryProgress >= 0 then 91 | opacity = math.min(1, (discoveryProgress - 0.5) * 2) -- Faster fade-in 92 | end 93 | buildings:render(tile, screenX, screenY, effectiveTileSize, opacity) 94 | end 95 | end 96 | else 97 | -- Draw undiscovered tile (fog of war) 98 | love.graphics.setColor(0.1, 0.1, 0.1) -- Dark gray for undiscovered 99 | love.graphics.rectangle("fill", screenX, screenY, effectiveTileSize, effectiveTileSize) 100 | love.graphics.setColor(0, 0, 0, 0.5) 101 | love.graphics.rectangle("line", screenX, screenY, effectiveTileSize, effectiveTileSize) 102 | end 103 | end 104 | end 105 | end 106 | end 107 | 108 | function Renderer:drawTooltip(tile, x, y, player, gridX, gridY) 109 | -- We already checked if the tile is discovered in main.lua, so we don't need to check again 110 | if not tile then return end 111 | 112 | local tileDef = TileDefinitions.types[tile.type] 113 | local totalYield = tile:getTotalYield() 114 | 115 | -- Get base tile yields 116 | local baseYield = { 117 | food = tileDef.bonuses.food, 118 | production = tileDef.bonuses.production, 119 | gold = tileDef.bonuses.gold 120 | } 121 | 122 | -- Get resource yields 123 | local resourceYield = {food = 0, production = 0, gold = 0} 124 | if tile.resource ~= ResourceType.NONE then 125 | local resources = Resources.new() 126 | resourceYield = resources:getBonuses(tile.resource) 127 | end 128 | 129 | -- Get building yields 130 | local buildingYield = {food = 0, production = 0, gold = 0} 131 | if #tile.buildings > 0 then 132 | local buildings = Buildings.new() 133 | buildingYield = buildings:getBonuses(tile) 134 | end 135 | 136 | -- Check if the tile is in a settlement 137 | local inSettlement, settlement = player:isTileInSettlement(gridX, gridY) 138 | local tooltipHeight = inSettlement and 160 or 140 139 | 140 | -- Draw tooltip background 141 | love.graphics.setColor(0, 0, 0, 0.8) 142 | love.graphics.rectangle("fill", x + 10, y + 10, 180, tooltipHeight) 143 | 144 | -- Draw tooltip border 145 | love.graphics.setColor(0.7, 0.7, 0.7, 0.9) -- Light gray border 146 | love.graphics.rectangle("line", x + 10, y + 10, 180, tooltipHeight) 147 | 148 | -- Reset color for tooltip text 149 | love.graphics.setColor(1, 1, 1) 150 | 151 | local buildingNames = "" 152 | local buildings = Buildings.new() 153 | for i, building in ipairs(tile.buildings) do 154 | buildingNames = buildingNames .. (i > 1 and ", " or "") .. buildings.definitions[building].name 155 | end 156 | 157 | local resourceName = "None" 158 | if tile.resource ~= ResourceType.NONE then 159 | local resources = Resources.new() 160 | resourceName = resources.definitions[tile.resource].name 161 | end 162 | 163 | -- Format yields to show base + bonus 164 | local foodText = string.format("Food: %d (%d+%d+%d)", 165 | totalYield.food, baseYield.food, resourceYield.food, buildingYield.food) 166 | local prodText = string.format("Prod: %d (%d+%d+%d)", 167 | totalYield.production, baseYield.production, resourceYield.production, buildingYield.production) 168 | local goldText = string.format("Gold: %d (%d+%d+%d)", 169 | totalYield.gold, baseYield.gold, resourceYield.gold, buildingYield.gold) 170 | 171 | local tooltipText = string.format("Type: %s\nResource: %s\n%s\n%s\n%s\nBuildings: %s", 172 | tileDef.name, 173 | resourceName, 174 | foodText, 175 | prodText, 176 | goldText, 177 | buildingNames ~= "" and buildingNames or "None" 178 | ) 179 | 180 | -- Add settlement information if applicable 181 | if inSettlement then 182 | tooltipText = tooltipText .. ("\nSettlement: %s"):format(settlement.name) 183 | tooltipText = tooltipText .. "\n(All tiles in settlements contribute to yields)" 184 | end 185 | 186 | -- Add yield explanation 187 | tooltipText = tooltipText .. "\n(Base+Resource+Building)" 188 | 189 | love.graphics.print(tooltipText, x + 15, y + 15) 190 | end 191 | 192 | return Renderer 193 | -------------------------------------------------------------------------------- /grid.lua: -------------------------------------------------------------------------------- 1 | -- Grid class 2 | Grid = {} 3 | Grid.__index = Grid 4 | 5 | function Grid.new(width, height, tileSize) 6 | local self = setmetatable({}, Grid) 7 | self.width = width 8 | self.height = height 9 | self.tileSize = tileSize 10 | self.tiles = {} 11 | self:initialize() 12 | return self 13 | end 14 | 15 | function Grid:initialize() 16 | math.randomseed(os.time()) 17 | 18 | -- Create a heightmap for more natural terrain generation 19 | local heightMap = self:generateHeightMap() 20 | 21 | -- Create tiles based on the heightmap 22 | for y = 1, self.height do 23 | self.tiles[y] = {} 24 | for x = 1, self.width do 25 | local height = heightMap[y][x] 26 | local tileType 27 | 28 | -- Determine tile type based on height 29 | if height < 0.3 then 30 | tileType = TileType.WATER -- Deep water (oceans, lakes) 31 | elseif height < 0.35 then 32 | tileType = TileType.WATER -- Shallow water (shores, rivers) 33 | elseif height < 0.5 then 34 | tileType = TileType.PLAINS -- Plains (low elevation) 35 | elseif height < 0.7 then 36 | tileType = TileType.FOREST -- Forests (medium elevation) 37 | else 38 | tileType = TileType.MOUNTAIN -- Mountains (high elevation) 39 | end 40 | 41 | -- Create the tile 42 | local tile = Tile.new(tileType) 43 | 44 | -- Add resources based on tile type 45 | self:addResourcesToTile(tile) 46 | 47 | self.tiles[y][x] = tile 48 | end 49 | end 50 | 51 | -- Generate rivers 52 | self:generateRivers(heightMap, 5) -- Generate 5 rivers 53 | end 54 | 55 | -- Generate a heightmap using simplex noise for natural-looking terrain 56 | function Grid:generateHeightMap() 57 | local heightMap = {} 58 | local scale = 0.1 -- Controls the scale of the terrain features 59 | local persistence = 0.5 -- Controls how much detail is added at each octave 60 | local octaves = 4 -- Number of layers of noise 61 | 62 | -- Initialize the heightmap 63 | for y = 1, self.height do 64 | heightMap[y] = {} 65 | for x = 1, self.width do 66 | heightMap[y][x] = 0 67 | end 68 | end 69 | 70 | -- Generate multiple octaves of noise 71 | for octave = 1, octaves do 72 | local frequency = 2^(octave-1) 73 | local amplitude = persistence^(octave-1) 74 | 75 | for y = 1, self.height do 76 | for x = 1, self.width do 77 | -- Use simple noise approximation since LÖVE doesn't have built-in simplex noise 78 | local nx = x / self.width * frequency * scale 79 | local ny = y / self.height * frequency * scale 80 | local noise = self:noise2D(nx, ny) 81 | 82 | heightMap[y][x] = heightMap[y][x] + noise * amplitude 83 | end 84 | end 85 | end 86 | 87 | -- Normalize the heightmap to 0-1 range 88 | local min, max = 1, 0 89 | for y = 1, self.height do 90 | for x = 1, self.width do 91 | min = math.min(min, heightMap[y][x]) 92 | max = math.max(max, heightMap[y][x]) 93 | end 94 | end 95 | 96 | for y = 1, self.height do 97 | for x = 1, self.width do 98 | heightMap[y][x] = (heightMap[y][x] - min) / (max - min) 99 | end 100 | end 101 | 102 | return heightMap 103 | end 104 | 105 | -- Simple 2D noise function (not true simplex noise, but good enough for our purposes) 106 | function Grid:noise2D(x, y) 107 | -- Use a different approach that doesn't rely on bitwise operations 108 | x = x * 12.9898 109 | y = y * 78.233 110 | local value = math.sin(x + y) * 43758.5453 111 | return value - math.floor(value) 112 | end 113 | 114 | -- Generate rivers flowing from high to low elevation, preferably toward oceans 115 | function Grid:generateRivers(heightMap, count) 116 | -- First, identify ocean tiles (water tiles at the edges of the map) 117 | local oceanTiles = {} 118 | for y = 1, self.height do 119 | for x = 1, self.width do 120 | -- Consider water tiles near the edge as ocean 121 | local isEdge = x <= 5 or x >= self.width - 5 or y <= 5 or y >= self.height - 5 122 | if heightMap[y][x] < 0.3 and isEdge then 123 | table.insert(oceanTiles, {x = x, y = y}) 124 | end 125 | end 126 | end 127 | 128 | for i = 1, count do 129 | -- Start at a random high point (mountain) 130 | local x, y 131 | local attempts = 0 132 | repeat 133 | x = math.random(1, self.width) 134 | y = math.random(1, self.height) 135 | attempts = attempts + 1 136 | until (heightMap[y][x] > 0.7 or attempts > 100) 137 | 138 | -- Trace the river path downhill 139 | local riverLength = math.random(20, 100) -- Longer rivers to reach oceans 140 | local currentX, currentY = x, y 141 | local riverTiles = {} 142 | local reachedOcean = false 143 | 144 | for j = 1, riverLength do 145 | -- Mark the current tile as part of the river 146 | if currentX >= 1 and currentX <= self.width and currentY >= 1 and currentY <= self.height then 147 | table.insert(riverTiles, {x = currentX, y = currentY}) 148 | 149 | -- Check if we've reached an ocean 150 | for _, ocean in ipairs(oceanTiles) do 151 | if math.abs(currentX - ocean.x) <= 1 and math.abs(currentY - ocean.y) <= 1 then 152 | reachedOcean = true 153 | break 154 | end 155 | end 156 | 157 | if reachedOcean then 158 | break 159 | end 160 | 161 | -- Find the next tile to flow to 162 | local nextX, nextY = self:findNextRiverTile(heightMap, currentX, currentY, oceanTiles) 163 | 164 | -- If we can't flow anymore, break 165 | if nextX == currentX and nextY == currentY then 166 | -- Create a small lake at the end of the river 167 | self:createLake(currentX, currentY, math.random(2, 5)) 168 | break 169 | end 170 | 171 | -- Move to the next tile 172 | currentX, currentY = nextX, nextY 173 | else 174 | break 175 | end 176 | end 177 | 178 | -- Now actually create the river tiles 179 | for _, tile in ipairs(riverTiles) do 180 | self.tiles[tile.y][tile.x] = Tile.new(TileType.WATER) 181 | 182 | -- Add fish resources to some river tiles 183 | if math.random() < 0.1 then 184 | self.tiles[tile.y][tile.x]:setResource(ResourceType.FISH) 185 | end 186 | end 187 | end 188 | end 189 | 190 | -- Find the next tile for a river to flow to, with a bias toward oceans 191 | function Grid:findNextRiverTile(heightMap, x, y, oceanTiles) 192 | local lowestX, lowestY = x, y 193 | local lowestHeight = heightMap[y][x] 194 | local oceanBias = 0.05 -- Bias toward flowing to oceans 195 | 196 | -- Find the closest ocean tile 197 | local closestOcean = nil 198 | local closestDist = math.huge 199 | 200 | for _, ocean in ipairs(oceanTiles) do 201 | local dist = math.sqrt((x - ocean.x)^2 + (y - ocean.y)^2) 202 | if dist < closestDist then 203 | closestDist = dist 204 | closestOcean = ocean 205 | end 206 | end 207 | 208 | -- Check all neighboring tiles 209 | for dy = -1, 1 do 210 | for dx = -1, 1 do 211 | if not (dx == 0 and dy == 0) then 212 | local nx, ny = x + dx, y + dy 213 | if nx >= 1 and nx <= #heightMap[1] and ny >= 1 and ny <= #heightMap then 214 | local height = heightMap[ny][nx] 215 | 216 | -- Apply ocean bias if we have a closest ocean 217 | if closestOcean then 218 | local oceanDir = { 219 | x = closestOcean.x - x, 220 | y = closestOcean.y - y 221 | } 222 | local dotProduct = dx * oceanDir.x + dy * oceanDir.y 223 | local oceanInfluence = dotProduct > 0 and oceanBias or 0 224 | height = height - oceanInfluence 225 | end 226 | 227 | if height < lowestHeight then 228 | lowestHeight = height 229 | lowestX, lowestY = nx, ny 230 | end 231 | end 232 | end 233 | end 234 | end 235 | 236 | return lowestX, lowestY 237 | end 238 | 239 | -- Create a lake of a given radius centered at (x, y) 240 | function Grid:createLake(x, y, radius) 241 | -- Increase the base lake size 242 | radius = radius + 2 -- Make all lakes bigger by default 243 | 244 | -- Check if we're near the edge of the map - if so, make an even larger lake 245 | local isNearEdge = x <= 10 or x >= self.width - 10 or y <= 10 or y >= self.height - 10 246 | if isNearEdge then 247 | radius = radius + 3 -- Larger lakes near edges to represent seas 248 | end 249 | 250 | -- Add some randomness to lake shape for more natural appearance 251 | local baseRadius = radius 252 | 253 | for dy = -radius-2, radius+2 do 254 | for dx = -radius-2, radius+2 do 255 | local nx, ny = x + dx, y + dy 256 | if nx >= 1 and nx <= self.width and ny >= 1 and ny <= self.height then 257 | -- Use distance formula with some noise for irregular lake shapes 258 | local distance = math.sqrt(dx*dx + dy*dy) 259 | 260 | -- Add noise to the radius check for more natural lake shapes 261 | local noiseValue = self:noise2D(nx/10, ny/10) * 2 - 0.5 262 | local adjustedRadius = baseRadius + noiseValue 263 | 264 | if distance <= adjustedRadius then 265 | self.tiles[ny][nx] = Tile.new(TileType.WATER) 266 | 267 | -- Add fish resources to some lake tiles 268 | if math.random() < 0.3 then 269 | self.tiles[ny][nx]:setResource(ResourceType.FISH) 270 | end 271 | end 272 | end 273 | end 274 | end 275 | 276 | -- Sometimes create a second connected lake for more complex water bodies 277 | if math.random() < 0.4 then 278 | local offsetX = math.random(-radius, radius) 279 | local offsetY = math.random(-radius, radius) 280 | local secondX = x + offsetX 281 | local secondY = y + offsetY 282 | 283 | -- Make sure the second lake center is within map bounds 284 | if secondX >= 1 and secondX <= self.width and secondY >= 1 and secondY <= self.height then 285 | local secondRadius = math.random(radius-1, radius+1) 286 | 287 | for dy = -secondRadius, secondRadius do 288 | for dx = -secondRadius, secondRadius do 289 | local nx, ny = secondX + dx, secondY + dy 290 | if nx >= 1 and nx <= self.width and ny >= 1 and ny <= self.height then 291 | -- Use distance formula with some noise for irregular lake shapes 292 | local distance = math.sqrt(dx*dx + dy*dy) 293 | 294 | -- Add noise to the radius check for more natural lake shapes 295 | local noiseValue = self:noise2D(nx/10, ny/10) * 2 - 0.5 296 | local adjustedRadius = secondRadius + noiseValue 297 | 298 | if distance <= adjustedRadius then 299 | self.tiles[ny][nx] = Tile.new(TileType.WATER) 300 | 301 | -- Add fish resources to some lake tiles 302 | if math.random() < 0.3 then 303 | self.tiles[ny][nx]:setResource(ResourceType.FISH) 304 | end 305 | end 306 | end 307 | end 308 | end 309 | end 310 | end 311 | end 312 | 313 | -- Add resources to a tile based on its type 314 | function Grid:addResourcesToTile(tile) 315 | -- Only add resources with a random chance based on rarity 316 | if math.random() > 0.2 then return end -- 20% chance to have any resource 317 | 318 | local resources = Resources.new() 319 | 320 | -- Get all possible resources for this tile type 321 | local possibleResources = {} 322 | for resourceType, resource in pairs(resources.definitions) do 323 | if resources:canPlaceOnTile(resourceType, tile.type) then 324 | -- Weight by rarity 325 | for i = 1, math.floor(resource.rarity * 100) do 326 | table.insert(possibleResources, resourceType) 327 | end 328 | end 329 | end 330 | 331 | -- If there are possible resources, randomly select one 332 | if #possibleResources > 0 then 333 | local selectedResource = possibleResources[math.random(#possibleResources)] 334 | tile:setResource(selectedResource) 335 | end 336 | end 337 | 338 | function Grid:getTileAt(x, y) 339 | if x >= 1 and x <= self.width and y >= 1 and y <= self.height then 340 | return self.tiles[y][x] 341 | end 342 | return nil 343 | end 344 | -------------------------------------------------------------------------------- /player.lua: -------------------------------------------------------------------------------- 1 | -- player.lua - Player entity and UI 2 | 3 | require("tile") 4 | require("settlement") 5 | local love = require("love") 6 | 7 | Player = {} 8 | Player.__index = Player 9 | 10 | function Player.new(grid) 11 | local self = setmetatable({}, Player) 12 | 13 | -- Player position 14 | self.gridX = nil 15 | self.gridY = nil 16 | 17 | -- Animation state 18 | self.isMoving = false 19 | self.moveStartX = nil 20 | self.moveStartY = nil 21 | self.moveTargetX = nil 22 | self.moveTargetY = nil 23 | self.moveProgress = 0 24 | self.moveSpeed = 8 -- Tiles per second (increased from 5) 25 | 26 | -- Visual position (for smooth movement) 27 | self.visualX = nil 28 | self.visualY = nil 29 | 30 | -- Tile discovery animation 31 | self.discoveringTiles = {} -- Table to track tiles being discovered 32 | self.discoveryDuration = 0.5 -- How long the discovery animation takes (reduced from 0.8) 33 | self.discoveryWaveSpeed = 10 -- How fast the discovery wave spreads (increased from 3) 34 | 35 | -- Player yields 36 | self.yields = { 37 | food = 0, 38 | production = 0, 39 | gold = 0 40 | } 41 | 42 | -- Sight range (how many tiles the player can see in each direction) 43 | self.sightRange = 4 44 | 45 | -- Track discovered tiles using a 2D table 46 | self.discoveredTiles = {} 47 | for y = 1, grid.height do 48 | self.discoveredTiles[y] = {} 49 | for x = 1, grid.width do 50 | self.discoveredTiles[y][x] = false 51 | end 52 | end 53 | 54 | -- Player settlements 55 | self.settlements = {} 56 | 57 | -- Find a random valid starting position (non-water tile) 58 | self:findRandomStartPosition(grid) 59 | 60 | -- Initialize visual position to match grid position 61 | self.visualX = self.gridX 62 | self.visualY = self.gridY 63 | 64 | -- Discover tiles around the starting position 65 | self:discoverTilesInRange(grid) 66 | 67 | return self 68 | end 69 | 70 | function Player:findRandomStartPosition(grid) 71 | local validTiles = {} 72 | 73 | -- Collect all non-water tiles as potential starting positions 74 | for y = 1, grid.height do 75 | for x = 1, grid.width do 76 | local tile = grid.tiles[y][x] 77 | if tile.type ~= TileType.WATER then 78 | table.insert(validTiles, {x = x, y = y}) 79 | end 80 | end 81 | end 82 | 83 | -- Select a random valid tile 84 | if #validTiles > 0 then 85 | local randomIndex = math.random(1, #validTiles) 86 | self.gridX = validTiles[randomIndex].x 87 | self.gridY = validTiles[randomIndex].y 88 | else 89 | -- Fallback to a random position if no valid tiles found 90 | self.gridX = math.random(1, grid.width) 91 | self.gridY = math.random(1, grid.height) 92 | end 93 | end 94 | 95 | function Player:getCurrentTile(grid) 96 | if self.gridX and self.gridY then 97 | return grid:getTileAt(self.gridX, self.gridY) 98 | end 99 | return nil 100 | end 101 | 102 | function Player:moveTo(x, y, grid) 103 | -- Don't allow movement if already moving 104 | if self.isMoving then 105 | return false 106 | end 107 | 108 | -- Check if the target position is valid 109 | if x >= 1 and x <= grid.width and y >= 1 and y <= grid.height then 110 | local tile = grid.tiles[y][x] 111 | -- Only allow movement to non-water tiles 112 | if tile.type ~= TileType.WATER then 113 | -- Start animation 114 | self.isMoving = true 115 | self.moveStartX = self.gridX 116 | self.moveStartY = self.gridY 117 | self.moveTargetX = x 118 | self.moveTargetY = y 119 | self.moveProgress = 0 120 | 121 | return true 122 | end 123 | end 124 | return false 125 | end 126 | 127 | -- Update player animation 128 | function Player:update(dt, grid) 129 | if self.isMoving then 130 | -- Update movement progress 131 | self.moveProgress = self.moveProgress + dt * self.moveSpeed 132 | 133 | -- Update visual position 134 | self.visualX = self.moveStartX + (self.moveTargetX - self.moveStartX) * math.min(1, self.moveProgress) 135 | self.visualY = self.moveStartY + (self.moveTargetY - self.moveStartY) * math.min(1, self.moveProgress) 136 | 137 | -- Smoothly follow player with camera during movement 138 | local effectiveTileSize = game.camera:getEffectiveTileSize() 139 | local centerX = (self.visualX - 1) * effectiveTileSize 140 | local centerY = (self.visualY - 1) * effectiveTileSize 141 | game:centerCameraOn(centerX, centerY) 142 | 143 | -- Check if movement is complete 144 | if self.moveProgress >= 1 then 145 | -- Finalize movement 146 | self.gridX = self.moveTargetX 147 | self.gridY = self.moveTargetY 148 | self.visualX = self.gridX 149 | self.visualY = self.gridY 150 | self.isMoving = false 151 | 152 | -- Discover tiles in range after moving 153 | self:discoverTilesInRange(grid) 154 | 155 | -- Increment turn counter when movement is complete 156 | game:nextTurn() 157 | end 158 | end 159 | 160 | -- Update tile discovery animations 161 | local i = 1 162 | while i <= #self.discoveringTiles do 163 | local tile = self.discoveringTiles[i] 164 | 165 | -- Only start animating after the delay 166 | if tile.delay <= 0 then 167 | tile.progress = tile.progress + dt / self.discoveryDuration 168 | 169 | -- Remove completed animations 170 | if tile.progress >= 1 then 171 | table.remove(self.discoveringTiles, i) 172 | else 173 | i = i + 1 174 | end 175 | else 176 | -- Decrease delay 177 | tile.delay = tile.delay - dt 178 | i = i + 1 179 | end 180 | end 181 | end 182 | 183 | -- Discover tiles within the player's sight range 184 | function Player:discoverTilesInRange(grid) 185 | if not self.gridX or not self.gridY then return end 186 | 187 | -- Calculate the range of tiles to check 188 | local startX = math.max(1, self.gridX - self.sightRange) 189 | local endX = math.min(grid.width, self.gridX + self.sightRange) 190 | local startY = math.max(1, self.gridY - self.sightRange) 191 | local endY = math.min(grid.height, self.gridY + self.sightRange) 192 | 193 | -- Check each tile in the rectangular area 194 | for y = startY, endY do 195 | for x = startX, endX do 196 | -- Calculate distance from player (using Euclidean distance for a circular sight) 197 | local distance = math.sqrt((x - self.gridX)^2 + (y - self.gridY)^2) 198 | 199 | -- If within sight range and not already discovered, start discovery animation 200 | if distance <= self.sightRange and not self.discoveredTiles[y][x] then 201 | -- Calculate delay based on distance from player 202 | local delay = distance / self.discoveryWaveSpeed 203 | 204 | -- Add to discovering tiles list 205 | table.insert(self.discoveringTiles, { 206 | x = x, 207 | y = y, 208 | progress = 0, 209 | delay = delay, 210 | distance = distance 211 | }) 212 | 213 | -- Mark as discovered immediately in the data structure 214 | self.discoveredTiles[y][x] = true 215 | end 216 | end 217 | end 218 | end 219 | 220 | -- Check if a tile is being discovered (for rendering) 221 | function Player:getTileDiscoveryProgress(x, y) 222 | for _, tile in ipairs(self.discoveringTiles) do 223 | if tile.x == x and tile.y == y then 224 | -- Return progress if delay is over, otherwise 0 225 | return tile.delay <= 0 and tile.progress or 0 226 | end 227 | end 228 | return -1 -- Not being discovered 229 | end 230 | 231 | -- Check if a tile is discovered by this player 232 | function Player:isTileDiscovered(x, y) 233 | -- Make sure coordinates are valid 234 | if not x or not y then return false end 235 | 236 | -- Convert to integers in case they're floats 237 | x, y = math.floor(x), math.floor(y) 238 | 239 | -- Check if coordinates are within bounds 240 | if x < 1 or y < 1 or x > #self.discoveredTiles[1] or y > #self.discoveredTiles then 241 | return false 242 | end 243 | 244 | return self.discoveredTiles[y][x] 245 | end 246 | 247 | function Player:updateYields(grid) 248 | -- Reset yields 249 | self.yields = { 250 | food = 0, 251 | production = 0, 252 | gold = 0 253 | } 254 | 255 | -- Calculate yields from all tiles within settlements 256 | for _, settlement in ipairs(self.settlements) do 257 | for _, tilePos in ipairs(settlement.tiles) do 258 | -- Make sure the tile position is valid 259 | if tilePos.x >= 1 and tilePos.x <= grid.width and 260 | tilePos.y >= 1 and tilePos.y <= grid.height then 261 | 262 | local tile = grid.tiles[tilePos.y][tilePos.x] 263 | 264 | -- Get the total yield from this tile 265 | local tileYield = tile:getTotalYield() 266 | 267 | -- Add tile yields to player yields 268 | self.yields.food = self.yields.food + tileYield.food 269 | self.yields.production = self.yields.production + tileYield.production 270 | self.yields.gold = self.yields.gold + tileYield.gold 271 | end 272 | end 273 | end 274 | end 275 | 276 | function Player:draw() 277 | -- Draw player UI at the top of the screen 278 | local screenWidth = love.graphics.getWidth() 279 | local barHeight = 40 280 | 281 | -- Draw background 282 | love.graphics.setColor(0, 0, 0, 0.8) 283 | love.graphics.rectangle("fill", 0, 0, screenWidth, barHeight) 284 | 285 | -- Draw yields with icons and better formatting 286 | local iconSize = 20 287 | local padding = 10 288 | local textOffset = 25 289 | 290 | -- Food (green) 291 | love.graphics.setColor(0.2, 0.8, 0.2) 292 | -- Draw food icon (simple circle for now) 293 | love.graphics.circle("fill", padding + iconSize/2, barHeight/2, iconSize/2) 294 | love.graphics.setColor(1, 1, 1) 295 | love.graphics.print("Food: " .. self.yields.food, padding + textOffset, 10) 296 | 297 | -- Production (brown) 298 | love.graphics.setColor(0.8, 0.6, 0.2) 299 | -- Draw production icon (hammer shape) 300 | love.graphics.rectangle("fill", 150 + padding, barHeight/2 - iconSize/2, iconSize/2, iconSize) 301 | love.graphics.polygon("fill", 302 | 150 + padding + iconSize/2, barHeight/2 - iconSize/2, 303 | 150 + padding + iconSize, barHeight/2 - iconSize/4, 304 | 150 + padding + iconSize, barHeight/2 + iconSize/4, 305 | 150 + padding + iconSize/2, barHeight/2 + iconSize/2 306 | ) 307 | love.graphics.setColor(1, 1, 1) 308 | love.graphics.print("Production: " .. self.yields.production, 150 + padding + textOffset, 10) 309 | 310 | -- Gold (yellow) 311 | love.graphics.setColor(1, 0.9, 0.2) 312 | -- Draw gold icon (coin) 313 | love.graphics.circle("fill", 300 + padding + iconSize/2, barHeight/2, iconSize/2) 314 | love.graphics.setColor(0.8, 0.7, 0.1) 315 | love.graphics.circle("line", 300 + padding + iconSize/2, barHeight/2, iconSize/3) 316 | love.graphics.setColor(1, 1, 1) 317 | love.graphics.print("Gold: " .. self.yields.gold, 300 + padding + textOffset, 10) 318 | 319 | -- Draw settlement count 320 | love.graphics.setColor(1, 1, 1) 321 | 322 | -- Count total tiles in all settlements 323 | local totalTiles = 0 324 | for _, settlement in ipairs(self.settlements) do 325 | totalTiles = totalTiles + #settlement.tiles 326 | end 327 | 328 | love.graphics.print("Settlements: " .. #self.settlements .. " (" .. totalTiles .. " tiles)", 450 + padding, 10) 329 | 330 | -- Draw turn counter 331 | love.graphics.setColor(0.7, 0.7, 1.0) -- Light blue for turn counter 332 | love.graphics.print("Turn: " .. game.turn, screenWidth - 180, 10) 333 | 334 | -- Draw End Turn button 335 | self:drawEndTurnButton(screenWidth - 80, 5, 70, 30) 336 | 337 | -- Reset color 338 | love.graphics.setColor(1, 1, 1) 339 | end 340 | 341 | -- Draw the End Turn button 342 | function Player:drawEndTurnButton(x, y, width, height) 343 | -- Check if mouse is hovering over the button 344 | local mx, my = love.mouse.getPosition() 345 | local isHovering = mx >= x and mx <= x + width and my >= y and my <= y + height 346 | 347 | -- Draw button background 348 | if isHovering then 349 | love.graphics.setColor(0.4, 0.4, 0.8, 0.9) -- Brighter when hovering 350 | else 351 | love.graphics.setColor(0.3, 0.3, 0.7, 0.8) -- Normal color 352 | end 353 | love.graphics.rectangle("fill", x, y, width, height) 354 | 355 | -- Draw button border 356 | love.graphics.setColor(0.5, 0.5, 1.0, 0.9) 357 | love.graphics.rectangle("line", x, y, width, height) 358 | 359 | -- Draw button text 360 | love.graphics.setColor(1, 1, 1, 1) 361 | local font = love.graphics.getFont() 362 | local text = "End Turn" 363 | local textWidth = font:getWidth(text) 364 | local textHeight = font:getHeight() 365 | love.graphics.print(text, x + width/2 - textWidth/2, y + height/2 - textHeight/2) 366 | end 367 | 368 | -- Handle mouse press for the End Turn button 369 | function Player:handleMousePress(x, y) 370 | -- Check if the End Turn button was clicked 371 | local screenWidth = love.graphics.getWidth() 372 | local buttonX = screenWidth - 80 373 | local buttonY = 5 374 | local buttonWidth = 70 375 | local buttonHeight = 30 376 | 377 | if x >= buttonX and x <= buttonX + buttonWidth and y >= buttonY and y <= buttonY + buttonHeight then 378 | -- End the current turn 379 | game:nextTurn() 380 | return true 381 | end 382 | 383 | return false 384 | end 385 | 386 | function Player:drawOnMap(tileSize) 387 | if not self.visualX or not self.visualY then return end 388 | 389 | -- Get the effective tile size based on zoom level 390 | local effectiveTileSize = game.camera:getEffectiveTileSize() 391 | 392 | -- Draw movement path indicator if moving 393 | if self.isMoving then 394 | -- Draw a line from start to target 395 | love.graphics.setColor(1, 1, 1, 0.7) -- Brighter line 396 | local startX = (self.moveStartX - 1) * effectiveTileSize + effectiveTileSize/2 397 | local startY = (self.moveStartY - 1) * effectiveTileSize + effectiveTileSize/2 398 | local targetX = (self.moveTargetX - 1) * effectiveTileSize + effectiveTileSize/2 399 | local targetY = (self.moveTargetY - 1) * effectiveTileSize + effectiveTileSize/2 400 | 401 | -- Draw dashed line with animation 402 | local dashLength = 6 403 | local gapLength = 4 404 | local dx = targetX - startX 405 | local dy = targetY - startY 406 | local distance = math.sqrt(dx * dx + dy * dy) 407 | local steps = math.floor(distance / (dashLength + gapLength)) 408 | 409 | -- Animate the dashes by shifting them based on time 410 | local shift = (love.timer.getTime() * 10) % (dashLength + gapLength) 411 | 412 | for i = -1, steps do 413 | local t1 = math.max(0, (i * (dashLength + gapLength) + shift) / distance) 414 | local t2 = math.min(1, ((i * (dashLength + gapLength)) + dashLength + shift) / distance) 415 | 416 | if t1 < t2 then 417 | local x1 = startX + dx * t1 418 | local y1 = startY + dy * t1 419 | local x2 = startX + dx * t2 420 | local y2 = startY + dy * t2 421 | 422 | love.graphics.setLineWidth(2) -- Thicker line 423 | love.graphics.line(x1, y1, x2, y2) 424 | love.graphics.setLineWidth(1) 425 | end 426 | end 427 | 428 | -- Draw target indicator with pulsing effect 429 | local pulseScale = 0.8 + 0.2 * math.sin(love.timer.getTime() * 6) 430 | love.graphics.circle("line", targetX, targetY, tileSize/4 * pulseScale) 431 | 432 | -- Draw a small arrow at the end 433 | local arrowSize = tileSize/6 434 | local angle = math.atan2(dy, dx) 435 | local arrowX1 = targetX - arrowSize * math.cos(angle - math.pi/6) 436 | local arrowY1 = targetY - arrowSize * math.sin(angle - math.pi/6) 437 | local arrowX2 = targetX - arrowSize * math.cos(angle + math.pi/6) 438 | local arrowY2 = targetY - arrowSize * math.sin(angle + math.pi/6) 439 | 440 | love.graphics.setLineWidth(2) 441 | love.graphics.line(targetX, targetY, arrowX1, arrowY1) 442 | love.graphics.line(targetX, targetY, arrowX2, arrowY2) 443 | love.graphics.setLineWidth(1) 444 | end 445 | 446 | local screenX = (self.visualX - 1) * effectiveTileSize 447 | local screenY = (self.visualY - 1) * effectiveTileSize 448 | 449 | -- Draw player avatar (a simple circle for now) 450 | love.graphics.setColor(1, 0, 0) -- Red color for player 451 | love.graphics.circle("fill", screenX + effectiveTileSize/2, screenY + effectiveTileSize/2, effectiveTileSize/3) 452 | love.graphics.setColor(1, 1, 1) -- Reset color 453 | end 454 | 455 | -- Create a new settlement at the specified position 456 | function Player:createSettlement(x, y) 457 | -- Check if there's already a settlement at this position 458 | if self:getSettlementAt(x, y) then 459 | return false 460 | end 461 | 462 | -- Check if there's a settlement too close (within 4 tiles) 463 | for _, settlement in ipairs(self.settlements) do 464 | local distance = math.sqrt((x - settlement.centerX)^2 + (y - settlement.centerY)^2) 465 | if distance < 4 then 466 | return false 467 | end 468 | end 469 | 470 | -- Create the new settlement 471 | local settlement = Settlement.new(x, y, self) 472 | table.insert(self.settlements, settlement) 473 | 474 | -- Show a notification for the new settlement 475 | game:showNotification(NotificationType.ACHIEVEMENT, "New settlement founded: " .. settlement.name) 476 | 477 | return true 478 | end 479 | 480 | -- Get a settlement at the specified position 481 | function Player:getSettlementAt(x, y) 482 | for _, settlement in ipairs(self.settlements) do 483 | if settlement.centerX == x and settlement.centerY == y then 484 | return settlement 485 | end 486 | end 487 | return nil 488 | end 489 | 490 | -- Handle end of turn updates for the player 491 | function Player:onTurnEnd() 492 | -- Update player yields 493 | self:updateYields(game.grid) 494 | 495 | -- Here you can add other end-of-turn logic like: 496 | -- - Resource accumulation 497 | -- - Building progress 498 | -- - Settlement growth 499 | -- - Research progress 500 | -- - Unit healing 501 | end 502 | 503 | -- Check if a tile is within any of the player's settlements 504 | function Player:isTileInSettlement(x, y) 505 | for _, settlement in ipairs(self.settlements) do 506 | if settlement:containsTile(x, y) then 507 | return true, settlement 508 | end 509 | end 510 | return false, nil 511 | end 512 | 513 | -- Draw all settlements 514 | function Player:drawSettlements(tileSize, grid) 515 | -- Get the effective tile size based on zoom level 516 | local effectiveTileSize = game.camera:getEffectiveTileSize() 517 | 518 | for _, settlement in ipairs(self.settlements) do 519 | settlement:draw(effectiveTileSize, grid) 520 | end 521 | end 522 | 523 | return Player 524 | --------------------------------------------------------------------------------