diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md deleted file mode 100644 index 4b47742..0000000 --- a/STYLEGUIDE.md +++ /dev/null @@ -1,10 +0,0 @@ - -### -this is a project written in lua, using the amulet game engine. -if you don't know lua, or know lua but don't know amulet, read or skim the documentation: -https://amulet.xyz/doc - -amulet does extend lua syntax in some small ways. - -@TODO - diff --git a/conf.lua b/conf.lua index 71317e2..19793f7 100644 --- a/conf.lua +++ b/conf.lua @@ -2,9 +2,9 @@ title = "hexyz" author = "nick hayashi" shortname = "hexyz" -version = "0.3.0" -support_email = "" -copyright_message = "Copyright © 2021 Nick Hayashi" +version = "0.0.1" +support_email = "hayashi.nicholas@gmail.com" +copyright_message = "Copyright © nick hayashi" dev_region = "en" supported_languages = "en" diff --git a/curves.lua b/curves.lua deleted file mode 100644 index 79d8077..0000000 --- a/curves.lua +++ /dev/null @@ -1,4 +0,0 @@ - --- nice curve, f(0) = ~1.1, f(100) = ~100, exponential slope roughly inbetween --- math.exp((x + 1)/22) / 100 --- diff --git a/data/towers.lua b/data/towers.lua index 9d44e44..42124de 100644 --- a/data/towers.lua +++ b/data/towers.lua @@ -1,4 +1,3 @@ - --[[ the following is a list of tower specifications, which are declarations of a variety of properties describing what a tower is, and how it functions this a lua file. a quick run-down of what writing code in lua looks like: https://www.amulet.xyz/doc/#lua-primer @@ -68,6 +67,7 @@ return { { + id = "WALL", name = "Wall", placement_rules_text = "Place on Ground", short_description = "Restricts movement, similar to a mountain.", @@ -79,6 +79,7 @@ return { update = false, }, { + id = "GATTLER", name = "Gattler", placement_rules_text = "Place on Ground", short_description = "Short-range, fast-fire rate single-target tower.", @@ -131,6 +132,7 @@ return { end }, { + id = "HOWITZER", name = "Howitzer", placement_rules_text = "Place on Ground, with a 1 space gap between other towers and mountains - walls/moats don't count.", short_description = "Medium-range, medium fire-rate area of effect artillery tower.", @@ -226,6 +228,7 @@ return { end }, { + id = "REDEYE", name = "Redeye", placement_rules_text = "Place on Mountains.", short_description = "Long-range, penetrating high-velocity laser tower.", @@ -273,6 +276,7 @@ return { end }, { + id = "MOAT", name = "Moat", placement_rules_text = "Place on Ground", short_description = "Restricts movement, similar to water.", @@ -285,6 +289,7 @@ return { update = false }, { + id = "RADAR", name = "Radar", placement_rules_text = "n/a", short_description = "Doesn't do anything right now :(", @@ -296,6 +301,7 @@ return { update = false }, { + id = "LIGHTHOUSE", name = "Lighthouse", placement_rules_text = "Place on Ground, adjacent to Water or Moats", short_description = "Attracts nearby mobs; temporarily redirects their path", diff --git a/color.lua b/lib/color.lua similarity index 79% rename from color.lua rename to lib/color.lua index 6f66c88..6ca58c9 100644 --- a/color.lua +++ b/lib/color.lua @@ -1,14 +1,16 @@ COLORS = { - TRANSPARENT = vec4(0.6), - TRANSPARENT1 = vec4(0.4), + TRANSPARENT1 = vec4(0), + TRANSPARENT2 = vec4(0.4), + TRANSPARENT3 = vec4(0.6), -- tones WHITE = vec4(1, 1, 0.98, 1), - PALE_SILVER = vec4(193/255, 178/255, 171/255, 1), + TRUE_WHITE = vec4(1, 1, 1, 1), BLACK = vec4(0, 0, 0.05, 1), - VERY_DARK_GRAY = vec4(35/255, 35/255, 25/255, 1), TRUE_BLACK = vec4(0, 0, 0, 1), + PALE_SILVER = vec4(193/255, 178/255, 171/255, 1), + VERY_DARK_GRAY = vec4(35/255, 35/255, 25/255, 1), EIGENGRAU = vec4(0, 0, 0.02, 1), -- non-standard ??? hues @@ -22,6 +24,6 @@ COLORS = { SUNRAY = vec4(228/255, 179/255, 99/255, 1), GREEN_YELLOW = vec4(204/255, 255/255, 102/255, 1), BLUE = vec4(50/255, 50/255, 180/255, 1), - MAGENTA = vec4(183/255, 0/255, 213/255, 1), + MAGENTA = vec4(183/255, 0/255, 213/255, 1) } diff --git a/lib/extra.lua b/lib/extra.lua new file mode 100644 index 0000000..392abf1 --- /dev/null +++ b/lib/extra.lua @@ -0,0 +1,100 @@ + +-- utility functions that don't below elsewhere go here, +-- especially if they would be at home on the global 'math' or 'table' variables, or are otherwise extensions of standard lua features +-- try to avoid *too* much amulet specific stuff, but vector types are probably ok. + +function fprofile(f, ...) + local t1 = am.current_time() + local result = { f(...) } + local time = am.current_time() - t1 + --log("%f", time) + return time, unpack(result) +end + +function math.wrapf(float, range) + return float - range * math.floor(float / range) +end + +function math.lerp(v1, v2, t) + return v1 * t + v2 * (1 - t) +end + +-- don't use this with sparse arrays +function table.rchoice(t) + return t[math.floor(math.random() * #t) + 1] +end + +function table.count(t) + local count = 0 + for i,v in pairs(t) do + if v ~= nil then + count = count + 1 + end + end + return count +end + +function table.highest_index(t) + local highest = nil + for i,v in pairs(t) do + if i and not highest then + highest = i + end + + if i > highest then + highest = i + end + end + return highest +end + +function table.find(t, predicate) + for i,v in pairs(t) do + if predicate(v) then + return i,v + end + end + return nil +end + +-- don't use with sparse arrays or hash tables. +-- only arrays. +-- mutates the array in place. +function table.reverse(t) + local n = #t + for i,v in pairs(t) do + t[i], t[n] = t[n], t[i] + n = n - 1 + end +end + +function table.quicksort(t, low_index, high_index, comparator) + local function partition(t, low_index, high_index) + local i = low_index - 1 + local pivot = t[high_index] + + for j = low_index, high_index - 1 do + if comparator(t[j], t[pivot]) <= 0 then + i = i + 1 + t[i], t[j] = t[j], t[i] + end + end + + t[i + 1], t[high_index] = t[high_index], t[i + 1] + return i + 1 + end + + if #t == 1 then + return t + end + + if comparator(t[low_index], t[high_index]) < 0 then + local partition_index = partition(t, low_index, high_index) + + quicksort(t, low_index, partition_index - 1, comparator) + quicksort(t, partition_index + 1, high_index, comparator) + end + + return t +end + diff --git a/lib/geometry.lua b/lib/geometry.lua new file mode 100644 index 0000000..ede2bea --- /dev/null +++ b/lib/geometry.lua @@ -0,0 +1,23 @@ + +function circles_intersect(center1, center2, radius1, radius2) + local c1, c2, r1, r2 = center1, center2, radius1, radius2 + local d = math.distance(center1, center2) + local radii_sum = r1 + r2 + -- touching + if d == radii_sum then return 1 + + -- not touching or intersecting + elseif d > radii_sum then return false + + -- intersecting + else return 2 + end +end + +function point_in_rect(point, rect) + return point.x > rect.x1 + and point.x < rect.x2 + and point.y > rect.y1 + and point.y < rect.y2 +end + diff --git a/lib/gui.lua b/lib/gui.lua new file mode 100644 index 0000000..1152602 --- /dev/null +++ b/lib/gui.lua @@ -0,0 +1,223 @@ + +-- text popup in the middle of the screen that dissapates +function gui_alert(message, color, decay_time) + win.scene:append( + am.scale(3) ^ am.text(message, color or COLORS.WHITE) + :action(coroutine.create(function(self) + am.wait(am.tween(self, decay_time or 1, { color = vec4(0) }, am.ease_in_out)) + win.scene:remove(self) + end)) + ) +end + +local function gui_make_backing_rect( + position, + content_width, + content_height, + padding +) + local half_width = content_width/2 + local half_height = content_height/2 + + local x1 = position[1] - half_width - padding + local y1 = position[2] - half_height - padding + local x2 = position[1] + half_width + padding + local y2 = position[2] + half_height + padding + + return x1, y1, x2, y2 +end + +-- args { +-- position vec2 +-- onclick function +-- padding number +-- +-- min_width number +-- min_height number +-- +-- label string +-- font { +-- color vec4 +-- halign "center" | "left" | "right" +-- valign "center" | "top" | "bottom" +-- } +-- } +function gui_make_button(args) + local args = args or {} + + local position = args.position or vec2(0) + local onclick = args.onclick + local padding = args.padding or 6 + + local min_width = args.min_width or 0 + local min_height = args.min_height or 0 + + local label = args.label or "" + local font = args.font or { + color = vec4(1), + halign = "center", + valign = "center" + } + + local scene = am.group() + + local text = am.text(args.label or "", font.color, font.halign, font.valign) + scene:append(am.translate(args.position) ^ text) + + local content_width = math.max(min_width, text.width) + local content_height = math.max(min_height, text.height) + + local x1, y1, x2, y2 = gui_make_backing_rect(position, content_width, content_height, padding) + + local back_rect = am.rect(x1 - padding/2, y1, x2, y2 + padding/2, vec4(0, 0, 0, 1)) + local front_rect = am.rect(x1, y1, x2, y2, vec4(0.4)) + scene:prepend(front_rect) + scene:prepend(back_rect) + + scene:action(function(self) + if point_in_rect(win:mouse_position(), back_rect) then + if win:mouse_pressed"left" then + front_rect.color = vec4(0.4) + + if onclick then + onclick() + end + else + front_rect.color = vec4(0, 0, 0, 1) + end + else + front_rect.color = vec4(0.4) + end + end) + + return scene +end + +function gui_textfield( + position, + dimensions, + max, + disallowed_chars +) + local width, height = dimensions.x, dimensions.y + local disallowed_chars = disallowed_chars or {} + local max = max or 10 + local padding = padding or 6 + local half_width = width/2 + local half_height = height/2 + + local x1 = position[1] - half_width - padding + local y1 = position[2] - half_height - padding + local x2 = position[1] + half_width + padding + local y2 = position[2] + half_height + padding + + local back_rect = am.rect(x1 - padding/2, y1, x2, y2 + padding/2, vec4(0, 0, 0, 1)) + local front_rect = am.rect(x1, y1, x2, y2, vec4(0.4)) + + local group = am.group{ + back_rect, + front_rect, + am.translate(-width/2 + padding, 0) ^ am.scale(2) ^ am.text("", vec4(0, 0, 0, 1), "left"), + am.translate(-width/2 + padding, -8) ^ am.line(vec2(0, 0), vec2(16, 0), 2, vec4(0, 0, 0, 1)) + } + + group:action(function(self) + local keys = win:keys_pressed() + if #keys == 0 then return end + + local chars = {} + local shift = win:key_down("lshift") or win:key_down("rshift") + for i,k in pairs(keys) do + if k:len() == 1 then -- @HACK alphabetical or digit characters + if string.match(k, "%a") then + if shift then + table.insert(chars, k:upper()) + else + table.insert(chars, k) + end + elseif string.match(k, "%d") then + if shift then + if k == "1" then table.insert(chars, "!") + elseif k == "2" then table.insert(chars, "@") + elseif k == "3" then table.insert(chars, "#") + elseif k == "4" then table.insert(chars, "$") + elseif k == "5" then table.insert(chars, "%") + elseif k == "6" then table.insert(chars, "^") + elseif k == "7" then table.insert(chars, "&") + elseif k == "8" then table.insert(chars, "*") + elseif k == "9" then table.insert(chars, "(") + elseif k == "0" then table.insert(chars, ")") + end + else + table.insert(chars, k) + end + end + -- begin non-alphabetical/digit + elseif k == "minus" then + if shift then table.insert(chars, "_") + else table.insert(chars, "-") end + elseif k == "equals" then + if shift then table.insert(chars, "=") + else table.insert(chars, "+") end + elseif k == "leftbracket" then + if shift then table.insert(chars, "{") + else table.insert(chars, "[") end + elseif k == "rightbracket" then + if shift then table.insert(chars, "}") + else table.insert(chars, "]") end + elseif k == "backslash" then + if shift then table.insert(chars, "|") + else table.insert(chars, "\\") end + elseif k == "semicolon" then + if shift then table.insert(chars, ":") + else table.insert(chars, ";") end + elseif k == "quote" then + if shift then table.insert(chars, "\"") + else table.insert(chars, "'") end + elseif k == "backquote" then + if shift then table.insert(chars, "~") + else table.insert(chars, "`") end + elseif k == "comma" then + if shift then table.insert(chars, "<") + else table.insert(chars, ",") end + elseif k == "period" then + if shift then table.insert(chars, ">") + else table.insert(chars, ".") end + elseif k == "slash" then + if shift then table.insert(chars, "?") + else table.insert(chars, "/") end + + -- control characters + elseif k == "backspace" then + -- @NOTE this doesn't preserve the order of chars in the array so if + -- someone presses a the key "a" then the backspace key in the same frame, in that order + -- the backspace occurs first + self"text".text = self"text".text:sub(1, self"text".text:len() - 1) + + elseif k == "tab" then + -- @TODO + + elseif k == "space" then + table.insert(chars, " ") + + elseif k == "capslock" then + -- @TODO + end + end + + for _,c in pairs(chars) do + if not disallowed_chars[c] then + if self"text".text:len() <= max then + self"text".text = self"text".text .. c + end + end + end + end) + + return group +end + +function gui_open_modal() + +end + diff --git a/lib/memory.lua b/lib/memory.lua new file mode 100644 index 0000000..2af2121 --- /dev/null +++ b/lib/memory.lua @@ -0,0 +1,33 @@ + +-- the garbage collector decides when to run its cycles on its own, and this can cause frame spikes in your game. +-- lua provides some amount of control over its garbage collector. +-- +-- by storing the average time it takes for a full gc cycle to run, we can check at the end of a frame if we have enough time +-- to run it for 'free' +-- +-- if you wish, you can call 'collectgarbage("stop")', and then: +-- at the start of each game frame, call and cache the results of 'am.current_time()' - am.frame_time doesn't seem to work as well +-- at the end of each game frame, call 'check_if_can_collect_garbage_for_free()' with the cached frame time and a desired minimum fps +-- +local garbage_collector_cycle_timing_history = {} +local garbage_collector_average_cycle_time = 0 +function run_garbage_collector_cycle() + local time, result = fprofile(collectgarbage, "collect") + + table.insert(garbage_collector_cycle_timing_history, time) + -- re-calc average gc timing + local total = 0 + for _,v in pairs(garbage_collector_cycle_timing_history) do + total = total + v + end + garbage_collector_average_cycle_time = total / #garbage_collector_cycle_timing_history +end + +function check_if_can_collect_garbage_for_free(frame_start_time, min_fps) + -- often this will be polled at the end of a frame to see if we're running fast or slow, + -- and if we have some time to kill before the start of the next frame, we could maybe run gc. + if (am.current_time() - frame_start_time) < (1 / (min_fps or 60) + garbage_collector_average_cycle_time) then + run_garbage_collector_cycle() + end +end + diff --git a/lib/random.lua b/lib/random.lua new file mode 100644 index 0000000..738e0b2 --- /dev/null +++ b/lib/random.lua @@ -0,0 +1,76 @@ + +-- seed the random number generator with the current time +math.randomseed(os.clock()) + +-- https://stackoverflow.com/a/20177466/12464892 +--local A1, A2 = 727595, 798405 -- 5^17=D20*A1+A2 +--local D20, D40 = 1048576, 1099511627776 -- 2^20, 2^40 +--local X1, X2 = 0, 1 +--local function rand() +-- local U = X2*A2 +-- local V = (X1*A2 + X2*A1) % D20 +-- V = (V*D20 + U) % D40 +-- X1 = math.floor(V/D20) +-- X2 = V - X1*D20 +-- return V/D40 +--end +-- +--local SEED_BOUNDS = 2^20 - 1 +--math.randomseed = function(seed) +-- local v = math.clamp(math.abs(seed), 0, SEED_BOUNDS) +-- X1 = v +-- X2 = v + 1 +--end + +-- to enable allowing the random number generator's state to be restored post-load (game-deserialize), +-- we count the number of times we call math.random(), and on deserialize, seed the random +-- number generator, and then discard |count| calls. +local R = math.random +RANDOM_CALLS_COUNT = 0 +local function random(n, m) + RANDOM_CALLS_COUNT = RANDOM_CALLS_COUNT + 1 + + if n then + if m then + return R(n, m) + else + return R(n) + end + else + return R() + end +end + +-- whenever we refer to math.random, actually use the function 'random' above +math.random = random + +function g_octave_noise(x, y, num_octaves, seed) + local seed = seed or os.clock() + local noise = 0 + + for oct = 1, num_octaves do + local f = 1/4^oct + local l = 2^oct + local pos = vec2(x + seed, y + seed) + noise = noise + f * math.simplex(pos * l) + end + + return noise +end + +---- @TODO test, fix +--function poisson_knuth(lambda) +-- local e = 2.71828 +-- +-- local L = e^-lambda +-- local k = 0 +-- local p = 1 +-- +-- while p > L do +-- k = k + 1 +-- p = p * math.random() +-- end +-- +-- return k - 1 +--end + diff --git a/sound.lua b/lib/sound.lua similarity index 63% rename from sound.lua rename to lib/sound.lua index dea8a13..fcc978e 100644 --- a/sound.lua +++ b/lib/sound.lua @@ -21,31 +21,32 @@ SOUNDS = { RANDOM5 = 36680709, -- audio buffers - MAIN_THEME = am.track(am.load_audio("res/maintheme.ogg"), true, 1, settings.music_volume) + MAIN_THEME = am.track(am.load_audio("res/ogg/main_theme.ogg"), true, 1, SETTINGS.music_volume) } --- doesn't get unset when a track is 'done' playing automatically -CURRENT_TRACK = nil - -function update_sfx_volume() end +CURRENT_TRACKS = {} function update_music_volume(volume) - if CURRENT_TRACK then - CURRENT_TRACK.volume = math.clamp(volume, 0, 1) + for _,track in pairs(CURRENT_TRACKS) do + track.volume = math.clamp(volume, 0, 1) end end -- play sound effect with variable pitch function vplay_sfx(sound, pitch_range) local pitch = (math.random() + 0.5)/(pitch_range and 1/pitch_range or 2) - win.scene:action(am.play(sound, false, pitch, settings.sfx_volume)) + win.scene:action(am.play(sound, false, pitch, SETTINGS.sfx_volume)) end function play_sfx(sound) - win.scene:action(am.play(sound, false, 1, settings.sfx_volume)) + win.scene:action(am.play(sound, false, 1, SETTINGS.sfx_volume)) +end + +function stop_track() + end -function play_track(track, loop) - CURRENT_TRACK = track - win.scene:action(am.play(track, loop or true)) +function play_track(track, do_loop) + table.insert(CURRENT_TRACKS, track) + win.scene:action(am.play(track, do_loop or true)) end diff --git a/lib/texture.lua b/lib/texture.lua new file mode 100644 index 0000000..36d528d --- /dev/null +++ b/lib/texture.lua @@ -0,0 +1,105 @@ + +local IMG_FILE_PREFIX = "res/img/" + +local function load_texture(filepath) + local path = IMG_FILE_PREFIX .. filepath + local status, texture = pcall(am.texture2d, path) + + if status then + return texture + else + log("failed to load texture at path: " .. path) + return am.texture2d(IMG_FILE_PREFIX .. "bagel.jpg") + end +end + +TEXTURES = { + -- note that in amulet, if you prefix paths with './', they fail to be found in the exported data.pak + WHITE = load_texture("white-texture.png"), + LOGO = load_texture("logo.png"), + GEM1 = load_texture("gem1.png"), + + SHADED_HEX = load_texture("shaded_hex.png"), + NEW_GAME_HEX = load_texture("newgamehex.png"), + SAVE_GAME_HEX = load_texture("savegamehex.png"), + LOAD_GAME_HEX = load_texture("loadgamehex.png"), + SETTINGS_HEX = load_texture("settingshex.png"), + MAP_EDITOR_HEX = load_texture("mapeditorhex.png"), + ABOUT_HEX = load_texture("abouthex.png"), + QUIT_HEX = load_texture("quithex.png"), + UNPAUSE_HEX = load_texture("unpausehex.png"), + MAIN_MENU_HEX = load_texture("mainmenuhex.png"), + + CURTAIN = load_texture("curtain1.png"), + + SOUND_ON1 = load_texture("sound-on.png"), + SOUND_OFF = load_texture("sound-off.png"), + + -- gui stuff + BUTTON1 = load_texture("button1.png"), + WIDER_BUTTON1 = load_texture("wider_button1.png"), + GEAR = load_texture("gear.png"), + + SELECT_BOX = load_texture("select_box.png"), + + -- tower stuff + TOWER_WALL = load_texture("tower_wall.png"), + TOWER_WALL_ICON = load_texture("tower_wall_icon.png"), + TOWER_GATTLER = load_texture("tower_gattler.png"), + TOWER_GATTLER_ICON = load_texture("tower_gattler_icon.png"), + TOWER_HOWITZER = load_texture("tower_howitzer.png"), + TOWER_HOWITZER_ICON = load_texture("tower_howitzer_icon.png"), + TOWER_REDEYE = load_texture("tower_redeye.png"), + TOWER_REDEYE_ICON = load_texture("tower_redeye_icon.png"), + TOWER_MOAT = load_texture("tower_moat.png"), + TOWER_MOAT_ICON = load_texture("tower_moat_icon.png"), + TOWER_RADAR = load_texture("tower_radar.png"), + TOWER_RADAR_ICON = load_texture("tower_radar_icon.png"), + TOWER_LIGHTHOUSE = load_texture("tower_lighthouse.png"), + TOWER_LIGHTHOUSE_ICON = load_texture("tower_lighthouse_icon.png"), + + -- mob stuff + MOB_BEEPER = load_texture("mob_beeper.png"), + MOB_SPOODER = load_texture("mob_spooder.png"), + MOB_VELKOOZ = load_texture("mob_velkooz.png"), + MOB_VELKOOZ1 = load_texture("mob_velkooz1.png"), + MOB_VELKOOZ2 = load_texture("mob_velkooz2.png"), + MOB_VELKOOZ3 = load_texture("mob_velkooz3.png"), +} + +function pack_texture_into_sprite( + texture, + width, + height, + color, + s1, + s2, + t1, + t2 +) + local width, height = width or texture.width, height or texture.height + + local sprite = am.sprite{ + texture = texture, + s1 = s1 or 0, s2 = s2 or 1, t1 = t1 or 0, t2 = t2 or 1, + x1 = 0, x2 = width, width = width, + y1 = 0, y2 = height, height = height + } + + if color then sprite.color = color end + + return sprite +end + +function update_sprite(sprite, texture, width, height, s1, t1, s2, t2) + local s1, t1, s2, t2 = s1 or 0, t1 or 0, s2 or 1, t2 or 1 + local width, height = width or texture.width, height or texture.height + + sprite.source = { + texture = texture, + s1 = s1, t1 = t1, s2 = s2, t2 = t2, + x1 = 0, x2 = width, width = width, + y1 = 0, y2 = height, height = height + } +end + diff --git a/main.lua b/main.lua index 74e8e8c..c907c12 100644 --- a/main.lua +++ b/main.lua @@ -1,43 +1,5 @@ --- @TODO @TODO @TODO @TODO --- main --- -- scale menu hexes to window size, right now they look bad on smaller resolutions - --- settings menu --- -- make the volume icon clickable --- -- music volume slider or number input box --- -- sfx volume slider or number input box --- -- allow different resolution options, as long as you are 4:3 - --- serialization --- -- allow saving by name --- -- allow loading by name --- -- investigate saving as lua instead, and having as a consequence a less janky map serialization - we don't care about exploitability - --- sound --- -- fix the non-seamless loop in the soundtrack --- -- more trax - --- game --- -- allow selecting of tiles, if tower is selected then allow sell/upgrade --- -- new game menu allowing set seed --- -- make art, birds-eye-ify the redeye tower and lighthouse maybe? - --- map editor? --- -- paint terrain elevation levels --- -- place tiles of set elevation --- -- place towers --- -- move home? - --- lua's random number generator doesn't really produce random looking values if you don't seed it and discard a few calls first -math.randomseed(os.time()) -math.random() -math.random() -math.random() -math.random() - --- aspect ratios seem like a huge mess --- for now, i think we should enforce 4:3 +-- all 4:3 aspect ratio local RESOLUTION_OPTIONS = { { width = 1440, height = 1080 }, { width = 1400, height = 1050 }, -- seems like a good default one @@ -50,7 +12,7 @@ local RESOLUTION_OPTIONS = { } local DEFAULT_RESOLUTION = RESOLUTION_OPTIONS[2] -settings = am.load_state("settings", "json") or { +SETTINGS = am.load_state("settings", "json") or { fullscreen = false, window_width = DEFAULT_RESOLUTION.width, window_height = DEFAULT_RESOLUTION.height, @@ -60,14 +22,15 @@ settings = am.load_state("settings", "json") or { } win = am.window{ - width = settings.window_width, - height = settings.window_height, - title = "hexyz", - mode = settings.fullscreen and "fullscreen" or "windowed", + width = SETTINGS.window_width, + height = SETTINGS.window_height, + title = "", + mode = SETTINGS.fullscreen and "fullscreen" or "windowed", resizable = false, highdpi = true, letterbox = true, show_cursor = true, + clear_color = vec4(0), } -- top right display types @@ -89,63 +52,27 @@ function make_top_right_display_node() ^ am.text("", "right", "top"):tag"top_right_display" end --- asset interfaces and/or trivial code require "conf" -require "color" -require "sound" -require "texture" --- -require "src/entity" -require "src/extra" -require "src/memory" -require "src/geometry" +-- library/standard code (ours) +require "lib/random" +require "lib/extra" +require "lib/memory" +require "lib/geometry" +require "lib/gui" +require "lib/color" +require "lib/sound" +require "lib/texture" + +-- other internal dependencies require "src/hexyz" -require "src/game" -require "src/gui" require "src/grid" -require "src/mob" -require "src/projectile" +require "src/game" require "src/tower" +require "src/mob" require "src/map-editor" - ------------------------------------------------------------------- -local sound_toggle_node_tag = "sound_on_off_icon" -local function make_sound_toggle_node(on) - local sprite - if on then - sprite = pack_texture_into_sprite(TEXTURES.SOUND_ON1, 40, 30) - else - sprite = pack_texture_into_sprite(TEXTURES.SOUND_OFF, 40, 30) - end - - return (am.translate(win.right - 30, win.top - 60) ^ sprite) - :tag(sound_toggle_node_tag) - :action(function() - -- @TODO click me! - end) -end - -local cached_music_volume = 0.2 -local cached_sfx_volume = 0.1 -local function toggle_mute() - settings.sound_on = not settings.sound_on - - if settings.sound_on then - settings.music_volume = cached_music_volume - settings.sfx_volume = cached_sfx_volume - else - cached_music_volume = settings.music_volume - cached_sfx_volume = settings.sfx_volume - - settings.music_volume = 0 - settings.sfx_volume = 0 - end - - update_music_volume(settings.music_volume) - - win.scene:replace(sound_toggle_node_tag, make_sound_toggle_node(settings.sound_on)) -end +require "src/entity" +require "src/projectile" function main_action(self) if win:key_pressed("escape") then @@ -166,69 +93,6 @@ function main_action(self) end end -function make_scene_menu(scene_options, tag) - - -- calculate the dimensions of the whole grid - local spacing = 150 - local grid_width = 6 - local grid_height = 2 - local hhs = hex_horizontal_spacing(spacing) - local hvs = hex_vertical_spacing(spacing) - local grid_pixel_width = grid_width * hhs - local grid_pixel_height = grid_height * hvs - local pixel_offset = vec2(-grid_pixel_width/2, win.bottom + hex_height(spacing)/2 + 20) - - -- generate a map of hexagons (the menu is made up of two rows of hexes) and populate their locations with buttons from the provided options - local map = hex_rectangular_map(grid_width, grid_height, HEX_ORIENTATION.POINTY) - local group = am.group():tag(tag or "menu") - local option_index = 1 - for i,_ in pairs(map) do - for j,_ in pairs(map[i]) do - local hex = vec2(i, j) - local position = hex_to_pixel(hex, vec2(spacing), HEX_ORIENTATION.POINTY) - local option = scene_options[option_index] - local texture = option and option.texture or TEXTURES.SHADED_HEX - local color = option and COLORS.TRANSPARENT or vec4(0.3) - local node = am.translate(position) - ^ pack_texture_into_sprite(texture, texture.width, texture.height, color) - - hex_map_set(map, i, j, { - node = node, - option = option - }) - local tile = hex_map_get(map, i, j) - - local selected = false - node:action(function(self) - local mouse = win:mouse_position() - local hex_ = pixel_to_hex(mouse - pixel_offset, vec2(spacing), HEX_ORIENTATION.POINTY) - - if tile.option then - if hex == hex_ then - if not selected then - play_sfx(SOUNDS.SELECT1) - end - selected = true - tile.node"sprite".color = vec4(1) - - if win:mouse_pressed("left") then - tile.option.action() - end - else - selected = false - tile.node"sprite".color = COLORS.TRANSPARENT - end - end - end) - - group:append(node) - option_index = option_index + 1 - end - end - - return am.translate(pixel_offset) ^ group -end - function main_scene(do_backdrop, do_logo) local group = am.group():tag"main_scene" @@ -237,7 +101,7 @@ function main_scene(do_backdrop, do_logo) local hex_backdrop = (am.rotate(0) ^ am.group()):tag"hex_backdrop" for i,_ in pairs(map) do for j,n in pairs(map[i]) do - local color = map_elevation_color(n) + local color = map_elevation_to_color(n) color = color{a=color.a - 0.1} local node = am.translate(hex_to_pixel(vec2(i, j), vec2(HEX_SIZE))) @@ -261,10 +125,6 @@ function main_scene(do_backdrop, do_logo) ^ am.text(string.format("v%s, by %s", version, author), COLORS.WHITE, "right", "bottom") ) - group:append( - make_sound_toggle_node(settings.sound_on) - ) - if do_logo then local position = vec2(0, win.top - 20 - TEXTURES.LOGO.height/2) local logo = @@ -293,10 +153,7 @@ function main_scene(do_backdrop, do_logo) false, { texture = TEXTURES.NEW_GAME_HEX, - action = function() - win.scene:remove"main_scene" - game_init() - end + action = game_init }, false, false, @@ -307,7 +164,6 @@ function main_scene(do_backdrop, do_logo) local save = am.load_state("save", "json") if save then - win.scene:remove("main_scene") game_init(save) else gui_alert("no saved games") @@ -344,10 +200,85 @@ function main_scene(do_backdrop, do_logo) return group end -win.scene = am.group( - main_scene(true, true) -) -play_track(SOUNDS.MAIN_THEME) +function make_scene_menu(scene_options, tag) + -- calculate the dimensions of the whole grid + local spacing = 150 + local grid_width = 6 + local grid_height = 2 + local hhs = hex_horizontal_spacing(spacing) + local hvs = hex_vertical_spacing(spacing) + local grid_pixel_width = grid_width * hhs + local grid_pixel_height = grid_height * hvs + local pixel_offset = vec2(-grid_pixel_width/2, win.bottom + hex_height(spacing)/2 + 20) + + -- generate a map of hexagons (the menu is made up of two rows of hexes) and populate their locations with buttons from the provided options + local map = hex_rectangular_map(grid_width, grid_height, HEX_ORIENTATION.POINTY) + local group = am.group():tag(tag or "menu") + local option_index = 1 + for i,_ in pairs(map) do + for j,_ in pairs(map[i]) do + local hex = vec2(i, j) + local position = hex_to_pixel(hex, vec2(spacing), HEX_ORIENTATION.POINTY) + local option = scene_options[option_index] + local texture = option and option.texture or TEXTURES.SHADED_HEX + local color = option and COLORS.TRANSPARENT3 or vec4(0.3) + local node = am.translate(position) + ^ pack_texture_into_sprite(texture, texture.width, texture.height, color) + + hex_map_set(map, i, j, { + node = node, + option = option + }) + local tile = hex_map_get(map, i, j) + + local selected = false + node:action(function(self) + local mouse = win:mouse_position() + local hex_ = pixel_to_hex(mouse - pixel_offset, vec2(spacing), HEX_ORIENTATION.POINTY) + + if tile.option then + if hex == hex_ then + if not selected then + play_sfx(SOUNDS.SELECT1) + end + selected = true + tile.node"sprite".color = vec4(1) + + if win:mouse_pressed("left") then + tile.option.action() + end + else + selected = false + tile.node"sprite".color = COLORS.TRANSPARENT3 + end + end + end) + + group:append(node) + option_index = option_index + 1 + end + end + + return am.translate(pixel_offset) ^ group +end + +function switch_context(scene, action) + win.scene:remove("menu") + + if action then + win.scene:replace("context", scene:action(action):tag"context") + else + win.scene:replace("context", scene:tag"context") + end +end + +function init() + load_entity_specs() + + switch_context(main_scene(true, true)) +end +win.scene = am.group(am.group():tag"context") +init() noglobals() diff --git a/res/cells.jpg b/res/cells.jpg deleted file mode 100644 index dd08220..0000000 Binary files a/res/cells.jpg and /dev/null differ diff --git a/res/abouthex.png b/res/img/abouthex.png similarity index 100% rename from res/abouthex.png rename to res/img/abouthex.png diff --git a/res/bagel.jpg b/res/img/bagel.jpg similarity index 100% rename from res/bagel.jpg rename to res/img/bagel.jpg diff --git a/res/button1.png b/res/img/button1.png similarity index 100% rename from res/button1.png rename to res/img/button1.png diff --git a/res/curtain1.png b/res/img/curtain1.png similarity index 100% rename from res/curtain1.png rename to res/img/curtain1.png diff --git a/res/gear.png b/res/img/gear.png similarity index 100% rename from res/gear.png rename to res/img/gear.png diff --git a/res/gears.png b/res/img/gears.png similarity index 100% rename from res/gears.png rename to res/img/gears.png diff --git a/res/gem1.png b/res/img/gem1.png similarity index 100% rename from res/gem1.png rename to res/img/gem1.png diff --git a/res/loadgamehex.png b/res/img/loadgamehex.png similarity index 100% rename from res/loadgamehex.png rename to res/img/loadgamehex.png diff --git a/res/logo.png b/res/img/logo.png similarity index 100% rename from res/logo.png rename to res/img/logo.png diff --git a/res/mainmenuhex.png b/res/img/mainmenuhex.png similarity index 100% rename from res/mainmenuhex.png rename to res/img/mainmenuhex.png diff --git a/res/mapeditorhex.png b/res/img/mapeditorhex.png similarity index 100% rename from res/mapeditorhex.png rename to res/img/mapeditorhex.png diff --git a/res/mob_beeper.png b/res/img/mob_beeper.png similarity index 100% rename from res/mob_beeper.png rename to res/img/mob_beeper.png diff --git a/res/mob_spooder.png b/res/img/mob_spooder.png similarity index 100% rename from res/mob_spooder.png rename to res/img/mob_spooder.png diff --git a/res/mob_velkooz.png b/res/img/mob_velkooz.png similarity index 100% rename from res/mob_velkooz.png rename to res/img/mob_velkooz.png diff --git a/res/mob_velkooz0.png b/res/img/mob_velkooz0.png similarity index 100% rename from res/mob_velkooz0.png rename to res/img/mob_velkooz0.png diff --git a/res/mob_velkooz1.png b/res/img/mob_velkooz1.png similarity index 100% rename from res/mob_velkooz1.png rename to res/img/mob_velkooz1.png diff --git a/res/mob_velkooz2.png b/res/img/mob_velkooz2.png similarity index 100% rename from res/mob_velkooz2.png rename to res/img/mob_velkooz2.png diff --git a/res/mob_velkooz3.png b/res/img/mob_velkooz3.png similarity index 100% rename from res/mob_velkooz3.png rename to res/img/mob_velkooz3.png diff --git a/res/newgamehex.png b/res/img/newgamehex.png similarity index 100% rename from res/newgamehex.png rename to res/img/newgamehex.png diff --git a/res/quithex.png b/res/img/quithex.png similarity index 100% rename from res/quithex.png rename to res/img/quithex.png diff --git a/res/savegamehex.png b/res/img/savegamehex.png similarity index 100% rename from res/savegamehex.png rename to res/img/savegamehex.png diff --git a/res/select_box.png b/res/img/select_box.png similarity index 100% rename from res/select_box.png rename to res/img/select_box.png diff --git a/res/settingshex.png b/res/img/settingshex.png similarity index 100% rename from res/settingshex.png rename to res/img/settingshex.png diff --git a/res/shaded_hex.png b/res/img/shaded_hex.png similarity index 100% rename from res/shaded_hex.png rename to res/img/shaded_hex.png diff --git a/res/slider.png b/res/img/slider.png similarity index 100% rename from res/slider.png rename to res/img/slider.png diff --git a/res/sound-off.png b/res/img/sound-off.png similarity index 100% rename from res/sound-off.png rename to res/img/sound-off.png diff --git a/res/sound-on.png b/res/img/sound-on.png similarity index 100% rename from res/sound-on.png rename to res/img/sound-on.png diff --git a/res/tower_gattler.png b/res/img/tower_gattler.png similarity index 100% rename from res/tower_gattler.png rename to res/img/tower_gattler.png diff --git a/res/tower_gattler_icon.png b/res/img/tower_gattler_icon.png similarity index 100% rename from res/tower_gattler_icon.png rename to res/img/tower_gattler_icon.png diff --git a/res/tower_howitzer.png b/res/img/tower_howitzer.png similarity index 100% rename from res/tower_howitzer.png rename to res/img/tower_howitzer.png diff --git a/res/tower_howitzer_icon.png b/res/img/tower_howitzer_icon.png similarity index 100% rename from res/tower_howitzer_icon.png rename to res/img/tower_howitzer_icon.png diff --git a/res/tower_lighthouse.png b/res/img/tower_lighthouse.png similarity index 100% rename from res/tower_lighthouse.png rename to res/img/tower_lighthouse.png diff --git a/res/tower_lighthouse_icon.png b/res/img/tower_lighthouse_icon.png similarity index 100% rename from res/tower_lighthouse_icon.png rename to res/img/tower_lighthouse_icon.png diff --git a/res/tower_moat.png b/res/img/tower_moat.png similarity index 100% rename from res/tower_moat.png rename to res/img/tower_moat.png diff --git a/res/tower_moat_icon.png b/res/img/tower_moat_icon.png similarity index 100% rename from res/tower_moat_icon.png rename to res/img/tower_moat_icon.png diff --git a/res/tower_radar.png b/res/img/tower_radar.png similarity index 100% rename from res/tower_radar.png rename to res/img/tower_radar.png diff --git a/res/tower_radar_icon.png b/res/img/tower_radar_icon.png similarity index 100% rename from res/tower_radar_icon.png rename to res/img/tower_radar_icon.png diff --git a/res/tower_redeye.png b/res/img/tower_redeye.png similarity index 100% rename from res/tower_redeye.png rename to res/img/tower_redeye.png diff --git a/res/tower_redeye_icon.png b/res/img/tower_redeye_icon.png similarity index 100% rename from res/tower_redeye_icon.png rename to res/img/tower_redeye_icon.png diff --git a/res/tower_wall.png b/res/img/tower_wall.png similarity index 100% rename from res/tower_wall.png rename to res/img/tower_wall.png diff --git a/res/tower_wall_icon.png b/res/img/tower_wall_icon.png similarity index 100% rename from res/tower_wall_icon.png rename to res/img/tower_wall_icon.png diff --git a/res/unpausehex.png b/res/img/unpausehex.png similarity index 100% rename from res/unpausehex.png rename to res/img/unpausehex.png diff --git a/res/img/white-texture.png b/res/img/white-texture.png new file mode 100644 index 0000000..7e085ef Binary files /dev/null and b/res/img/white-texture.png differ diff --git a/res/wider_button1.png b/res/img/wider_button1.png similarity index 100% rename from res/wider_button1.png rename to res/img/wider_button1.png diff --git a/res/maintheme.ogg b/res/ogg/main_theme.ogg similarity index 100% rename from res/maintheme.ogg rename to res/ogg/main_theme.ogg diff --git a/src/entity.lua b/src/entity.lua index 4de468f..44f4413 100644 --- a/src/entity.lua +++ b/src/entity.lua @@ -58,6 +58,10 @@ function do_entity_updates() do_projectile_updates() end +function load_entity_specs() + resolve_tower_specs("data/towers.lua") +end + function entity_basic_devectored_copy(entity) local copy = table.shallow_copy(entity) copy.position = { copy.position.x, copy.position.y } diff --git a/src/extra.lua b/src/extra.lua index c3ec57b..2822d97 100644 --- a/src/extra.lua +++ b/src/extra.lua @@ -1,3 +1,4 @@ + -- utility functions that don't below elsewhere go here, -- especially if they would be at home on the global 'math' or 'table' variables, or are otherwise extensions of standard lua features -- try to avoid *too* much amulet specific stuff, but vector types are probably ok. diff --git a/src/game.lua b/src/game.lua index 5c54601..5b5ebdc 100644 --- a/src/game.lua +++ b/src/game.lua @@ -34,7 +34,6 @@ local game_scene_menu_options = { { texture = TEXTURES.MAP_EDITOR_HEX, action = function() - win.scene:remove("game") map_editor_init(game_state.map.seed) end }, @@ -75,8 +74,8 @@ local function get_initial_game_state(seed) score = 0, -- current game score money = STARTING_MONEY, -- current money - towers = {}, -- list of tower entities mobs = {}, -- list of mob entities + towers = {}, -- list of tower entities projectiles = {}, -- list of projectile entities current_wave = 1, @@ -147,7 +146,7 @@ local function do_day_night_cycle() end local function game_pause() - win.scene("game").paused = true + win.scene("context").paused = true win.scene:append(make_scene_menu(game_scene_menu_options)) end @@ -156,7 +155,7 @@ local function game_deserialize(json_string) local new_game_state = am.parse_json(json_string) if new_game_state.version ~= version then - log("loading incompatible old save data. starting a fresh game instead.") + gui_alert("loading incompatible old save data.\nstarting a fresh game instead.", nil, 10) return get_initial_game_state() end @@ -247,10 +246,6 @@ local function deselect_tile() win.scene:remove("tile_select_box") end -local function game_pause_menu() - -end - local function game_action(scene) game_state.frame_start_time = am.current_time() if game_state.score < 0 then @@ -309,14 +304,14 @@ local function game_action(scene) if broken then local node = win.scene("cursor"):child(2) node.color = COLORS.CLARET - node:action(am.tween(0.1, { color = COLORS.TRANSPARENT })) + node:action(am.tween(0.1, { color = COLORS.TRANSPARENT3 })) play_sfx(SOUNDS.BIRD2) gui_alert("closes the circle") elseif cost > game_state.money then local node = win.scene("cursor"):child(2) node.color = COLORS.CLARET - node:action(am.tween(0.1, { color = COLORS.TRANSPARENT })) + node:action(am.tween(0.1, { color = COLORS.TRANSPARENT3 })) play_sfx(SOUNDS.BIRD2) gui_alert("not enough money") @@ -483,7 +478,7 @@ local function make_game_toolbelt() local toolbelt = am.group( am.group():tag"tower_tooltip_text", - am.rect(win.left, win.bottom, win.right, win.bottom + toolbelt_height, COLORS.TRANSPARENT) + am.rect(win.left, win.bottom, win.right, win.bottom + toolbelt_height, COLORS.TRANSPARENT3) ) :tag"toolbelt" @@ -513,7 +508,6 @@ local function make_game_toolbelt() if win:mouse_pressed("left") then select_toolbelt_button(i) end - break end end @@ -576,7 +570,7 @@ local function make_game_toolbelt() -- de-selecting currently selected tower if any toolbelt("toolbelt_select_square").hidden = true - win.scene:replace("cursor", make_hex_cursor_node(0, COLORS.TRANSPARENT):tag"cursor") + win.scene:replace("cursor", make_hex_cursor_node(0, COLORS.TRANSPARENT3):tag"cursor") end end @@ -657,7 +651,7 @@ local function game_scene() local scene = am.group( am.scale(1):tag"world_scale" ^ game_state.world, - am.translate(HEX_GRID_CENTER):tag"cursor_translate" ^ make_hex_cursor_node(0, COLORS.TRANSPARENT):tag"cursor", + am.translate(HEX_GRID_CENTER):tag"cursor_translate" ^ make_hex_cursor_node(0, COLORS.TRANSPARENT3):tag"cursor", score, money, wave_timer, @@ -671,14 +665,15 @@ local function game_scene() -- dangling actions run before the main action scene:late_action(game_action) + play_track(SOUNDS.MAIN_THEME) + return scene end --- this is a stupid name, it just returns a scene node group of hexagons in a hexagonal shape centered at 0,0, of size |radius| -- |color_f| can be a function that takes a hex and returns a color, or just a color -- optionally, |action_f| is a function that operates on the group node every frame function make_hex_cursor_node(radius, color_f, action_f, min_radius) - local color = type(color_f) == "userdata" and color_f or nil + local color = type(color_f) == "userdata" and color_f or COLORS.TRANSPARENT3 local group = am.group() if not min_radius then @@ -712,6 +707,7 @@ function game_end() local hmob = table.highest_index(game_state.mobs) local htower = table.highest_index(game_state.towers) local hprojectile = table.highest_index(game_state.projectiles) + gui_alert(string.format( "\nmobs spawned: %d\ntowers built: %d\nprojectiles spawned: %d\n", hmob, htower, hprojectile @@ -746,8 +742,8 @@ function game_init(saved_state) end game = true - win.scene:remove("game") - win.scene:append(game_scene()) + switch_context(game_scene()) + collectgarbage("stop") end diff --git a/src/grid.lua b/src/grid.lua index 0737d16..9a02ee5 100644 --- a/src/grid.lua +++ b/src/grid.lua @@ -19,6 +19,7 @@ do -- given a grid width gx, and window width wx, what's the smallest size a hex can be to fill the whole screen? -- wx / (gx * 3 / 2) HEX_SIZE = win.width / ((HEX_GRID_WIDTH - padding) * 3 / 2) + hex_set_default_size(vec2(HEX_SIZE)) HEX_PIXEL_WIDTH = hex_width(HEX_SIZE, HEX_ORIENTATION.FLAT) HEX_PIXEL_HEIGHT = hex_height(HEX_SIZE, HEX_ORIENTATION.FLAT) @@ -88,13 +89,7 @@ HEX_GRID_MAXIMUM_ELEVATION = 1 function grid_cost(map, from, to) local t1, t2 = hex_map_get(map, from), hex_map_get(map, to) - local elevation_epsilon = HEX_GRID_MAXIMUM_ELEVATION - HEX_GRID_MINIMUM_ELEVATION + 0.2 - local elevation_cost = 2 + math.abs(t1.elevation)^0.5 - math.abs(t2.elevation)^0.5 - - local epsilon = elevation_epsilon - local cost = elevation_cost - - return cost + return 2 + math.abs(t1.elevation)^0.5 - math.abs(t2.elevation)^0.5 end function grid_neighbours(map, hex) @@ -173,7 +168,7 @@ function building_tower_breaks_flow_field(tower_type, hex) return result, flow_field end -function map_elevation_color(elevation) +function map_elevation_to_color(elevation) if elevation < -0.5 then -- lowest elevation return COLORS.WATER{ a = (elevation + 1.4) / 2 + 0.2 } @@ -201,41 +196,98 @@ function make_hex_node(hex, tile, color) local mask = vec4(0, 0, 0, math.max(((evenq.x - HEX_GRID_WIDTH/2) / HEX_GRID_WIDTH) ^ 2 , ((-evenq.y - HEX_GRID_HEIGHT/2) / HEX_GRID_HEIGHT) ^ 2)) - color = map_elevation_color(tile.elevation) - mask + color = map_elevation_to_color(tile.elevation) - mask end return am.translate(hex_to_pixel(vec2(hex.x, hex.y), vec2(HEX_SIZE))) ^ am.circle(vec2(0), HEX_SIZE, color, 6) end - function make_hex_grid_scene(map, do_generate_flow_field) - -- the world's appearance relies largely on a backdrop which can be scaled in - -- tone to give the appearance of light or darkness - -- @NOTE replace this with a shader program - -- interestingly, if it's colored white, it almost gives the impression of a winter biome - local neg_mask = am.rect( - 0, - 0, - HEX_GRID_PIXEL_WIDTH, - HEX_GRID_PIXEL_HEIGHT, - COLORS.TRUE_BLACK - ) - :tag"negative_mask" + local world = am.group():tag"world" - local world = am.group(neg_mask):tag"world" - for i,_ in pairs(map) do - for j,tile in pairs(map[i]) do - local node = make_hex_node(vec2(i, j), tile) + local texture = TEXTURES.WHITE - hex_map_set(map, i, j, { - elevation = tile.elevation, - node = node - }) + local quads = am.quads(map.size * 2, {"vert", "vec2", "uv", "vec2", "color", "vec4"}) + quads.usage = "static" -- see am.buffer documentation, hint to gpu + local prog = am.program([[ + precision highp float; + + attribute vec2 uv; + attribute vec2 vert; + attribute vec4 color; + + uniform mat4 MV; + uniform mat4 P; - world:append(node) + varying vec2 v_uv; + varying vec4 v_color; + + void main() { + v_uv = uv; + v_color = color; + gl_Position = P * MV * vec4(vert, 0.0, 1.0); + } + ]], [[ + precision mediump float; + + uniform sampler2D texture; + + varying vec2 v_uv; + varying vec4 v_color; + + void main() { + gl_FragColor = texture2D(texture, v_uv) * v_color; + } + ]]) + + local s60 = math.sin(math.rad(60)) + local c60 = math.cos(math.rad(60)) + for i,_ in pairs(map) do + for j,tile in pairs(map[i]) do + local v = vec2(i, j) + local p = hex_to_pixel(v) + local d = math.distance(p, vec2(0)) -- distance to center + + -- light shading on edge cells, scaled by distance to center + local mask = vec4(0, 0, 0, 1/d) + local color = map_elevation_to_color(tile.elevation) - mask + + local radius = HEX_SIZE + quads:add_quad{ + vert = { + p.x - c60 * radius, p.y + s60 * radius, + p.x - radius, p.y, + p.x + radius, p.y, + p.x + c60 * radius, p.y + s60 * radius + }, + uv = am.vec2_array{ + vec2(0, 0), + vec2(1, 0), + vec2(1, 1), + vec2(0, 1) + }, + color = color, + } + quads:add_quad{ + vert = { + p.x - radius, p.y, + p.x - c60 * radius, p.y - s60 * radius, + p.x + c60 * radius, p.y - s60 * radius, + p.x + radius, p.y + }, + uv = am.vec2_array{ + vec2(0, 0), + vec2(1, 0), + vec2(1, 1), + vec2(0, 1) + }, + color = color, + } end end + world:append(am.blend("alpha") ^ am.use_program(prog) ^ am.bind{ texture = texture } ^ quads) + -- add the magenta diamond that represents 'home' world:append( am.translate(hex_to_pixel(HEX_GRID_CENTER, vec2(HEX_SIZE))) @@ -284,7 +336,7 @@ function random_map(seed) end hex_map_set(map, i, j, { - elevation = noise, + elevation = noise }) end end diff --git a/src/hexyz.lua b/src/hexyz.lua index f3fed50..6c5d5e2 100644 --- a/src/hexyz.lua +++ b/src/hexyz.lua @@ -7,6 +7,7 @@ -- (vec2, mat2) -- and some utility functions not present in your standard lua, like: -- table.append +-- @TODO we probably shouldn't return vec2's at all. it's annoying to serialize vec2s, which is stupid because they should be easy -- @TODO if not table.append then end @@ -36,6 +37,11 @@ local HEX_DEFAULT_ORIENTATION = HEX_ORIENTATION.FLAT -- 'size' here is distance from the centerpoint to any vertex in pixel local HEX_DEFAULT_SIZE = vec2(26) +-- if you need to dynamically calculate the default hexagon size, you can pass it here after +function hex_set_default_size(size) + HEX_DEFAULT_SIZE = size +end + -- actual width (longest contained horizontal line) of the hexagon function hex_width(size, orientation) local orientation = orientation or HEX_DEFAULT_ORIENTATION @@ -302,12 +308,18 @@ function hex_parallelogram_map(width, height, seed) end end return setmetatable(map, { __index = { + seed = seed, + get = function(hex, y) + return hex_map_get(map, hex, y) + end, + set = function(hex, y, v) + hex_map_set(map, hex, y, v) + end, width = width, height = height, - seed = seed, neighbours = function(hex) - return table.filter(hex_neighbours(hex), function(_hex) - return hex_map_get(map, _hex) + return table.filter(hex_neighbours(hex), function(hex_) + return map.get(hex_) end) end }}) @@ -337,11 +349,17 @@ function hex_triangular_map(size, seed) end end return setmetatable(map, { __index = { - size = size, seed = seed, + size = size, + get = function(hex, y) + return hex_map_get(map, hex, y) + end, + set = function(hex, y, v) + hex_map_set(map, hex, y, v) + end, neighbours = function(hex) - return table.filter(hex_neighbours(hex), function(_hex) - return hex_map_get(map, _hex) + return table.filter(hex_neighbours(hex), function(hex_) + return map.get(hex_) end) end }}) @@ -349,8 +367,11 @@ end -- Returns Unordered Hexagonal Map of |radius| with Simplex Noise function hex_hexagonal_map(radius, seed) - local seed = seed or math.random(radius * 2 * math.pi) + -- @NOTE usually i try and generate a seed within the range of the area of the map, but for lua's math.random starts to exhibit some really weird behavior + -- when you seed it with a high integer value, so I changed 'radius^2' to just 'radius' here. + local seed = seed or math.random(math.floor(2 * math.pi * radius)) + local size = 0 local map = {} for i = -radius, radius do map[i] = {} @@ -373,14 +394,22 @@ function hex_hexagonal_map(radius, seed) noise = noise + f * math.simplex(pos * l) end map[i][j] = noise + size = size + 1 end end return setmetatable(map, { __index = { - radius = radius, seed = seed, + get = function(hex, y) + return hex_map_get(map, hex, y) + end, + set = function(hex, y, v) + hex_map_set(map, hex, y, v) + end, + radius = radius, + size = size, neighbours = function(hex) - return table.filter(hex_neighbours(hex), function(_hex) - return hex_map_get(map, _hex.x, _hex.y) + return table.filter(hex_neighbours(hex), function(hex_) + return map.get(hex_) end) end }}) @@ -410,7 +439,6 @@ function hex_rectangular_map(width, height, orientation, seed, do_generate_noise noise = noise + f * math.simplex(pos * l) end j = j - math.floor(i/2) -- this is what makes it rectangular - hex_map_set(map, i, j, noise) else j = j - math.floor(i/2) -- this is what makes it rectangular @@ -431,12 +459,19 @@ function hex_rectangular_map(width, height, orientation, seed, do_generate_noise end return setmetatable(map, { __index = { + seed = seed, + get = function(hex, y) + return hex_map_get(map, hex, y) + end, + set = function(hex, y, v) + hex_map_set(map, hex, y, v) + end, width = width, height = height, - seed = seed, + size = width * height, neighbours = function(hex) - return table.filter(hex_neighbours(hex), function(_hex) - return hex_map_get(map, _hex) + return table.filter(hex_neighbours(hex), function(hex_) + return map.get(hex_) end) end }}) @@ -477,7 +512,7 @@ end function hex_dijkstra(map, start, goal, neighbour_f, cost_f) local frontier = {} - frontier[1] = { hex = start, priority = 0 } + table.insert(frontier, { hex = start, priority = 0 }) local came_from = {} hex_map_set(came_from, start, false) diff --git a/src/map-editor.lua b/src/map-editor.lua index 8e205bb..1a4206b 100644 --- a/src/map-editor.lua +++ b/src/map-editor.lua @@ -41,14 +41,12 @@ local map_editor_scene_menu_options = { { texture = TEXTURES.MAP_EDITOR_HEX, action = function() - win.scene:remove("menu") map_editor_init(game_state and game_state.map and game_state.map.seed) end }, { texture = TEXTURES.UNPAUSE_HEX, action = function() - win.scene:remove("menu") win.scene("map_editor").paused = false end }, @@ -101,29 +99,29 @@ function map_editor_action() if win:key_down"a" then -- make the selected tile 'mountain' map_editor_state.selected_tile.elevation = 0.75 - map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation) + --map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation) elseif win:key_down"w" then -- make the selected tile 'water' map_editor_state.selected_tile.elevation = -0.75 - map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation) + --map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation) elseif win:key_down"d" then -- make the selected tile 'dirt' map_editor_state.selected_tile.elevation = 0.25 - map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation) + --map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation) elseif win:key_down"g" then -- make the selected tile 'grass' map_editor_state.selected_tile.elevation = -0.25 - map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation) + --map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation) end -- fine tune tile's elevation with mouse wheel local mouse_wheel_delta = win:mouse_wheel_delta().y / 100 if map_editor_state.selected_tile and mouse_wheel_delta ~= 0 then map_editor_state.selected_tile.elevation = math.clamp(map_editor_state.selected_tile.elevation + mouse_wheel_delta, -1, 1) - map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation) + --map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation) end end @@ -146,8 +144,6 @@ end function map_editor_init() -- remove existing map_editor scene from the graph if it's there - win.scene:remove("map_editor") - local map_editor_scene = am.group():tag"map_editor" map_editor_scene:late_action(map_editor_action) @@ -164,6 +160,6 @@ function map_editor_init() map_editor_scene:append(map_editor_state.ui) -- add the scene to the window - win.scene:append(map_editor_scene) + switch_context(map_editor_scene) end diff --git a/src/memory.lua b/src/memory.lua index 5fb860b..88b92ea 100644 --- a/src/memory.lua +++ b/src/memory.lua @@ -16,8 +16,7 @@ end function check_if_can_collect_garbage_for_free(frame_start_time, min_fps) -- often this will be polled at the end of a frame to see if we're running fast or slow, -- and if we have some time to kill before the start of the next frame, we could maybe run gc. - -- - if (am.current_time() - frame_start_time) < (1 / min_fps + garbage_collector_average_cycle_time) then + if (am.current_time() - frame_start_time) < (1 / (min_fps or 60) + garbage_collector_average_cycle_time) then run_garbage_collector_cycle() end end diff --git a/src/tower.lua b/src/tower.lua index 3bc3217..a1be60d 100644 --- a/src/tower.lua +++ b/src/tower.lua @@ -20,6 +20,7 @@ function default_tower_target_acquisition_f(tower, tower_index) -- first, find out if a tower even *should*, acquire a target. -- a tower should try and acquire a target if atleast one of its weapons that could be shooting, isn't + if not tower.target_index then for index,mob in pairs(game_state.mobs) do if mob then @@ -36,6 +37,41 @@ end function default_tower_update_f(tower, tower_index) end +-- load tower spec file +TOWER_SPECS = {} +TOWER_TYPE = {} + +function get_tower_spec(tower_type) + return TOWER_SPECS[tower_type] +end +function get_tower_name(tower_type) + return TOWER_SPECS[tower_type].name +end +function get_tower_placement_rules_text(tower_type) + return TOWER_SPECS[tower_type].placement_rules_text +end +function get_tower_short_description(tower_type) + return TOWER_SPECS[tower_type].short_description +end +function get_tower_texture(tower_type) + return TOWER_SPECS[tower_type].texture +end +function get_tower_icon_texture(tower_type) + return TOWER_SPECS[tower_type].icon_texture +end +function get_tower_cost(tower_type) + return TOWER_SPECS[tower_type].cost +end +function get_tower_range(tower_type) + return TOWER_SPECS[tower_type].range +end +function get_tower_fire_rate(tower_type) + return TOWER_SPECS[tower_type].fire_rate +end +function get_tower_size(tower_type) + return TOWER_SPECS[tower_type].size +end + function resolve_tower_specs(spec_file_path) local spec_file = am.load_script(spec_file_path) local error_message @@ -103,7 +139,12 @@ function resolve_tower_specs(spec_file_path) end end - return tower_specs + TOWER_SPECS = tower_specs + for i,t in pairs(TOWER_SPECS) do + TOWER_TYPE[t.id] = i + end + build_tower_cursors() + return else -- runtime error - including syntax errors error_message = result @@ -115,44 +156,8 @@ function resolve_tower_specs(spec_file_path) log(error_message) -- @TODO no matter what fucked up, we should load defaults - return {} -end - - --- load tower spec file -TOWER_SPECS = resolve_tower_specs("data/towers.lua") - - - -function get_tower_spec(tower_type) - return TOWER_SPECS[tower_type] -end -function get_tower_name(tower_type) - return TOWER_SPECS[tower_type].name -end -function get_tower_placement_rules_text(tower_type) - return TOWER_SPECS[tower_type].placement_rules_text -end -function get_tower_short_description(tower_type) - return TOWER_SPECS[tower_type].short_description -end -function get_tower_texture(tower_type) - return TOWER_SPECS[tower_type].texture -end -function get_tower_icon_texture(tower_type) - return TOWER_SPECS[tower_type].icon_texture -end -function get_tower_cost(tower_type) - return TOWER_SPECS[tower_type].cost -end -function get_tower_range(tower_type) - return TOWER_SPECS[tower_type].range -end -function get_tower_fire_rate(tower_type) - return TOWER_SPECS[tower_type].fire_rate -end -function get_tower_size(tower_type) - return TOWER_SPECS[tower_type].size + TOWER_SPECS = {} + build_tower_cursors() end local function default_tower_weapon_target_acquirer(tower, tower_index) @@ -219,11 +224,11 @@ function make_tower_node(tower_type) end end -do +function build_tower_cursors() local tower_cursors = {} for i,tower_spec in pairs(TOWER_SPECS) do local tower_sprite = make_tower_node(i) - tower_sprite.color = COLORS.TRANSPARENT + tower_sprite.color = COLORS.TRANSPARENT3 local coroutine_ = coroutine.create(function(node) local flash_on = {} diff --git a/texture.lua b/texture.lua deleted file mode 100644 index 3bb5df8..0000000 --- a/texture.lua +++ /dev/null @@ -1,84 +0,0 @@ - -local fail_count = 0 -local function load_texture(filepath) - local status, texture = pcall(am.texture2d, filepath) - - if status then - return texture - else - fail_count = fail_count + 1 - log(filepath) - return am.texture2d("res/bagel.jpg") - end -end - -TEXTURES = { - -- note that in amulet, if you prefix paths with './', they fail to be found in the exported data.pak - LOGO = load_texture("res/logo.png"), - GEM1 = load_texture("res/gem1.png"), - - SHADED_HEX = load_texture("res/shaded_hex.png"), - NEW_GAME_HEX = load_texture("res/newgamehex.png"), - SAVE_GAME_HEX = load_texture("res/savegamehex.png"), - LOAD_GAME_HEX = load_texture("res/loadgamehex.png"), - SETTINGS_HEX = load_texture("res/settingshex.png"), - MAP_EDITOR_HEX = load_texture("res/mapeditorhex.png"), - ABOUT_HEX = load_texture("res/abouthex.png"), - QUIT_HEX = load_texture("res/quithex.png"), - UNPAUSE_HEX = load_texture("res/unpausehex.png"), - MAIN_MENU_HEX = load_texture("res/mainmenuhex.png"), - - CURTAIN = load_texture("res/curtain1.png"), - - SOUND_ON1 = load_texture("res/sound-on.png"), - SOUND_OFF = load_texture("res/sound-off.png"), - - -- gui stuff - BUTTON1 = load_texture("res/button1.png"), - WIDER_BUTTON1 = load_texture("res/wider_button1.png"), - GEAR = load_texture("res/gear.png"), - - SELECT_BOX = load_texture("res/select_box.png"), - - -- tower stuff - TOWER_WALL = load_texture("res/tower_wall.png"), - TOWER_WALL_ICON = load_texture("res/tower_wall_icon.png"), - TOWER_GATTLER = load_texture("res/tower_gattler.png"), - TOWER_GATTLER_ICON = load_texture("res/tower_gattler_icon.png"), - TOWER_HOWITZER = load_texture("res/tower_howitzer.png"), - TOWER_HOWITZER_ICON = load_texture("res/tower_howitzer_icon.png"), - TOWER_REDEYE = load_texture("res/tower_redeye.png"), - TOWER_REDEYE_ICON = load_texture("res/tower_redeye_icon.png"), - TOWER_MOAT = load_texture("res/tower_moat.png"), - TOWER_MOAT_ICON = load_texture("res/tower_moat_icon.png"), - TOWER_RADAR = load_texture("res/tower_radar.png"), - TOWER_RADAR_ICON = load_texture("res/tower_radar_icon.png"), - TOWER_LIGHTHOUSE = load_texture("res/tower_lighthouse.png"), - TOWER_LIGHTHOUSE_ICON = load_texture("res/tower_lighthouse_icon.png"), - - -- mob stuff - MOB_BEEPER = load_texture("res/mob_beeper.png"), - MOB_SPOODER = load_texture("res/mob_spooder.png"), - MOB_VELKOOZ = load_texture("res/mob_velkooz.png"), - MOB_VELKOOZ1 = load_texture("res/mob_velkooz1.png"), - MOB_VELKOOZ2 = load_texture("res/mob_velkooz2.png"), - MOB_VELKOOZ3 = load_texture("res/mob_velkooz3.png"), -} - -function pack_texture_into_sprite(texture, width, height, color) - local sprite = am.sprite{ - texture = texture, - s1 = 0, s2 = 1, t1 = 0, t2 = 1, - x1 = 0, x2 = width, width = width, - y1 = 0, y2 = height, height = height - } - - if color then sprite.color = color end - - return sprite -end - -if fail_count > 0 then - log("failed to load %d texture(s)", fail_count) -end -