---@class Node Node = Object:extend() --Node represent any game object that needs to have some transform available in the game itself.\ --Everything that you see in the game is a Node, and some invisible things like the G.ROOM are also\ --represented here. -- ---@param args {T: table, container: Node} --**T** The transform ititializer, with keys of x|1, y|2, w|3, h|4, r|5\ --**container** optional container for this Node, defaults to G.ROOM function Node:init(args) --From args, set the values of self transform args = args or {} args.T = args.T or {} --Store all argument and return tables here for reuse, because Lua likes to generate garbage self.ARGS = self.ARGS or {} self.RETS = {} --Config table used for any metadata about this node self.config = self.config or {} --For transform init, accept params in the form x|1, y|2, w|3, h|4, r|5 self.T = { x = args.T.x or args.T[1] or 0, y = args.T.y or args.T[2] or 0, w = args.T.w or args.T[3] or 1, h = args.T.h or args.T[4] or 1, r = args.T.r or args.T[5] or 0, scale = args.T.scale or args.T[6] or 1, } --Transform to use for collision detection self.CT = self.T --Create the offset tables, used to determine things like drag offset and 3d shader effects self.click_offset = {x = 0, y = 0} self.hover_offset = {x = 0, y = 0} --To keep track of all nodes created on pause. If true, this node moves normally even when the G.TIMERS.TOTAL doesn't increment self.created_on_pause = G.SETTINGS.paused --ID tracker, every Node has a unique ID G.ID = G.ID or 1 self.ID = G.ID G.ID = G.ID + 1 --Frame tracker to aid in not doing too many extra calculations self.FRAME = { DRAW = -1, MOVE = -1 } --The states for this Node and all derived nodes. This is how we control the visibility and interactibility of any object --All nodes do not collide by default. This reduces the size of n for the O(n^2) collision detection self.states = { visible = true, collide = {can = false, is = false}, focus = {can = false, is = false}, hover = {can = true, is = false}, click = {can = true, is = false}, drag = {can = true, is = false}, release_on = {can = true, is = false} } --If we provide a container, all nodes within that container are translated with that container as the reference frame. --For example, if G.ROOM is set at x = 5 and y = 5, and we create a new game object at 0, 0, it will actually be drawn at --5, 5. This allows us to control things like screen shake, room positioning, rotation, padding, etc. without needing to modify --every game object that we need to draw self.container = args.container or G.ROOM --The list of children give Node a treelike structure. This can be used for things like drawing, deterministice movement and parallax --calculations when child nodes rely on updated information from parents, and inherited attributes like button click functions if not self.children then self.children = {} end --Add this object to the appropriate instance table only if the metatable matches with NODE if getmetatable(self) == Node then table.insert(G.I.NODE, self) end --Unless node was created during a stage transition (when G.STAGE_OBJECT_INTERRUPT is true), add all nodes to their appropriate --stage object table so they can be easily deleted on stage transition if not G.STAGE_OBJECT_INTERRUPT then table.insert(G.STAGE_OBJECTS[G.STAGE], self) end end --Draw a bounding rectangle representing the transform of this node. Used in debugging. function Node:draw_boundingrect() self.under_overlay = G.under_overlay if G.DEBUG then local transform = self.VT or self.T love.graphics.push() love.graphics.scale(G.TILESCALE, G.TILESCALE) love.graphics.translate(transform.x*G.TILESIZE+transform.w*G.TILESIZE*0.5, transform.y*G.TILESIZE+transform.h*G.TILESIZE*0.5) love.graphics.rotate(transform.r) love.graphics.translate(-transform.w*G.TILESIZE*0.5, -transform.h*G.TILESIZE*0.5) if self.DEBUG_VALUE then love.graphics.setColor(1, 1, 0, 1) love.graphics.print((self.DEBUG_VALUE or ''), transform.w*G.TILESIZE,transform.h*G.TILESIZE, nil, 1/G.TILESCALE) end love.graphics.setLineWidth(1 + (self.states.focus.is and 1 or 0)) if self.states.collide.is then love.graphics.setColor(0, 1, 0, 0.3) else love.graphics.setColor(1, 0, 0, 0.3) end if self.states.focus.can then love.graphics.setColor(G.C.GOLD) love.graphics.setLineWidth(1) end if self.CALCING then love.graphics.setColor({0,0,1,1}) love.graphics.setLineWidth(3) end love.graphics.rectangle('line', 0, 0, transform.w*G.TILESIZE,transform.h*G.TILESIZE, 3) love.graphics.pop() end end --Draws self, then adds self the the draw hash, then draws all children function Node:draw() self:draw_boundingrect() if self.states.visible then add_to_drawhash(self) for _, v in pairs(self.children) do v:draw() end end end --Determines if this node collides with some point. Applies any container translations and rotations, then\ --applies translations and rotations specific to this node. This means the collision detection effectively\ --determines if some point intersects this node regargless of rotation. -- ---@param point {x: number, y: number} --**x and y** The coordinates of the cursor transformed into game units function Node:collides_with_point(point) --First reset the collision state to false if self.container then local T = self.CT or self.T self.ARGS.collides_with_point_point = self.ARGS.collides_with_point_point or {} self.ARGS.collides_with_point_translation = self.ARGS.collides_with_point_translation or {} self.ARGS.collides_with_point_rotation = self.ARGS.collides_with_point_rotation or {} local _p = self.ARGS.collides_with_point_point local _t = self.ARGS.collides_with_point_translation local _r = self.ARGS.collides_with_point_rotation local _b = self.states.hover.is and G.COLLISION_BUFFER or 0 _p.x, _p.y = point.x, point.y if self.container ~= self then --if there is some valid container, we need to apply all translations and rotations for the container first if math.abs(self.container.T.r) < 0.1 then --Translate to normalize this Node to the center of the container _t.x, _t.y = -self.container.T.w/2, -self.container.T.h/2 point_translate(_p, _t) --Rotate node about the center of the container point_rotate(_p, self.container.T.r) --Translate node to undo the container translation, essentially reframing it in 'container' space _t.x, _t.y = self.container.T.w/2-self.container.T.x, self.container.T.h/2-self.container.T.y point_translate(_p, _t) else --Translate node to undo the container translation, essentially reframing it in 'container' space _t.x, _t.y = -self.container.T.x, -self.container.T.y point_translate(_p, _t) end end if math.abs(T.r) < 0.1 then --If we can essentially disregard transform rotation, just treat it like a normal rectangle if _p.x >= T.x - _b and _p.y >= T.y - _b and _p.x <= T.x + T.w + _b and _p.y <= T.y + T.h + _b then return true end else --Otherwise we need to do some silly point rotation garbage to determine if the point intersects the rotated rectangle _r.cos, _r.sin = math.cos(T.r+math.pi/2), math.sin(T.r+math.pi/2) _p.x, _p.y = _p.x - (T.x + 0.5*(T.w)), _p.y - (T.y + 0.5*(T.h)) _t.x, _t.y = _p.y*_r.cos - _p.x*_r.sin, _p.y*_r.sin + _p.x*_r.cos _p.x, _p.y = _t.x + (T.x + 0.5*(T.w)), _t.y + (T.y + 0.5*(T.h)) if _p.x >= T.x - _b and _p.y >= T.y - _b and _p.x <= T.x + T.w + _b and _p.y <= T.y + T.h + _b then return true end end end end --Sets the offset of passed point in terms of this nodes T.x and T.y -- ---@param point {x: number, y: number} ---@param type string --**x and y** The coordinates of the cursor transformed into game units --**type** the type of offset to set for this Node, either 'Click' or 'Hover' function Node:set_offset(point, type) self.ARGS.set_offset_point = self.ARGS.set_offset_point or {} self.ARGS.set_offset_translation = self.ARGS.set_offset_translation or {} local _p = self.ARGS.set_offset_point local _t = self.ARGS.set_offset_translation _p.x, _p.y = point.x, point.y --Translate to middle of the container _t.x = -self.container.T.w/2 _t.y = -self.container.T.h/2 point_translate(_p, _t) --Rotate about the container midpoint according to node rotation point_rotate(_p, self.container.T.r) --Translate node to undo the container translation, essentially reframing it in 'container' space _t.x = self.container.T.w/2-self.container.T.x _t.y = self.container.T.h/2-self.container.T.y point_translate(_p, _t) if type == 'Click' then self.click_offset.x = (_p.x - self.T.x) self.click_offset.y = (_p.y - self.T.y) elseif type == 'Hover' then self.hover_offset.x = (_p.x - self.T.x) self.hover_offset.y = (_p.y - self.T.y) end end --If the current container is being 'Dragged', usually by a cursor, determine if any drag popups need to be generated and do so function Node:drag() if self.config and self.config.d_popup then if not self.children.d_popup then self.children.d_popup = UIBox{ definition = self.config.d_popup, config = self.config.d_popup_config } self.children.h_popup.states.collide.can = false table.insert(G.I.POPUP, self.children.d_popup) self.children.d_popup.states.drag.can = true end end end --Determines if this Node can be dragged. This is a simple function but more complex objects may redefine this to return a parent\ --if the parent needs to drag other children with it function Node:can_drag() return self.states.drag.can and self or nil end --Called by the CONTROLLER when this node is no longer being dragged, removes any d_popups function Node:stop_drag() if self.children.d_popup then for k, v in pairs(G.I.POPUP) do if v == self.children.d_popup then table.remove(G.I.POPUP, k) end end self.children.d_popup:remove() self.children.d_popup = nil end end --If the current container is being 'Hovered', usually by a cursor, determine if any hover popups need to be generated and do so function Node:hover() if self.config and self.config.h_popup then if not self.children.h_popup then self.config.h_popup_config.instance_type = 'POPUP' self.children.h_popup = UIBox{ definition = self.config.h_popup, config = self.config.h_popup_config, } self.children.h_popup.states.collide.can = false self.children.h_popup.states.drag.can = true end end end --Called by the CONTROLLER when this node is no longer being hovered, removes any h_popups function Node:stop_hover() if self.children.h_popup then self.children.h_popup:remove() self.children.h_popup = nil end end --Called by the CONTROLLER to determine the position the cursor should be set to for this node function Node:put_focused_cursor() return (self.T.x + self.T.w/2 + self.container.T.x)*(G.TILESCALE*G.TILESIZE), (self.T.y + self.T.h/2 + self.container.T.y)*(G.TILESCALE*G.TILESIZE) end --Sets the container of this node and all child nodes to be a new container node -- ---@param container Node The new node that will behave as this nodes container function Node:set_container(container) if self.children then for _, v in pairs(self.children) do v:set_container(container) end end self.container = container end --Translation function used before any draw calls, translates this node according to the transform of the container node function Node:translate_container() if self.container and self.container ~= self then love.graphics.translate(self.container.T.w*G.TILESCALE*G.TILESIZE*0.5, self.container.T.h*G.TILESCALE*G.TILESIZE*0.5) love.graphics.rotate(self.container.T.r) love.graphics.translate( -self.container.T.w*G.TILESCALE*G.TILESIZE*0.5 + self.container.T.x*G.TILESCALE*G.TILESIZE, -self.container.T.h*G.TILESCALE*G.TILESIZE*0.5 + self.container.T.y*G.TILESCALE*G.TILESIZE) end end --When this Node needs to be deleted, removes self from any tables it may have been added to to destroy any weak references\ --Also calls the remove method of all children to have them do the same function Node:remove() for k, v in pairs(G.I.POPUP) do if v == self then table.remove(G.I.POPUP, k) break; end end for k, v in pairs(G.I.NODE) do if v == self then table.remove(G.I.NODE, k) break; end end for k, v in pairs(G.STAGE_OBJECTS[G.STAGE]) do if v == self then table.remove(G.STAGE_OBJECTS[G.STAGE], k) break; end end if self.children then for k, v in pairs(self.children) do v:remove() end end if G.CONTROLLER.clicked.target ==self then G.CONTROLLER.clicked.target = nil end if G.CONTROLLER.focused.target ==self then G.CONTROLLER.focused.target = nil end if G.CONTROLLER.dragging.target ==self then G.CONTROLLER.dragging.target = nil end if G.CONTROLLER.hovering.target ==self then G.CONTROLLER.hovering.target = nil end if G.CONTROLLER.released_on.target ==self then G.CONTROLLER.released_on.target = nil end if G.CONTROLLER.cursor_down.target ==self then G.CONTROLLER.cursor_down.target = nil end if G.CONTROLLER.cursor_up.target ==self then G.CONTROLLER.cursor_up.target = nil end if G.CONTROLLER.cursor_hover.target ==self then G.CONTROLLER.cursor_hover.target = nil end self.REMOVED = true end --returns the squared(fast) distance in game units from the center of this node to the center of another node -- ---@param other_node Node to measure the distance from function Node:fast_mid_dist(other_node) return math.sqrt((other_node.T.x + 0.5*other_node.T.w) - (self.T.x + self.T.w))^2 + ((other_node.T.y + 0.5*other_node.T.h) - (self.T.y + self.T.h))^2 end --Prototype for a click release function, when the cursor is released on this node function Node:release(dragged) end --Prototype for a click function function Node:click() end --Prototype animation function for any frame manipulation needed function Node:animate() end --Prototype update function for any object specific logic that needs to occur every frame function Node:update(dt) end