Browse Source

some rather severe refactors

master
Nicholas Hayashi 3 years ago
parent
commit
bdd30f6e51
  1. 10
      STYLEGUIDE.md
  2. 6
      conf.lua
  3. 4
      curves.lua
  4. 8
      data/towers.lua
  5. 12
      lib/color.lua
  6. 100
      lib/extra.lua
  7. 23
      lib/geometry.lua
  8. 223
      lib/gui.lua
  9. 33
      lib/memory.lua
  10. 76
      lib/random.lua
  11. 25
      lib/sound.lua
  12. 105
      lib/texture.lua
  13. 275
      main.lua
  14. BIN
      res/cells.jpg
  15. 0
      res/img/abouthex.png
  16. 0
      res/img/bagel.jpg
  17. 0
      res/img/button1.png
  18. 0
      res/img/curtain1.png
  19. 0
      res/img/gear.png
  20. 0
      res/img/gears.png
  21. 0
      res/img/gem1.png
  22. 0
      res/img/loadgamehex.png
  23. 0
      res/img/logo.png
  24. 0
      res/img/mainmenuhex.png
  25. 0
      res/img/mapeditorhex.png
  26. 0
      res/img/mob_beeper.png
  27. 0
      res/img/mob_spooder.png
  28. 0
      res/img/mob_velkooz.png
  29. 0
      res/img/mob_velkooz0.png
  30. 0
      res/img/mob_velkooz1.png
  31. 0
      res/img/mob_velkooz2.png
  32. 0
      res/img/mob_velkooz3.png
  33. 0
      res/img/newgamehex.png
  34. 0
      res/img/quithex.png
  35. 0
      res/img/savegamehex.png
  36. 0
      res/img/select_box.png
  37. 0
      res/img/settingshex.png
  38. 0
      res/img/shaded_hex.png
  39. 0
      res/img/slider.png
  40. 0
      res/img/sound-off.png
  41. 0
      res/img/sound-on.png
  42. 0
      res/img/tower_gattler.png
  43. 0
      res/img/tower_gattler_icon.png
  44. 0
      res/img/tower_howitzer.png
  45. 0
      res/img/tower_howitzer_icon.png
  46. 0
      res/img/tower_lighthouse.png
  47. 0
      res/img/tower_lighthouse_icon.png
  48. 0
      res/img/tower_moat.png
  49. 0
      res/img/tower_moat_icon.png
  50. 0
      res/img/tower_radar.png
  51. 0
      res/img/tower_radar_icon.png
  52. 0
      res/img/tower_redeye.png
  53. 0
      res/img/tower_redeye_icon.png
  54. 0
      res/img/tower_wall.png
  55. 0
      res/img/tower_wall_icon.png
  56. 0
      res/img/unpausehex.png
  57. BIN
      res/img/white-texture.png
  58. 0
      res/img/wider_button1.png
  59. 0
      res/ogg/main_theme.ogg
  60. 4
      src/entity.lua
  61. 1
      src/extra.lua
  62. 32
      src/game.lua
  63. 116
      src/grid.lua
  64. 65
      src/hexyz.lua
  65. 16
      src/map-editor.lua
  66. 3
      src/memory.lua
  67. 87
      src/tower.lua
  68. 84
      texture.lua

10
STYLEGUIDE.md

@ -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

6
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"

4
curves.lua

@ -1,4 +0,0 @@
-- nice curve, f(0) = ~1.1, f(100) = ~100, exponential slope roughly inbetween
-- math.exp((x + 1)/22) / 100
--

8
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",

12
color.lua → 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)
}

100
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

23
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

223
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

33
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

76
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

25
sound.lua → 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

105
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

275
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()

BIN
res/cells.jpg

Before

Width: 2000  |  Height: 2000  |  Size: 716 KiB

0
res/abouthex.png → res/img/abouthex.png

Before

Width: 282  |  Height: 290  |  Size: 59 KiB

After

Width: 282  |  Height: 290  |  Size: 59 KiB

0
res/bagel.jpg → res/img/bagel.jpg

Before

Width: 820  |  Height: 820  |  Size: 165 KiB

After

Width: 820  |  Height: 820  |  Size: 165 KiB

0
res/button1.png → res/img/button1.png

Before

Width: 128  |  Height: 128  |  Size: 454 B

After

Width: 128  |  Height: 128  |  Size: 454 B

0
res/curtain1.png → res/img/curtain1.png

Before

Width: 1920  |  Height: 1080  |  Size: 238 KiB

After

Width: 1920  |  Height: 1080  |  Size: 238 KiB

0
res/gear.png → res/img/gear.png

Before

Width: 200  |  Height: 200  |  Size: 4.8 KiB

After

Width: 200  |  Height: 200  |  Size: 4.8 KiB

0
res/gears.png → res/img/gears.png

Before

Width: 980  |  Height: 798  |  Size: 62 KiB

After

Width: 980  |  Height: 798  |  Size: 62 KiB

0
res/gem1.png → res/img/gem1.png

Before

Width: 419  |  Height: 483  |  Size: 99 KiB

After

Width: 419  |  Height: 483  |  Size: 99 KiB

0
res/loadgamehex.png → res/img/loadgamehex.png

Before

Width: 282  |  Height: 290  |  Size: 50 KiB

After

Width: 282  |  Height: 290  |  Size: 50 KiB

0
res/logo.png → res/img/logo.png

Before

Width: 905  |  Height: 483  |  Size: 138 KiB

After

Width: 905  |  Height: 483  |  Size: 138 KiB

0
res/mainmenuhex.png → res/img/mainmenuhex.png

Before

Width: 282  |  Height: 290  |  Size: 59 KiB

After

Width: 282  |  Height: 290  |  Size: 59 KiB

0
res/mapeditorhex.png → res/img/mapeditorhex.png

Before

Width: 283  |  Height: 290  |  Size: 58 KiB

After

Width: 283  |  Height: 290  |  Size: 58 KiB

0
res/mob_beeper.png → res/img/mob_beeper.png

Before

Width: 638  |  Height: 639  |  Size: 6.1 KiB

After

Width: 638  |  Height: 639  |  Size: 6.1 KiB

0
res/mob_spooder.png → res/img/mob_spooder.png

Before

Width: 40  |  Height: 40  |  Size: 558 B

After

Width: 40  |  Height: 40  |  Size: 558 B

0
res/mob_velkooz.png → res/img/mob_velkooz.png

Before

Width: 450  |  Height: 392  |  Size: 26 KiB

After

Width: 450  |  Height: 392  |  Size: 26 KiB

0
res/mob_velkooz0.png → res/img/mob_velkooz0.png

Before

Width: 64  |  Height: 64  |  Size: 2.7 KiB

After

Width: 64  |  Height: 64  |  Size: 2.7 KiB

0
res/mob_velkooz1.png → res/img/mob_velkooz1.png

Before

Width: 64  |  Height: 64  |  Size: 1.9 KiB

After

Width: 64  |  Height: 64  |  Size: 1.9 KiB

0
res/mob_velkooz2.png → res/img/mob_velkooz2.png

Before

Width: 64  |  Height: 64  |  Size: 1.9 KiB

After

Width: 64  |  Height: 64  |  Size: 1.9 KiB

0
res/mob_velkooz3.png → res/img/mob_velkooz3.png

Before

Width: 64  |  Height: 64  |  Size: 1.8 KiB

After

Width: 64  |  Height: 64  |  Size: 1.8 KiB

0
res/newgamehex.png → res/img/newgamehex.png

Before

Width: 282  |  Height: 290  |  Size: 59 KiB

After

Width: 282  |  Height: 290  |  Size: 59 KiB

0
res/quithex.png → res/img/quithex.png

Before

Width: 282  |  Height: 290  |  Size: 57 KiB

After

Width: 282  |  Height: 290  |  Size: 57 KiB

0
res/savegamehex.png → res/img/savegamehex.png

Before

Width: 283  |  Height: 290  |  Size: 58 KiB

After

Width: 283  |  Height: 290  |  Size: 58 KiB

0
res/select_box.png → res/img/select_box.png

Before

Width: 128  |  Height: 128  |  Size: 404 B

After

Width: 128  |  Height: 128  |  Size: 404 B

0
res/settingshex.png → res/img/settingshex.png

Before

Width: 282  |  Height: 290  |  Size: 57 KiB

After

Width: 282  |  Height: 290  |  Size: 57 KiB

0
res/shaded_hex.png → res/img/shaded_hex.png

Before

Width: 253  |  Height: 290  |  Size: 49 KiB

After

Width: 253  |  Height: 290  |  Size: 49 KiB

0
res/slider.png → res/img/slider.png

Before

Width: 400  |  Height: 20  |  Size: 442 B

After

Width: 400  |  Height: 20  |  Size: 442 B

0
res/sound-off.png → res/img/sound-off.png

Before

Width: 980  |  Height: 728  |  Size: 28 KiB

After

Width: 980  |  Height: 728  |  Size: 28 KiB

0
res/sound-on.png → res/img/sound-on.png

Before

Width: 835  |  Height: 653  |  Size: 123 KiB

After

Width: 835  |  Height: 653  |  Size: 123 KiB

0
res/tower_gattler.png → res/img/tower_gattler.png

Before

Width: 64  |  Height: 64  |  Size: 335 B

After

Width: 64  |  Height: 64  |  Size: 335 B

0
res/tower_gattler_icon.png → res/img/tower_gattler_icon.png

Before

Width: 64  |  Height: 64  |  Size: 360 B

After

Width: 64  |  Height: 64  |  Size: 360 B

0
res/tower_howitzer.png → res/img/tower_howitzer.png

Before

Width: 64  |  Height: 64  |  Size: 292 B

After

Width: 64  |  Height: 64  |  Size: 292 B

0
res/tower_howitzer_icon.png → res/img/tower_howitzer_icon.png

Before

Width: 64  |  Height: 64  |  Size: 366 B

After

Width: 64  |  Height: 64  |  Size: 366 B

0
res/tower_lighthouse.png → res/img/tower_lighthouse.png

Before

Width: 64  |  Height: 64  |  Size: 796 B

After

Width: 64  |  Height: 64  |  Size: 796 B

0
res/tower_lighthouse_icon.png → res/img/tower_lighthouse_icon.png

Before

Width: 64  |  Height: 64  |  Size: 796 B

After

Width: 64  |  Height: 64  |  Size: 796 B

0
res/tower_moat.png → res/img/tower_moat.png

Before

Width: 137  |  Height: 137  |  Size: 1.4 KiB

After

Width: 137  |  Height: 137  |  Size: 1.4 KiB

0
res/tower_moat_icon.png → res/img/tower_moat_icon.png

Before

Width: 137  |  Height: 137  |  Size: 1.4 KiB

After

Width: 137  |  Height: 137  |  Size: 1.4 KiB

0
res/tower_radar.png → res/img/tower_radar.png

Before

Width: 300  |  Height: 300  |  Size: 30 KiB

After

Width: 300  |  Height: 300  |  Size: 30 KiB

0
res/tower_radar_icon.png → res/img/tower_radar_icon.png

Before

Width: 300  |  Height: 300  |  Size: 30 KiB

After

Width: 300  |  Height: 300  |  Size: 30 KiB

0
res/tower_redeye.png → res/img/tower_redeye.png

Before

Width: 64  |  Height: 64  |  Size: 637 B

After

Width: 64  |  Height: 64  |  Size: 637 B

0
res/tower_redeye_icon.png → res/img/tower_redeye_icon.png

Before

Width: 64  |  Height: 64  |  Size: 637 B

After

Width: 64  |  Height: 64  |  Size: 637 B

0
res/tower_wall.png → res/img/tower_wall.png

Before

Width: 126  |  Height: 131  |  Size: 1.4 KiB

After

Width: 126  |  Height: 131  |  Size: 1.4 KiB

0
res/tower_wall_icon.png → res/img/tower_wall_icon.png

Before

Width: 126  |  Height: 131  |  Size: 1.4 KiB

After

Width: 126  |  Height: 131  |  Size: 1.4 KiB

0
res/unpausehex.png → res/img/unpausehex.png

Before

Width: 282  |  Height: 290  |  Size: 59 KiB

After

Width: 282  |  Height: 290  |  Size: 59 KiB

BIN
res/img/white-texture.png

After

Width: 32  |  Height: 32  |  Size: 117 B

0
res/wider_button1.png → res/img/wider_button1.png

Before

Width: 256  |  Height: 128  |  Size: 533 B

After

Width: 256  |  Height: 128  |  Size: 533 B

0
res/maintheme.ogg → res/ogg/main_theme.ogg

4
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 }

1
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.

32
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

116
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

65
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)

16
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

3
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

87
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 = {}

84
texture.lua

@ -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
Loading…
Cancel
Save