renderer.lua
1 -- src/render/renderer.lua 2 -- Pixel renderer for a Kind-based world grid (chunked). 3 4 local Kind = require("src.core.kind") 5 6 local Renderer = {} 7 Renderer.__index = Renderer 8 9 local function chunk_index(self, cx, cy) 10 return (cy - 1) * self.chunk_cols + cx 11 end 12 13 function Renderer.new(world, palette, scale) 14 assert(world and world.w and world.h) 15 local self = setmetatable({}, Renderer) 16 17 self.world = world 18 self.palette = palette 19 self.scale = scale or 4 20 self.chunk_size = world.chunk_size or 16 21 self.chunk_cols = math.ceil(world.w / self.chunk_size) 22 self.chunk_rows = math.ceil(world.h / self.chunk_size) 23 self.chunks = {} 24 25 for cy = 1, self.chunk_rows do 26 for cx = 1, self.chunk_cols do 27 local x0 = (cx - 1) * self.chunk_size + 1 28 local y0 = (cy - 1) * self.chunk_size + 1 29 local w = math.min(self.chunk_size, world.w - x0 + 1) 30 local h = math.min(self.chunk_size, world.h - y0 + 1) 31 local imagedata = love.image.newImageData(w, h) 32 local image = love.graphics.newImage(imagedata) 33 image:setFilter("nearest", "nearest") 34 35 local i = chunk_index(self, cx, cy) 36 self.chunks[i] = { 37 cx = cx, 38 cy = cy, 39 x0 = x0, 40 y0 = y0, 41 w = w, 42 h = h, 43 imagedata = imagedata, 44 image = image, 45 } 46 end 47 end 48 49 self:full_rebuild() 50 51 return self 52 end 53 54 function Renderer:set_scale(scale) 55 self.scale = math.max(1, math.floor(scale)) 56 end 57 58 function Renderer:color_for_kind(kind) 59 return self.palette[kind] or self.palette.FALLBACK 60 end 61 62 function Renderer:rebuild_chunk(chunk) 63 for y = 0, chunk.h - 1 do 64 local wy = chunk.y0 + y 65 for x = 0, chunk.w - 1 do 66 local wx = chunk.x0 + x 67 local kind = self.world:get(wx, wy) or Kind.EMPTY 68 local c = self:color_for_kind(kind) 69 chunk.imagedata:setPixel(x, y, c[1], c[2], c[3], c[4]) 70 end 71 end 72 chunk.image:replacePixels(chunk.imagedata) 73 end 74 75 function Renderer:full_rebuild() 76 for i = 1, #self.chunks do 77 self:rebuild_chunk(self.chunks[i]) 78 end 79 end 80 81 function Renderer:apply_dirty_chunks(dirty_chunks) 82 if not dirty_chunks or #dirty_chunks == 0 then 83 return false 84 end 85 86 for i = 1, #dirty_chunks do 87 local entry = dirty_chunks[i] 88 local index = chunk_index(self, entry.cx, entry.cy) 89 local chunk = self.chunks[index] 90 if chunk then 91 self:rebuild_chunk(chunk) 92 end 93 end 94 95 return true 96 end 97 98 function Renderer:apply_dirty(dirty) 99 if not dirty or #dirty == 0 then 100 return false 101 end 102 103 local touched = {} 104 105 for i = 1, #dirty do 106 local p = dirty[i] 107 local index = self.world:chunk_index(p.x, p.y) 108 if index then 109 local chunk = self.chunks[index] 110 if chunk then 111 local kind = self.world:get(p.x, p.y) or Kind.EMPTY 112 local c = self:color_for_kind(kind) 113 local lx = p.x - chunk.x0 114 local ly = p.y - chunk.y0 115 chunk.imagedata:setPixel(lx, ly, c[1], c[2], c[3], c[4]) 116 touched[index] = true 117 end 118 end 119 end 120 121 for index in pairs(touched) do 122 local chunk = self.chunks[index] 123 if chunk then 124 chunk.image:replacePixels(chunk.imagedata) 125 end 126 end 127 128 return true 129 end 130 131 function Renderer:draw(x, y, scale) 132 local sx = scale or self.scale 133 x = x or 0 134 y = y or 0 135 136 love.graphics.setColor(1, 1, 1, 1) 137 for i = 1, #self.chunks do 138 local chunk = self.chunks[i] 139 local dx = x + (chunk.x0 - 1) * sx 140 local dy = y + (chunk.y0 - 1) * sx 141 love.graphics.draw(chunk.image, dx, dy, 0, sx, sx) 142 end 143 end 144 145 return Renderer