local Tilemap = require("tilemap") local Entity = require("entity") local Player = require("player") local Camera = require("camera") local World = {} World.__index = World local GRAVITY = 9.81 * 64 local PHYSICS_DT = 1 / 60 function World:new() local self = setmetatable({}, World) self.physicsWorld = nil self.tilemap = nil self.mapData = nil self.groundEntities = {} self.player = nil self.enemies = {} self.entities = {} self.liquidPolygons = {} self.liquidSurfaces = {} self.liquidSurfaceFixtures = {} self.camera = nil self.groundContacts = {} self.waterContacts = {} self.contactCounts = {} self.contactEntity = {} self.contactKind = {} return self end function World:load(mapPath, tilesets) if self.physicsWorld then self.groundEntities = {} end self.physicsWorld = love.physics.newWorld(0, GRAVITY) self.physicsWorld:setCallbacks( function(fa, fb, contact) local udA, udB = fa:getUserData(), fb:getUserData() local nx, ny = contact:getNormal() self:_onBeginContact(udA, udB, nx, ny, contact) end, function(fa, fb, contact) local udA, udB = fa:getUserData(), fb:getUserData() self:_onEndContact(udA, udB, contact) end ) self.tilemap = Tilemap:new(mapPath, tilesets) self.mapData = self.tilemap:getMapData() if not self.mapData then error("World:load - no map data from " .. tostring(mapPath)) end local groundDims = self.tilemap:getLayerTileDimensions("ground") local tileWidth = groundDims.tilewidth local tileHeight = groundDims.tileheight local groundLayer = self.tilemap:getGroundLayer() if groundLayer and groundLayer.data then local mapW = self.mapData.width or 0 local mapH = self.mapData.height or 0 local data = groundLayer.data local solid = {} for row = 0, mapH - 1 do solid[row] = {} for col = 0, mapW - 1 do local idx = row * mapW + col + 1 local gid = data[idx] solid[row][col] = (gid and gid ~= 0) end end for row = 0, mapH - 1 do local col = 0 while col < mapW do if solid[row][col] then local startCol = col while col < mapW and solid[row][col] do col = col + 1 end local stripWidth = (col - startCol) * tileWidth local x = startCol * tileWidth local y = row * tileHeight local strip = Entity:new(x, y, stripWidth, tileHeight) strip:enablePhysics(self.physicsWorld, "static") strip.fixture:setUserData("ground") table.insert(self.groundEntities, strip) else col = col + 1 end end end end self.player = nil self.enemies = {} self.entities = {} self.liquidPolygons = self.tilemap:getEntitiesLiquidPolygons() or {} self.liquidSurfaces = self.tilemap:getEntitiesLiquidSurfaces() or {} self.liquidSurfaceFixtures = {} local spawns = self.tilemap:getEntitiesSpawns() for _, spawn in ipairs(spawns) do local entityType = spawn:get("entity", "") if entityType == "Player" and not self.player then self.player = Player.new(self.physicsWorld, spawn.x, spawn.y) self.player.isPlayer = true table.insert(self.entities, self.player) else local e = Entity:new(spawn.x, spawn.y, spawn.width, spawn.height) e:setPropertiesFromOptions({ properties = spawn.properties or {}, name = spawn.name, type = spawn.type }) if entityType ~= "" then table.insert(self.enemies, e) end table.insert(self.entities, e) end end for _, surface in ipairs(self.liquidSurfaces) do local cx = (surface.x or 0) + (surface.width or 0) * 0.5 local cy = (surface.y or 0) + (surface.height or 0) * 0.5 local body = love.physics.newBody(self.physicsWorld, cx, cy, "static") local shape = love.physics.newRectangleShape(surface.width or 0, surface.height or 0) local fixture = love.physics.newFixture(body, shape, 1) fixture:setSensor(true) fixture:setUserData({ type = "water", water = surface }) table.insert(self.liquidSurfaceFixtures, { body = body, fixture = fixture }) end end function World:_addContact(entity, kind) self.contactCounts[entity] = self.contactCounts[entity] or { floor = 0, wall = 0, ceiling = 0 } self.contactCounts[entity][kind] = (self.contactCounts[entity][kind] or 0) + 1 entity.contact = entity.contact or { floor = 0, wall = 0, ceiling = 0 } entity.contact[kind] = 1 end function World:_removeContact(entity, kind) self.contactCounts[entity] = self.contactCounts[entity] or { floor = 0, wall = 0, ceiling = 0 } self.contactCounts[entity][kind] = math.max(0, (self.contactCounts[entity][kind] or 0) - 1) entity.contact = entity.contact or { floor = 0, wall = 0, ceiling = 0 } entity.contact[kind] = (self.contactCounts[entity][kind] > 0) and 1 or 0 end function World:_normalToContactType(nx, ny) local ax, ay = math.abs(nx), math.abs(ny) if ay >= ax then if ny < -0.3 then return "floor" end if ny > 0.3 then return "ceiling" end else if ax > 0.3 then return "wall" end end return nil end function World:_isTrackedEntity(ud) return type(ud) == "table" and (ud.body ~= nil or ud.x ~= nil) end function World:_isPlayerLike(ud) return type(ud) == "table" and ud.jump ~= nil end function World:_onBeginContact(udA, udB, nx, ny, contact) if udA == "ground" and self:_isTrackedEntity(udB) then local kind = self:_normalToContactType(nx, ny) if kind then self:_addContact(udB, kind) self.contactEntity[contact] = udB self.contactKind[contact] = kind end self.groundContacts[udB] = (self.groundContacts[udB] or 0) + 1 if self:_isPlayerLike(udB) then udB.grounded = true end elseif udB == "ground" and self:_isTrackedEntity(udA) then local kind = self:_normalToContactType(-nx, -ny) if kind then self:_addContact(udA, kind) self.contactEntity[contact] = udA self.contactKind[contact] = kind end self.groundContacts[udA] = (self.groundContacts[udA] or 0) + 1 if self:_isPlayerLike(udA) then udA.grounded = true end end local function doWaterSplash(waterData, entity) if not (waterData and waterData.water and waterData.water.splash) then return end local centerX = 0 local ok, x1, y1, x2, y2 = pcall(function() return contact:getPositions() end) if ok and x1 then centerX = (x1 + (x2 or x1)) * 0.5 elseif entity and entity.body then centerX = entity.body:getX() else centerX = waterData.water.x or 0 end waterData.water:splash(centerX, 2) end if type(udA) == "table" and udA.type == "water" and self:_isTrackedEntity(udB) then self.waterContacts[udB] = (self.waterContacts[udB] or 0) + 1 doWaterSplash(udA, udB) elseif type(udB) == "table" and udB.type == "water" and self:_isTrackedEntity(udA) then self.waterContacts[udA] = (self.waterContacts[udA] or 0) + 1 doWaterSplash(udB, udA) end end function World:_onEndContact(udA, udB, contact) local ent = self.contactEntity[contact] local kind = ent and self.contactKind[contact] if ent and kind then self:_removeContact(ent, kind) self.contactEntity[contact] = nil self.contactKind[contact] = nil end if type(udA) == "table" and udA.type == "water" and self:_isTrackedEntity(udB) then self.waterContacts[udB] = math.max(0, (self.waterContacts[udB] or 0) - 1) elseif type(udB) == "table" and udB.type == "water" and self:_isTrackedEntity(udA) then self.waterContacts[udA] = math.max(0, (self.waterContacts[udA] or 0) - 1) end if udA == "ground" and self:_isTrackedEntity(udB) then self.groundContacts[udB] = math.max(0, (self.groundContacts[udB] or 0) - 1) if self:_isPlayerLike(udB) then udB.grounded = (self.groundContacts[udB] or 0) > 0 end elseif udB == "ground" and self:_isTrackedEntity(udA) then self.groundContacts[udA] = math.max(0, (self.groundContacts[udA] or 0) - 1) if self:_isPlayerLike(udA) then udA.grounded = (self.groundContacts[udA] or 0) > 0 end end end function World:setCamera(camera) self.camera = camera self.camera:setLimits(self.tilemap:getCameraLimits()) end function World:getPlayer() return self.player end function World:getPhysicsWorld() return self.physicsWorld end function World:getMapData() return self.mapData end local function drawTileLayer(layer, mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) if not layer or not layer.visible or not layer.data then return end local w = layer.width or 0 local h = layer.height or 0 if w == 0 or h == 0 then return end local minCol, maxCol, minRow, maxRow if viewMinX and viewMaxX and viewMinY and viewMaxY then minCol = math.max(0, math.floor(viewMinX / mapTileW)) maxCol = math.min(w - 1, math.floor(viewMaxX / mapTileW)) minRow = math.max(0, math.floor(viewMinY / mapTileH)) maxRow = math.min(h - 1, math.floor(viewMaxY / mapTileH)) else minCol, maxCol = 0, w - 1 minRow, maxRow = 0, h - 1 end local batches = {} local fallbacks = {} local data = layer.data for ty = minRow, maxRow do for tx = minCol, maxCol do local idx = ty * w + tx + 1 local gid = data[idx] if gid and gid ~= 0 then local x = tx * mapTileW local y = ty * mapTileH local info = tileGidInfo and tileGidInfo[gid] if info then local img = info.image batches[img] = batches[img] or {} table.insert(batches[img], { quad = info.quad, x = x, y = y }) else table.insert(fallbacks, { x = x, y = y, gid = gid }) end end end end for img, tiles in pairs(batches) do local batch = love.graphics.newSpriteBatch(img, #tiles, "static") for _, t in ipairs(tiles) do batch:add(t.quad, t.x, t.y) end love.graphics.draw(batch) end for _, t in ipairs(fallbacks) do local r = ((t.gid * 17) % 256) / 255 local g = ((t.gid * 31 + 50) % 256) / 255 local b = ((t.gid * 47 + 100) % 256) / 255 love.graphics.setColor(r, g, b, 1) love.graphics.rectangle("fill", t.x, t.y, mapTileW, mapTileH) love.graphics.setColor(1, 1, 1, 1) end end function World:draw() local mapTileW = self.tilemap:getTileWidth() local mapTileH = self.tilemap:getTileHeight() local tileGidInfo = self.tilemap:getTileGidInfo() local viewMinX, viewMinY, viewMaxX, viewMaxY if self.camera then local cx, cy = self.camera.x, self.camera.y local cw = self.camera.width or 0 local ch = self.camera.height or 0 local pad = math.max(mapTileW, mapTileH) * 2 viewMinX = cx - pad viewMinY = cy - pad viewMaxX = cx + cw + pad viewMaxY = cy + ch + pad end drawTileLayer(self.tilemap:getBackgroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) drawTileLayer(self.tilemap:getGroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) love.graphics.setColor(0.25, 0.5, 0.9, 0.6) for _, liquid in ipairs(self.liquidPolygons) do if liquid.triangles then for _, tri in ipairs(liquid.triangles) do love.graphics.polygon("fill", tri) end end end love.graphics.setColor(1,1,1,1) for _, surface in ipairs(self.liquidSurfaces) do if surface.draw then surface:draw() end end for _, p in ipairs(self.liquidPolygons) do love.graphics.circle("fill", p.x, p.y, 3) end for _, e in ipairs(self.entities) do if e.draw then e:draw() else World.drawEntityDefault(e) end end if DEBUG then World.drawPhysicsBodyOutlines(self.entities) World.drawPhysicsBodyOutlines(self.groundEntities) end drawTileLayer(self.tilemap:getForegroundLayer(), mapTileW, mapTileH, tileGidInfo, viewMinX, viewMinY, viewMaxX, viewMaxY) end function World.drawPhysicsBodyOutlines(entityList) if not entityList then return end love.graphics.setColor(0, 1, 0, 1) for _, e in ipairs(entityList) do local body = e.body if body and e.physicsWidth and e.physicsHeight then local cx, cy = body:getX(), body:getY() local x = math.floor(cx - e.physicsWidth / 2) local y = math.floor(cy - e.physicsHeight / 2) love.graphics.rectangle("line", x, y, e.physicsWidth, e.physicsHeight) end end love.graphics.setColor(1, 1, 1, 1) end function World.drawEntityDefault(entity) love.graphics.setColor(0.2, 0.6, 1, 1) if entity.isPlayer then love.graphics.setColor(1, 0.3, 0.2, 1) end love.graphics.rectangle("fill", math.floor(entity.x), math.floor(entity.y), entity.width, entity.height) love.graphics.setColor(1, 1, 1, 1) end function World:update(dt) if not self.physicsWorld then return end self.physicsWorld:update(PHYSICS_DT) for _, e in ipairs(self.entities) do if e.body then e:syncFromPhysicsBody() end end if self.player then self.player.isInLiquid = false self.player.waterSurfaceContact = (self.waterContacts[self.player] or 0) > 0 for _, liquid in ipairs(self.liquidPolygons) do if liquid:containsEntity(self.player) then self.player.isInLiquid = true break end end end for _, e in ipairs(self.entities) do if e.update then e:update(dt) end end for _, liquid in ipairs(self.liquidPolygons) do if liquid.update then liquid:update(dt) end end for _, surface in ipairs(self.liquidSurfaces) do if surface.update then surface:update(dt) end end if self.camera then self.camera:setTarget(self.player) self.camera:update(dt) end end return World