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.camera = nil self.groundContacts = {} self.contactCounts = {} self.contactEntity = {} self.contactKind = {} return self end function World:load(mapPath, tilesetPath) 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, tilesetPath) self.mapData = self.tilemap:getMapData() if not self.mapData then error("World:load - no map data from " .. tostring(mapPath)) end local tileWidth = self.tilemap:getTileWidth() local tileHeight = self.tilemap:getTileHeight() 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 = {} 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 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 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 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, tileWidth, tileHeight, tilesetImage, tileQuads) if not layer or not layer.visible or not layer.data then return end local w = layer.width or 0 local data = layer.data for i, gid in ipairs(data) do if gid and gid ~= 0 then local idx = i - 1 local tx = idx % w local ty = math.floor(idx / w) local x = tx * tileWidth local y = ty * tileHeight if tilesetImage and tileQuads and tileQuads[gid] then love.graphics.draw(tilesetImage, tileQuads[gid], x, y) else local r = ((gid * 17) % 256) / 255 local g = ((gid * 31 + 50) % 256) / 255 local b = ((gid * 47 + 100) % 256) / 255 love.graphics.setColor(r, g, b, 1) love.graphics.rectangle("fill", x, y, tileWidth, tileHeight) love.graphics.setColor(1, 1, 1, 1) end end end end function World:draw() local tw = self.tilemap:getTileWidth() local th = self.tilemap:getTileHeight() local tilesetImage = self.tilemap:getTilesetImage() local tileQuads = self.tilemap:getTileQuads() drawTileLayer(self.tilemap:getBackgroundLayer(), tw, th, tilesetImage, tileQuads) drawTileLayer(self.tilemap:getGroundLayer(), tw, th, tilesetImage, tileQuads) 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(), tw, th, tilesetImage, tileQuads) 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 = cx - e.physicsWidth / 2 local y = 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", entity.x, 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) if self.camera then self.camera:setTarget(self.player) self.camera:update(dt) end for _, e in ipairs(self.entities) do if e.body then e:syncFromPhysicsBody() end if e.update then e:update(dt) end end end return World