function shared_init() local colors = { white = ColorRamp(Color(1, 1, 1, 1), 0.025), black = ColorRamp(Color(0, 0, 0, 1), 0.025), bg = ColorRamp(Color'#303030', 0.025), fg = ColorRamp(Color'#dadada', 0.025), fg_alt = ColorRamp(Color'#b0a89f', 0.025), yellow = ColorRamp(Color'#facf00', 0.025), orange = ColorRamp(Color'#f07021', 0.025), blue = ColorRamp(Color'#019bd6', 0.025), green = ColorRamp(Color'#8bbf40', 0.025), red = ColorRamp(Color'#e91d39', 0.025), purple = ColorRamp(Color'#8e559e', 0.025), } for name, color in pairs(colors) do _G[name] = color end modal_transparent = Color(0.1, 0.1, 0.1, 0.5) fg_transparent = Color(fg[0].r, fg[0].g, fg[0].b, 0.5) graphics.set_background_color(bg[0]) graphics.set_color(fg[0]) slow_amount = 1 sfx = SoundTag() sfx.volume = 0.5 music = SoundTag() music.volume = 0.5 fat_font = Font('FatPixelFont', 8) pixul_font = Font('PixulBrush', 8) main_canvas = Canvas(gw, gh, {stencil = true}) shadow_canvas = Canvas(gw, gh) shadow_shader = Shader(nil, 'shadow.frag') end function shared_draw(draw_action) main_canvas:draw_to(function() draw_action() if flashing then graphics.rectangle(gw/2, gh/2, gw, gh, nil, nil, flash_color) end end) shadow_canvas:draw_to(function() graphics.set_color(white[0]) shadow_shader:set() main_canvas:draw2(0, 0, 0, 1, 1) shadow_shader:unset() end) shadow_canvas:draw(6, 6, 0, sx, sy) main_canvas:draw(0, 0, 0, sx, sy) end SpawnEffect = Object:extend() SpawnEffect:implement(GameObject) function SpawnEffect:init(args) self:init_game_object(args) self.target_color = self.color or red[0] self.color = fg[0] self.rs = 0 self.t:tween(0.1, self, {rs = 6}, math.cubic_in_out, function() if self.action then self.action(self.x, self.y) end self.spring:pull(1) for i = 1, random:int(6, 8) do HitParticle{group = main.current.effects, self.x, self.y, color = self.target_color, duration = random:float(0.3, 0.5), w = random:float(5, 8), v = random:float(150, 200)} end self.t:tween(0.25, self, {rs = 0}, math.linear, function() self.dead = true end) self.t:after(0.15, function() self.color = self.target_color end) end) end function SpawnEffect:update(dt) self:update_game_object(dt) end function SpawnEffect:draw() graphics.circle(self.x, self.y, random:float(0.9, 1.1)*self.rs*self.spring.x, self.color) end -- Mixin to be added to a state so it can have nodemap creation, saving and manipulating capabilities. Nodemap = Object:extend() -- nodemap is a table that contains the definition of the skill tree or overmap. -- Each node in it should have the following attributes defined: -- .x, .y, .neighbors or .links. Optionally: .rs, .color, .visited, .can_be_visited, .on_visit, .on_draw, .data -- An example would look like this: -- nodemap = { -- [1] = {x = x, y = y, visited = true, links = {2, 3, 4, 5}} -- [2] = {x = x, y = y - 64, links = {1}} -- [3] = {x = x + 64, y = y, links = {1}} -- [4] = {x = x, y = y + 64, links = {1}} -- [5] = {x = x - 64, y = y, links = {1}} -- } -- .data can be a table that contains other attributes. These will be automatically added to the Node object when it's created. -- .on_visit should be defined if you want the button to do something when it's clicked. Example: -- [2] = {x = x, y = y - 64, links = {1}, on_visit = function(visited_node) state.goto'level_2' end} -- .on_draw should be used when you want to draw the node in some specific way rather than the default one. -- color_mode can be nil or 'skill_tree', if it's the latter then nodes and edges will change colors according to if they were a part of a skill tree: -- Unvisitable nodes are gray, visitable nodes are white, visited nodes are their default color -- This is opposed to the default color mode, where all nodes and edges have their their default colors, and when they're visited they turn gray instead. function Nodemap:generate_nodemap(group, nodemap, color_mode) for id, node in pairs(nodemap) do Node{group = group, x = node.x, y = node.y, node_id = id, neighbors = node.links, rs = node.rs, visited = node.visited, can_be_visited = node.can_be_visited, color = node.color, label = node.label, data = node.data, on_visit = node.on_visit, on_draw = node.on_draw, color_mode = color_mode} end for id, node in pairs(nodemap) do for _, node_id in ipairs(node.links) do if nodemap[node_id] then Edge{group = group, x = 0, y = 0, node1_id = id, node2_id = node_id, color_mode = color_mode} end end end end Node = Object:extend() Node:implement(GameObject) function Node:init(args) self:init_game_object(args) for k, v in pairs(self.data) do self[k] = v end self.data = nil self.rs = self.rs or 6 self.shape = Circle(self.x, self.y, 1.5*self.rs) self.interact_with_mouse = true self.src_color = self.color if self.color_mode == 'skill_tree' then self.color = bg[5] if self.visited then self.color = fg[0] end end self.t:every_immediate(1.4, function() if self.can_be_visited then self.t:tween(0.7, self, {sx = 0.9, sy = 0.9}, math.linear, function() self.t:tween(0.7, self, {sx = 1.1, sy = 1.1}, math.linear, nil, 'visit_pulse_1') end, 'visit_pulse_2') end end, nil, nil, 'visit_pulse') if self.label then self.label_r = 0 end self.t:after(0.01, function() if self.label then local xs, ys = 0, 0 for _, neighbor_id in ipairs(self.neighbors) do local neighbor = self.group:get_object_by_property('node_id', neighbor_id) local r = math.angle(self.x, self.y, neighbor.x, neighbor.y) x, y = math.cos(r), math.sin(r) xs = xs + x ys = ys + y end self.label_r = Vector(-xs, -ys):angle() self.label_color = self.color end self.edges = {} for _, neighbor_id in ipairs(self.neighbors) do local edge = nil edge = self.group:get_object_by_properties({'node1_id', 'node2_id'}, {self.node_id, neighbor_id}) if not edge then edge = self.group:get_object_by_properties({'node1_id', 'node2_id'}, {neighbor_id, self.node_id}) end if edge then table.insert(self.edges, edge) end end end) end function Node:update(dt) self:update_game_object(dt) if not self.visited then for _, neighbor_id in ipairs(self.neighbors) do local neighbor = self.group:get_object_by_property('node_id', neighbor_id) if neighbor and neighbor.visited then self.can_be_visited = true end end end if self.color_mode == 'skill_tree' then if self.can_be_visited then self.color = bg[10] if self.label then self.label_color = bg[10] end if self.hot then self.color = fg[0] if self.label then self.label_color = fg[0] end end end end if self.hot and self.can_be_visited and not self.visited and input.m1.pressed then self.t:cancel'visit_pulse' self.t:cancel'visit_pulse_1' self.t:cancel'visit_pulse_2' self.sx, self.sy = 1, 1 self.spring:pull(0.25) self.can_be_visited = false self.visited = true self.hot = false if self.color_mode == 'skill_tree' then self.color = self.src_color if self.label then self.label_color = self.src_color end else self.color = bg[5] if self.label then self.label_color = bg[5] end end if self.label then self.label_color = self.color end if self.on_visit then self:on_visit() end end end function Node:draw() if self.on_draw then self:on_draw() else graphics.push(self.x, self.y, 0, self.spring.x*self.sx, self.spring.x*self.sy) if self.hot and self.can_be_visited then graphics.circle(self.x, self.y, 1.15*self.shape.rs, self.color) else graphics.circle(self.x, self.y, self.shape.rs, self.color, 3) end graphics.pop() end if self.label then local w = pixul_font:get_text_width(self.label) local s = math.remap(self.rs, 6, 12, 3.5, 2.5) graphics.print_centered(self.label, pixul_font, self.x + (w/6)*math.cos(self.label_r) + s*self.rs*math.cos(self.label_r), self.y + s*self.rs*math.sin(self.label_r), 0, self.spring.x*self.sx, self.spring.x*self.sy, nil, nil, self.label_color) end end function Node:on_mouse_enter() self.hot = true self.spring:pull(0.2, 200, 10) for _, edge in ipairs(self.edges) do edge.spring:pull(0.15, 200, 10) end end function Node:on_mouse_exit() self.hot = false self.spring:pull(0.05, 200, 10) for _, edge in ipairs(self.edges) do edge.spring:pull(0.05, 200, 10) end end Edge = Object:extend() Edge:implement(GameObject) function Edge:init(args) self:init_game_object(args) self.node1 = self.group:get_object_by_property('node_id', self.node1_id) self.node2 = self.group:get_object_by_property('node_id', self.node2_id) local r = math.angle(self.node1.x, self.node1.y, self.node2.x, self.node2.y) self.x1, self.y1 = self.node1.x + 2.75*self.node1.rs*math.cos(r), self.node1.y + 2.75*self.node1.rs*math.sin(r) self.x2, self.y2 = self.node2.x + 2.75*self.node2.rs*math.cos(r - math.pi), self.node2.y + 2.75*self.node2.rs*math.sin(r - math.pi) if self.color_mode == 'skill_tree' then self.color = bg[5] else self.color = fg[0] end end function Edge:update(dt) self:update_game_object(dt) if self.color_mode == 'skill' then self.color = bg[5] if (self.node1.visited and self.node2.can_be_visited) or (self.node2.visited and self.node1.can_be_visited) then self.color = bg[10] end if (self.node1.visited and self.node2.visited) or (self.node1.visited and self.node2.hot) or (self.node2.visited and self.node1.hot) then self.color = fg[0] end else if self.node1.visited and self.node2.visited then self.color = bg[5] end end --[[ self.color = bg[5] if (self.node1.visited and self.node2.can_be_visited) or (self.node2.visited and self.node1.can_be_visited) then self.color = bg[10] end if (self.node1.visited and self.node2.visited) or (self.node1.visited and self.node2.hot) or (self.node2.visited and self.node1.hot) then self.color = fg[0] end ]]-- end function Edge:draw() graphics.push((self.x1+self.x2)/2, (self.y1+self.y2)/2, 0, self.spring.x, self.spring.x) graphics.line(self.x1, self.y1, self.x2, self.y2, self.color, 4) graphics.circle(self.x1, self.y1, 2, self.color) graphics.circle(self.x2, self.y2, 2, self.color) graphics.pop() end HoverCrosshair = Object:extend() HoverCrosshair:implement(GameObject) function HoverCrosshair:init(args) self:init_game_object(args) self.ox, self.oy = 0, 0 self.sx, self.sy = 0, 0 self.line_width = 2 self.w, self.h = 10, 10 end function HoverCrosshair:update(dt) self:update_game_object(dt) if self.animation then self.animation:update(dt) end end function HoverCrosshair:draw() graphics.push(self.x, self.y, 0, self.sx*self.spring.x, self.sy*self.spring.x) graphics.polyline(fg[0], self.line_width, self.x - self.ox, self.y - self.oy + 0.4*self.h, self.x - self.ox, self.y - self.oy, self.x - self.ox + 0.4*self.w, self.y - self.oy) graphics.polyline(fg[0], self.line_width, self.x - self.ox, self.y + self.oy - 0.4*self.h, self.x - self.ox, self.y + self.oy, self.x - self.ox + 0.4*self.w, self.y + self.oy) graphics.polyline(fg[0], self.line_width, self.x + self.ox - 0.4*self.w, self.y - self.oy, self.x + self.ox, self.y - self.oy, self.x + self.ox, self.y - self.oy + 0.4*self.h) graphics.polyline(fg[0], self.line_width, self.x + self.ox - 0.4*self.w, self.y + self.oy, self.x + self.ox, self.y + self.oy, self.x + self.ox, self.y + self.oy - 0.4*self.h) graphics.pop() end function HoverCrosshair:activate(x, y, w, h, line_width) w, h = 10, 10 line_width = 2 self.x, self.y = camera:get_local_coords(x, y) self.t:cancel'deactivate' self.t:tween(0.1, self, {sx = 1, sy = 1}, math.cubic_in_out, function() self.sx, self.sy = 1, 1 end, 'activate') if self.w <= 10 and self.h <= 10 then self.animation = AnimationLogic(0.075, 3, 'bounce', { function() self.ox, self.oy = 0.6*self.w, 0.6*self.h end, function() self.ox, self.oy = 0.8*self.w, 0.8*self.h end, function() self.ox, self.oy = self.w, self.h end, }) else self.animation = AnimationLogic(0.075, 3, 'bounce', { function() self.ox, self.oy = 0.8*self.w, 0.8*self.h end, function() self.ox, self.oy = 0.9*self.w, 0.9*self.h end, function() self.ox, self.oy = self.w, self.h end, }) end end function HoverCrosshair:deactivate() self.t:cancel'activate' self.t:tween(0.05, self, {sx = 0, sy = 0}, math.linear, function() self.sx, self.sy = 0, 0 end, 'deactivate') end TransitionEffect = Object:extend() TransitionEffect:implement(GameObject) function TransitionEffect:init(args) self:init_game_object(args) if not self.text then error('TransitionEffect must have a Text object defined to the .text attribute') end self.rs = 0 self.text_sx, self.text_sy = 0, 0 self.t:after(0.25, function() self.t:after(0.1, function() self.t:tween(0.1, self, {text_sx = 1, text_sy = 1}, math.cubic_in_out) end) self.t:tween(0.75, self, {rs = gw}, math.linear, function() if self.transition_action then self.transition_action(unpack(self.transition_action_args)) end self.t:after(0.5, function() self.x, self.y = gw/2, gh/2 self.t:after(0.7, function() self.t:tween(0.05, self, {text_sx = 0, text_sy = 0}, math.cubic_in_out) end) self.t:tween(0.75, self, {rs = 0}, math.linear, function() self.text = nil; self.dead = true end) end) end) end) end function TransitionEffect:update(dt) self:update_game_object(dt) if self.text then self.text:update(dt) end end function TransitionEffect:draw() graphics.push(self.x, self.y, 0, self.sx, self.sy) graphics.circle(self.x, self.y, self.rs, self.color) graphics.pop() if self.text then self.text:draw(gw/2, gh/2, 0, self.text_sx, self.text_sy) end end global_text_tags = { red = TextTag{draw = function(c, i, text) graphics.set_color(red[0]) end}, orange = TextTag{draw = function(c, i, text) graphics.set_color(orange[0]) end}, yellow = TextTag{draw = function(c, i, text) graphics.set_color(yellow[0]) end}, green = TextTag{draw = function(c, i, text) graphics.set_color(green[0]) end}, purple = TextTag{draw = function(c, i, text) graphics.set_color(purple[0]) end}, blue = TextTag{draw = function(c, i, text) graphics.set_color(blue[0]) end}, bg = TextTag{draw = function(c, i, text) graphics.set_color(bg[0]) end}, fg = TextTag{draw = function(c, i, text) graphics.set_color(fg[0]) end}, wavy = TextTag{update = function(c, dt, i, text) c.oy = 2*math.sin(4*time + i) end}, } InfoText = Object:extend() InfoText:implement(GameObject) function InfoText:init(args) self:init_game_object(args) self.sx, self.sy = 0, 0 self.ox, self.oy = 0, 0 self.ow, self.oh = 0, 0 self.text = Text({}, global_text_tags) end function InfoText:update(dt) self:update_game_object(dt) end function InfoText:draw() graphics.push(self.x + self.ox, self.y + self.oy, 0, self.sx*self.spring.x, self.sy*self.spring.x) graphics.rectangle(self.x + self.ox, self.y + self.oy, self.text.w + self.ow, self.text.h + self.oh, self.text.h/4, self.text.h/4, bg[-2]) self.text:draw(self.x + self.ox, self.y + self.oy + self.text.h/2) graphics.pop() end function InfoText:activate(text, ox, oy, sx, sy, ow, oh) ox, oy = 0, 0 sx, sy = 1, 1 ow, oh = 16, 4 self.text:set_text(text) self.t:cancel'deactivate' self.t:tween(0.1, self, {sx = sx, sy = sy}, math.cubic_in_out, function() self.sx, self.sy = sx, sy end, 'activate_1') self.t:after(0.075, function() self.spring:pull(0.1, 200, 10) end, 'activate_2') end function InfoText:deactivate() self.t:cancel'activate_1' self.t:cancel'activate_2' self.t:tween(0.05, self, {sy = 0}, math.linear, function() self.sy = 0 end, 'deactivate') end ColorRamp = Object:extend() function ColorRamp:init(color, step) self.color = color self.step = step for i = -10, 10 do if i < 0 then self[i] = self.color:clone():lighten(i*self.step) elseif i > 0 then self[i] = self.color:clone():lighten(i*self.step) else self[i] = self.color:clone() end end end RefreshEffect = Object:extend() RefreshEffect:implement(GameObject) RefreshEffect:implement(Parent) function RefreshEffect:init(args) self:init_game_object(args) self.oy = self.h/3 self.t:tween(0.15, self, {h = 0}, math.linear, function() self.dead = true end) end function RefreshEffect:update(dt) self:update_game_object(dt) self:follow_parent_exclusively() end function RefreshEffect:draw() graphics.push(self.x, self.y, self.r) graphics.rectangle2(self.x - self.w/2, self.y - self.oy, self.w, self.h, nil, nil, fg[0]) graphics.pop() end function flash(duration, color) flashing = true flash_color = color or fg[0] t:after(duration, function() flashing = false end, 'flash') end function slow(amount, duration, tween_method) amount = amount or 0.5 duration = duration or 0.5 tween_method = tween_method or math.cubic_in_out slow_amount = amount t:tween(duration, _G, {slow_amount = 1}, tween_method, function() slow_amount = 1 end, 'slow') end HitCircle = Object:extend() HitCircle:implement(GameObject) function HitCircle:init(args) self:init_game_object(args) self.rs = self.rs or 8 self.duration = self.duration or 0.05 self.color = self.color or white self.t:after(self.duration, function() self.dead = true end, 'die') return self end function HitCircle:update(dt) self:update_game_object(dt) end function HitCircle:draw() graphics.circle(self.x, self.y, self.rs, self.color) end function HitCircle:scale_down(duration) duration = duration or 0.2 self.t:cancel'die' self.t:tween(self.duration, self, {rs = 0}, math.cubic_in_out, function() self.dead = true end) return self end function HitCircle:change_color(delay_multiplier, target_color) delay_multiplier = delay_multiplier or 0.5 self.t:after(delay_multiplier*self.duration, function() self.color = target_color end) return self end HitParticle = Object:extend() HitParticle:implement(GameObject) function HitParticle:init(args) self:init_game_object(args) self.v = self.v or random:float(50, 150) self.r = args.r or random:float(0, 2*math.pi) self.duration = self.duration or random:float(0.2, 0.6) self.w = self.w or random:float(3.5, 7) self.h = self.h or self.w/2 self.color = self.color or white self.t:tween(self.duration, self, {w = 2, h = 2, v = 0}, math.cubic_in_out, function() self.dead = true end) end function HitParticle:update(dt) self:update_game_object(dt) self.x = self.x + self.v*math.cos(self.r)*dt self.y = self.y + self.v*math.sin(self.r)*dt end function HitParticle:draw() graphics.push(self.x, self.y, self.r) graphics.rectangle(self.x, self.y, self.w, self.h, 2, 2, self.color) graphics.pop() end function HitParticle:change_color(delay_multiplier, target_color) delay_multiplier = delay_multiplier or 0.5 self.t:after(delay_multiplier*self.duration, function() self.color = target_color end) return self end AnimationEffect = Object:extend() AnimationEffect:implement(GameObject) function AnimationEffect:init(args) self:init_game_object(args) self.animation = Animation(self.delay, self.frames, 'once', {[0] = function() self.dead = true end}) self.color = self.color or white end function AnimationEffect:update(dt) self:update_game_object(dt) self.animation:update(dt) if self.linear_movement then self.x = self.x + self.v*math.cos(self.r)*dt self.y = self.y + self.v*math.sin(self.r)*dt end end function AnimationEffect:draw() self.animation:draw(self.x + (self.ox or 0), self.y + (self.ox or 0), self.r + (self.oa or 0), (self.flip_sx or 1)*self.sx, (self.flip_sy or 1)*self.sy, nil, nil, self.color) end function AnimationEffect:set_linear_movement(v, r) self.v = v self.r = r self.linear_movement = true local duration = self.animation.size*self.delay self.t:after(2*duration/3, function() self.t:tween(duration/3, self, {v = 0}, math.cubic_in_out) end) end Wall = Object:extend() Wall:implement(GameObject) Wall:implement(Physics) function Wall:init(args) self:init_game_object(args) self:set_as_chain(true, self.vertices, 'static', 'solid') self.color = self.color or fg[0] end function Wall:update(dt) self:update_game_object(dt) end function Wall:draw() self.shape:draw(self.color) end