diff --git a/main.lua b/main.lua index 906da57..a69f87e 100644 --- a/main.lua +++ b/main.lua @@ -26,7 +26,7 @@ do } end --- assets and/or trivial code +-- asset interfaces and/or trivial code require "color" require "sound" require "texture" @@ -42,10 +42,64 @@ require "src/mob" require "src/projectile" require "src/tower" -function main_action() end -function main_scene() end + +function main_action(self) + self"hex_backdrop""rotate".angle = math.wrapf(self"hex_backdrop""rotate".angle - 0.002 * am.delta_time, math.pi*2) +end + +function make_main_scene_toolbelt() + local options = { + { + label = "new game", + action = function(self) end + }, + { + label = "load game", + action = function(self) game_init(am.load_state("save", "json")) end + }, + { + label = "map editor", + action = function(self) log("map editor not implemented") end + }, + { + label = "settings", + action = function(self) end + }, + } + --local map = hex_rectangular_map(10, 20, HEX_ORIENTATION.POINTY) + + return group +end + +function main_scene() + local group = am.group() + + local map = hex_hexagonal_map(30) + 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) + color = color{a=color.a - 0.1} + + local node = am.translate(hex_to_pixel(vec2(i, j), vec2(HEX_SIZE))) + ^ am.circle(vec2(0), HEX_SIZE, vec4(0), 6) + + node"circle":action(am.tween(1, { color = color })) + + hex_backdrop:append(node) + end + end + group:append(hex_backdrop) + + group:append(am.translate(0, 200) ^ am.sprite("res/logo.png")) + group:append(make_main_scene_toolbelt()) + + group:action(main_action) + + return group +end win.scene = am.group() -game_init(nil) +game_init() noglobals() diff --git a/res/shaded_hex1.png b/res/shaded_hex1.png new file mode 100644 index 0000000..0a5fe4e Binary files /dev/null and b/res/shaded_hex1.png differ diff --git a/src/extra.lua b/src/extra.lua index 3c38873..491c7d0 100644 --- a/src/extra.lua +++ b/src/extra.lua @@ -9,10 +9,6 @@ function fprofile(f, ...) return result end -function booltostring(bool) - return bool and "true" or "false" -end - function math.wrapf(float, range) return float - range * math.floor(float / range) end diff --git a/src/game.lua b/src/game.lua index 4c8d113..8c2a130 100644 --- a/src/game.lua +++ b/src/game.lua @@ -82,7 +82,7 @@ local function get_top_right_display_text(hex, evenq, centered_evenq, display_ty str = "SEED: " .. state.map.seed elseif display_type == TRDTS.TILE then - str = table.tostring(map_get(state.map, hex)) + str = table.tostring(hex_map_get(state.map, hex)) end return str end @@ -247,7 +247,7 @@ local function game_action(scene) local evenq = hex_to_evenq(hex) local centered_evenq = evenq{ y = -evenq.y } - vec2(math.floor(HEX_GRID_WIDTH/2) , math.floor(HEX_GRID_HEIGHT/2)) - local tile = map_get(state.map, hex) + local tile = hex_map_get(state.map, hex) local interactable = evenq_is_in_interactable_region(evenq{ y = -evenq.y }) local buildable = tower_type_is_buildable_on(hex, tile, state.selected_tower_type) @@ -416,7 +416,8 @@ 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) - }:tag"toolbelt" + } + :tag"toolbelt" local padding = 15 local size = toolbelt_height - padding @@ -430,7 +431,9 @@ local function make_game_toolbelt() local tower_select_square = ( am.translate(vec2(size + padding, half_size) + offset) ^ am.rect(-size/2-3, -size/2-3, size/2+3, size/2+3, COLORS.SUNRAY) - ):tag"tower_select_square" + ) + :tag"tower_select_square" + tower_select_square.hidden = true toolbelt:append(tower_select_square) @@ -503,7 +506,7 @@ end -- optionally, |action_f| is a function that operates on the group node every frame function make_hex_cursor(radius, color_f, action_f) local color = type(color_f) == "userdata" and color_f or nil - local map = spiral_map(vec2(0), radius) + local map = hex_spiral_map(vec2(0), radius) local group = am.group() for _,h in pairs(map) do @@ -557,22 +560,15 @@ end function game_init(saved_state) if saved_state then state = game_deserialize(saved_state) - - -- scene nodes aren't (can't be?) serialized, so we re-generate them if we're loading from a save - - else state = get_initial_game_state() + local home_tower = build_tower(HEX_GRID_CENTER, TOWER_TYPE.RADAR) + for _,h in pairs(home_tower.hexes) do + -- @HACK to make the center tile(s) passable even though there's a tower on it + hex_map_get(state.map, h).elevation = 0 + end end - --[[ - local home_tower = build_tower(HEX_GRID_CENTER, TOWER_TYPE.RADAR) - for _,h in pairs(home_tower.hexes) do - -- @HACK to make the center tile(s) passable even though there's a tower on it - map_get(state.map, h).elevation = 0 - end - ]] - win.scene:remove("game") win.scene:append(game_scene()) end diff --git a/src/grid.lua b/src/grid.lua index 2f43e96..6e3c70d 100644 --- a/src/grid.lua +++ b/src/grid.lua @@ -3,8 +3,8 @@ -- distance from hex centerpoint to any vertex HEX_SIZE = 26 -HEX_PIXEL_WIDTH = hex_width(HEX_SIZE, ORIENTATION.FLAT) -HEX_PIXEL_HEIGHT = hex_height(HEX_SIZE, ORIENTATION.FLAT) +HEX_PIXEL_WIDTH = hex_width(HEX_SIZE, HEX_ORIENTATION.FLAT) +HEX_PIXEL_HEIGHT = hex_height(HEX_SIZE, HEX_ORIENTATION.FLAT) HEX_PIXEL_DIMENSIONS = vec2(HEX_PIXEL_WIDTH, HEX_PIXEL_HEIGHT) do @@ -80,7 +80,7 @@ function grid_heuristic(source, target) end function grid_cost(map, from, to) - local t1, t2 = map_get(map, from), map_get(map, to) + local t1, t2 = hex_map_get(map, from), hex_map_get(map, to) -- i have no fucking clue why, but adding +0.2 to the end of this fixes a bug where sometimes two (or more) -- equivalent paths are found and mobs backpedal trying to decide between them @@ -97,13 +97,13 @@ end function grid_neighbours(map, hex) return table.filter(hex_neighbours(hex), function(_hex) - local tile = map_get(map, _hex) + local tile = hex_map_get(map, _hex) return tile and tile_is_medium_elevation(tile) end) end function generate_flow_field(map, start) - return dijkstra(map, start, nil, grid_cost, grid_neighbours) + return hex_dijkstra(map, start, nil, grid_cost, grid_neighbours) end function apply_flow_field(map, flow_field, world) @@ -115,7 +115,7 @@ function apply_flow_field(map, flow_field, world) local overlay_group = am.group():tag"flow_field" for i,_ in pairs(map) do for j,f in pairs(map[i]) do - local flow = map_get(flow_field, i, j) + local flow = hex_map_get(flow_field, i, j) if flow then map[i][j].priority = flow.priority @@ -134,38 +134,12 @@ function apply_flow_field(map, flow_field, world) end end --- some convenience functions for setting and retrieving values from a 2d sparse array --- where the first index might return a nil value, causing the second second to crash the game --- and where it's often the case that the indexer is a vec2 -function map_get(map, hex, y) - if y then return map[hex] and map[hex][y] end - return map[hex.x] and map[hex.x][hex.y] -end - -function map_set(map, hex, y, v) - if v then - if map[hex] then - map[hex][y] = v - else - map[hex] = {} - map[hex][y] = v - end - else - if map[hex.x] then - map[hex.x][hex.y] = y - else - map[hex.x] = {} - map[hex.x][hex.y] = y - end - end -end - function building_tower_breaks_flow_field(tower_type, hex) local original_elevations = {} local all_impassable = true - local hexes = spiral_map(hex, get_tower_size(tower_type)) + local hexes = hex_spiral_map(hex, get_tower_size(tower_type)) for _,h in pairs(hexes) do - local tile = map_get(state.map, h) + local tile = hex_map_get(state.map, h) if all_impassable and mob_can_pass_through(nil, h) then all_impassable = false @@ -182,41 +156,42 @@ function building_tower_breaks_flow_field(tower_type, hex) -- (besides return all the tile's elevations back to their original state) if all_impassable then for i,h in pairs(hexes) do - map_get(state.map, h).elevation = original_elevations[i] + hex_map_get(state.map, h).elevation = original_elevations[i] end return false end local flow_field = generate_flow_field(state.map, HEX_GRID_CENTER) - local result = not map_get(flow_field, 0, 0) + local result = not hex_map_get(flow_field, 0, 0) for i,h in pairs(hexes) do - map_get(state.map, h).elevation = original_elevations[i] + hex_map_get(state.map, h).elevation = original_elevations[i] end return result, flow_field end -function make_hex_grid_scene(map) - local function color_at(elevation) - if elevation < -0.5 then -- lowest elevation - return COLORS.WATER{ a = (elevation + 1.4) / 2 + 0.2 } +function map_elevation_color(elevation) + if elevation < -0.5 then -- lowest elevation + return COLORS.WATER{ a = (elevation + 1.4) / 2 + 0.2 } - elseif elevation < 0 then -- med-low elevation - return math.lerp(COLORS.DIRT, COLORS.GRASS, elevation + 0.5){ a = (elevation + 1.8) / 2 + 0.3 } + elseif elevation < 0 then -- med-low elevation + return math.lerp(COLORS.DIRT, COLORS.GRASS, elevation + 0.5){ a = (elevation + 1.8) / 2 + 0.3 } - elseif elevation < 0.5 then -- med-high elevation - return math.lerp(COLORS.DIRT, COLORS.GRASS, elevation + 0.5){ a = (elevation + 1.6) / 2 + 0.3 } + elseif elevation < 0.5 then -- med-high elevation + return math.lerp(COLORS.DIRT, COLORS.GRASS, elevation + 0.5){ a = (elevation + 1.6) / 2 + 0.3 } - elseif elevation < 1 then -- high elevation - return COLORS.MOUNTAIN{ ra = elevation } + elseif elevation < 1 then -- high elevation + return COLORS.MOUNTAIN{ ra = elevation } - else - -- @TODO probably fix... this only happens when loading a save, and the tile has an elevation that's - -- higher that anything here - return vec4(0.1) - end + else + -- @TODO probably fix... this only happens when loading a save, and the tile has an elevation that's + -- higher that anything here + return vec4(0.1) end +end + +function make_hex_grid_scene(map) -- 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 @@ -239,12 +214,12 @@ function make_hex_grid_scene(map) 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)) - local color = color_at(tile.elevation) - mask + local color = map_elevation_color(tile.elevation) - mask local node = am.translate(hex_to_pixel(vec2(i, j), vec2(HEX_SIZE))) ^ am.circle(vec2(0), HEX_SIZE, color, 6) - map_set(map, i, j, { + hex_map_set(map, i, j, { elevation = tile.elevation, node = node }) @@ -259,7 +234,12 @@ function make_hex_grid_scene(map) end function random_map(seed) - local map = rectangular_map(HEX_GRID_DIMENSIONS.x, HEX_GRID_DIMENSIONS.y, seed) + local map = hex_rectangular_map( + HEX_GRID_DIMENSIONS.x, + HEX_GRID_DIMENSIONS.y, + HEX_ORIENTATION.FLAT, + seed + ) math.randomseed(map.seed) -- there are some things about the generated map we'd like to change... @@ -286,7 +266,7 @@ function random_map(seed) noise = noise * d^0.125 -- arbitrary, seems to work good end - map_set(map, i, j, { + hex_map_set(map, i, j, { elevation = noise, }) end diff --git a/src/gui.lua b/src/gui.lua index 21d033c..20d179f 100644 --- a/src/gui.lua +++ b/src/gui.lua @@ -4,19 +4,25 @@ function gui_numberfield(dimensions, opts) end - -function gui_textfield(position, dimensions, max, disallowed_keys) +function gui_textfield(position, dimensions, max, disallowed_chars) local width, height = dimensions.x, dimensions.y - local disallowed_keys = disallowed_keys or {} - local max = max or 99 - - local padding = 2 - local outer_rect = am.rect(-width/2, -height/2, width/2, height/2, COLORS.VERY_DARK_GRAY) - local inner_rect = am.rect(-width/2 + padding - , -height/2 + padding - , width/2 - padding - , height/2 - padding - , COLORS.PALE_SILVER) + local disallowed_chars = disallowed_chars or {} + local max = max or 10 + + local outer_rect = am.rect( + -width/2, + -height/2, + width/2, + height/2, + COLORS.VERY_DARK_GRAY + ) + local inner_rect = am.rect( + -width/2 + 1, + -height/2 + 1, + width/2 - 2, + height/2 - 2, + COLORS.PALE_SILVER + ) local group = am.group{ outer_rect, @@ -29,29 +35,90 @@ function gui_textfield(position, dimensions, max, disallowed_keys) local keys = win:keys_pressed() if #keys == 0 then return end - -- @HACK all characters and digits are represented by a single string in amulet - -- so we don't have to iterate over everything - -- pattern matching doesn't work because control characters are also just normal strings + local chars = {} + local shift = win:key_down("lshift") or win:key_down("rshift") for i,k in pairs(keys) do - if not disallowed_keys[k] then - if k:len() == 1 then - if string.match(k, "%d") then - self"text".text = self"text".text .. k - - elseif win:key_down("lshift") or win:key_down("rshift") then - self"text".text = self"text".text .. k:upper() - + 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 - self"text".text = self"text".text .. k + table.insert(chars, k) end - elseif k == "space" then - self"text".text = self"text".text .. " " - - elseif k == "backspace" then - self"text".text = self"text".text:sub(1, self"text".text:len() - 1) - - elseif k == "enter" then + 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 + -- @OTOD + 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 diff --git a/src/hexyz.lua b/src/hexyz.lua index 87cdc82..8403f77 100644 --- a/src/hexyz.lua +++ b/src/hexyz.lua @@ -1,14 +1,26 @@ +-- this is a single file with no dependencies which is meant to perform a bunch of mathy stuff +-- related to hexagons, grids of them, and pathfinding on them +-- +-- it basically owes its entire existence to this resource: https://www.redblobgames.com/grids/hexagons/ +-- it uses some datatypes internal to the amulet game engine: http://www.amulet.xyz/ +-- (vec2, mat2) +-- and some utility functions not present in your standard lua, like: +-- table.append + if not math.round then math.round = function(n) return math.floor(n + 0.5) end else - log("clobbering a math.round function.") + error("clobbering a math.round function, oopsie!") +end + +if not table.append then end -- wherever 'orientation' appears as an argument, use one of these two, or set a default just below -ORIENTATION = { +HEX_ORIENTATION = { -- Forward & Inverse Matrices used for the Flat Orientation FLAT = { M = mat2(3.0/2.0, 0.0, 3.0^0.5/2.0, 3.0^0.5 ), @@ -23,70 +35,71 @@ ORIENTATION = { } } --- whenver |orientation| appears as an argument, if it isn't provided, this is used instead. -local DEFAULT_ORIENTATION = ORIENTATION.FLAT +-- whenever |orientation| appears as an argument, if it isn't provided, this is used instead. +-- this is useful because most of the time you will only care about one orientation +local HEX_DEFAULT_ORIENTATION = HEX_ORIENTATION.FLAT -- whenever |size| for a hexagon appears as an argument, if it isn't provided, use this -- 'size' here is distance from the centerpoint to any vertex in pixel -local DEFAULT_HEX_SIZE = vec2(20) +local HEX_DEFAULT_SIZE = vec2(20) -- actual width (longest contained horizontal line) of the hexagon function hex_width(size, orientation) - local orientation = orientation or DEFAULT_ORIENTATION + local orientation = orientation or HEX_DEFAULT_ORIENTATION - if orientation == ORIENTATION.FLAT then + if orientation == HEX_ORIENTATION.FLAT then return size * 2 - elseif orientation == ORIENTATION.POINTY then + elseif orientation == HEX_ORIENTATION.POINTY then return math.sqrt(3) * size end end -- actual height (tallest contained vertical line) of the hexagon function hex_height(size, orientation) - local orientation = orientation or DEFAULT_ORIENTATION + local orientation = orientation or HEX_DEFAULT_ORIENTATION - if orientation == ORIENTATION.FLAT then + if orientation == HEX_ORIENTATION.FLAT then return math.sqrt(3) * size - elseif orientation == ORIENTATION.POINTY then + elseif orientation == HEX_ORIENTATION.POINTY then return size * 2 end end -- returns actual width and height of a hexagon given it's |size| which is the distance from the centerpoint to any vertex in pixels function hex_dimensions(size, orientation) - local orientation = orientation or DEFAULT_ORIENTATION + local orientation = orientation or HEX_DEFAULT_ORIENTATION return vec2(hex_width(size, orientation), hex_height(size, orientation)) end -- distance between two horizontally adjacent hexagon centerpoints function hex_horizontal_spacing(size, orientation) - local orientation = orientation or DEFAULT_ORIENTATION + local orientation = orientation or HEX_DEFAULT_ORIENTATION - if orientation == ORIENTATION.FLAT then + if orientation == HEX_ORIENTATION.FLAT then return hex_width(size, orientation) * 3/4 - elseif orientation == ORIENTATION.POINTY then + elseif orientation == HEX_ORIENTATION.POINTY then return hex_height(size, orientation) end end -- distance between two vertically adjacent hexagon centerpoints function hex_vertical_spacing(size, orientation) - local orientation = orientation or DEFAULT_ORIENTATION + local orientation = orientation or HEX_DEFAULT_ORIENTATION - if orientation == ORIENTATION.FLAT then + if orientation == HEX_ORIENTATION.FLAT then return hex_height(size, orientation) - elseif orientation == ORIENTATION.POINTY then + elseif orientation == HEX_ORIENTATION.POINTY then return hex_width(size, orientation) * 3/4 end end -- returns the distance between adjacent hexagon centers in a grid function hex_spacing(size, orientation) - local orientation = orientation or DEFAULT_ORIENTATION + local orientation = orientation or HEX_DEFAULT_ORIENTATION return vec2(hex_horizontal_spacing(size, orientation), hex_vertical_spacing(size, orientation)) end @@ -137,19 +150,19 @@ end -- Hex to Screen -- Orientation Must be Either POINTY or FLAT function hex_to_pixel(hex, size, orientation) - local M = orientation and orientation.M or DEFAULT_ORIENTATION.M + local M = orientation and orientation.M or HEX_DEFAULT_ORIENTATION.M - local x = (M[1][1] * hex[1] + M[1][2] * hex[2]) * (size and size[1] or DEFAULT_HEX_SIZE[1]) - local y = (M[2][1] * hex[1] + M[2][2] * hex[2]) * (size and size[2] or DEFAULT_HEX_SIZE[2]) + local x = (M[1][1] * hex[1] + M[1][2] * hex[2]) * (size and size[1] or HEX_DEFAULT_SIZE[1]) + local y = (M[2][1] * hex[1] + M[2][2] * hex[2]) * (size and size[2] or HEX_DEFAULT_SIZE[2]) return vec2(x, y) end -- Screen to Hex -- Orientation Must be Either POINTY or FLAT function pixel_to_hex(pix, size, orientation) - local W = orientation and orientation.W or DEFAULT_ORIENTATION.W + local W = orientation and orientation.W or HEX_DEFAULT_ORIENTATION.W - local pix = pix / (size or vec2(DEFAULT_HEX_SIZE)) + local pix = pix / (size or vec2(HEX_DEFAULT_SIZE)) local x = W[1][1] * pix[1] + W[1][2] * pix[2] local y = W[2][1] * pix[1] + W[2][2] * pix[2] @@ -159,14 +172,14 @@ end -- TODO test, learn am.draw function hex_corner_offset(corner, size, orientation) - local orientation = orientation or DEFAULT_ORIENTATION + local orientation = orientation or HEX_DEFAULT_ORIENTATION local angle = 2.0 * math.pi * orientation.angle + corner / 6 return vec2(size[1] * math.cos(angle), size[2] * math.sin(angle)) end -- TODO test this thing function hex_corners(hex, size, orientation) - local orientation = orientation or DEFAULT_ORIENTATION + local orientation = orientation or HEX_DEFAULT_ORIENTATION local corners = {} local center = hex_to_pixel(hex, size, orientation) for i = 0, 5 do @@ -220,7 +233,7 @@ end -- MAPS & STORAGE -- Returns Ordered Ring-Shaped Map of |radius| from |center| -function ring_map(center, radius) +function hex_ring_map(center, radius) local map = {} local walk = center + HEX_DIRECTIONS[6] * radius @@ -235,21 +248,21 @@ function ring_map(center, radius) end -- Returns Ordered Spiral Hexagonal Map of |radius| Rings from |center| -function spiral_map(center, radius) +function hex_spiral_map(center, radius) local map = { center } for i = 1, radius do - table.append(map, ring_map(center, i)) + table.append(map, hex_ring_map(center, i)) end return setmetatable(map, {__index={center=center, radius=radius}}) end -local function map_get(map, hex, y) +function hex_map_get(map, hex, y) if y then return map[hex] and map[hex][y] end return map[hex.x] and map[hex.x][hex.y] end -local function map_set(map, hex, y, v) +function hex_map_set(map, hex, y, v) if v then if map[hex] then map[hex][y] = v @@ -268,7 +281,7 @@ local function map_set(map, hex, y, v) end -- Returns Unordered Parallelogram-Shaped Map of |width| and |height| with Simplex Noise -function parallelogram_map(width, height, seed) +function hex_parallelogram_map(width, height, seed) local seed = seed or math.random(width * height) local map = {} @@ -296,14 +309,14 @@ function parallelogram_map(width, height, seed) seed = seed, neighbours = function(hex) return table.filter(hex_neighbours(hex), function(_hex) - return map_get(map, _hex) + return hex_map_get(map, _hex) end) end }}) end -- Returns Unordered Triangular (Equilateral) Map of |size| with Simplex Noise -function triangular_map(size, seed) +function hex_triangular_map(size, seed) local seed = seed or math.random(size * math.cos(size) / 2) local map = {} @@ -330,14 +343,14 @@ function triangular_map(size, seed) seed = seed, neighbours = function(hex) return table.filter(hex_neighbours(hex), function(_hex) - return map_get(map, _hex) + return hex_map_get(map, _hex) end) end }}) end -- Returns Unordered Hexagonal Map of |radius| with Simplex Noise -function hexagonal_map(radius, seed) +function hex_hexagonal_map(radius, seed) local seed = seed or math.random(radius * 2 * math.pi) local map = {} @@ -369,7 +382,7 @@ function hexagonal_map(radius, seed) seed = seed, neighbours = function(hex) return table.filter(hex_neighbours(hex), function(_hex) - return map_get(map, _hex.x, _hex.y) + return hex_map_get(map, _hex.x, _hex.y) end) end }}) @@ -377,37 +390,43 @@ end -- Returns Unordered Rectangular Map of |width| and |height| with Simplex Noise -- @TODO - this doesn't work for pointy orientations -function rectangular_map(width, height, seed) +function hex_rectangular_map(width, height, orientation, seed) + local orientation = orientation or HEX_DEFAULT_ORIENTATION local seed = seed or math.random(width * height) local map = {} - for i = 0, width - 1 do - map[i] = {} - for j = 0, height - 1 do - - -- Begin to Calculate Noise - local idelta = i / width - local jdelta = j / height - local noise = 0 - - for oct = 1, 6 do - local f = 2/3^oct - local l = 2^oct - local pos = vec2(idelta + seed * width, jdelta + seed * height) - noise = noise + f * math.simplex(pos * l) + if orientation == HEX_ORIENTATION.FLAT then + for i = 0, width - 1 do + map[i] = {} + for j = 0, height - 1 do + + -- begin to calculate noise + local idelta = i / width + local jdelta = j / height + local noise = 0 + + for oct = 1, 6 do + local f = 2/3^oct + local l = 2^oct + local pos = vec2(idelta + seed * width, jdelta + seed * height) + noise = noise + f * math.simplex(pos * l) + end + j = j - math.floor(i/2) -- this is what makes it rectangular + + map[i][j] = noise end - j = j - math.floor(i/2) -- this is what makes it rectangular - - map[i][j] = noise end + elseif orientation == HEX_ORIENTATION.POINTY then + error("don't use this, it's broken") end + return setmetatable(map, { __index = { width = width, height = height, seed = seed, neighbours = function(hex) return table.filter(hex_neighbours(hex), function(_hex) - return map_get(map, _hex) + return hex_map_get(map, _hex) end) end }}) @@ -415,25 +434,30 @@ end --============================================================================ -- PATHFINDING - - -function breadth_first(map, start) +-- note: +-- i kinda feel like after implementing these and making the game, there are tons of reasons +-- why you might want to specialize pathfinding, like you would any other kind of algorithm +-- +-- so, while (in theory) these algorithms work with the maps in this file, your maps and game +-- will have lots of other data which you may want your pathfinding algorithms to care about in some way, +-- that these don't. +-- +function hex_breadth_first(map, start) local frontier = {} frontier[1] = start local distance = {} - distance[start.x] = {} - distance[start.x][start.y] = 0 + hex_map_set(distance, start, 0) while not (#frontier == 0) do local current = table.remove(frontier, 1) for _,neighbour in pairs(map.neighbours(current)) do - local d = map_get(distance, neighbour.x, neighbour.y) + local d = hex_map_get(distance, neighbour.x, neighbour.y) if not d then table.insert(frontier, neighbour) - local current_distance = map_get(distance, current.x, current.y) - map_set(distance, neighbour.x, neighbour.y, current_distance + 1) + local current_distance = hex_map_get(distance, current.x, current.y) + hex_map_set(distance, neighbour.x, neighbour.y, current_distance + 1) end end end @@ -441,17 +465,15 @@ function breadth_first(map, start) return distance end -function dijkstra(map, start, goal, cost_f, neighbour_f) +function hex_dijkstra(map, start, goal, cost_f, neighbour_f) local frontier = {} frontier[1] = { hex = start, priority = 0 } local came_from = {} - came_from[start.x] = {} - came_from[start.x][start.y] = false + hex_map_set(came_from, start, false) local cost_so_far = {} - cost_so_far[start.x] = {} - cost_so_far[start.x][start.y] = 0 + hex_map_set(cost_so_far, start, 0) while not (#frontier == 0) do local current = table.remove(frontier, 1) @@ -461,14 +483,14 @@ function dijkstra(map, start, goal, cost_f, neighbour_f) end for _,neighbour in pairs(neighbour_f(map, current.hex)) do - local new_cost = map_get(cost_so_far, current.hex) + cost_f(map, current.hex, neighbour) - local neighbour_cost = map_get(cost_so_far, neighbour) + local new_cost = hex_map_get(cost_so_far, current.hex) + cost_f(map, current.hex, neighbour) + local neighbour_cost = hex_map_get(cost_so_far, neighbour) if not neighbour_cost or (new_cost < neighbour_cost) then - map_set(cost_so_far, neighbour, new_cost) + hex_map_set(cost_so_far, neighbour, new_cost) local priority = new_cost + math.distance(start, neighbour) table.insert(frontier, { hex = neighbour, priority = priority }) - map_set(came_from, neighbour, current) + hex_map_set(came_from, neighbour, current) end end end @@ -476,7 +498,7 @@ function dijkstra(map, start, goal, cost_f, neighbour_f) return came_from end --- generic A* pathfinding +-- A* pathfinding -- -- |heuristic| has the form: -- function(source, target) -- source and target are vec2's @@ -486,20 +508,15 @@ end -- function (from, to) -- from and to are vec2's -- return some numeric value -- --- returns a map that has map[hex.x][hex.y] = { hex = vec2, priority = number }, --- where the hex is the spot it thinks you should go to from the indexed hex, and priority is the cost of that decision, --- as well as 'made_it' a bool that tells you if we were successful in reaching |goal| -function Astar(map, start, goal, heuristic, cost_f) +function hex_Astar(map, start, goal, heuristic, cost_f) local path = {} - path[start.x] = {} - path[start.x][start.y] = false + hex_map_set(path, start, false) local frontier = {} frontier[1] = { hex = start, priority = 0 } local path_so_far = {} - path_so_far[start.x] = {} - path_so_far[start.x][start.y] = 0 + hex_map_set(path_so_far, start, 0) local made_it = false while not (#frontier == 0) do @@ -511,14 +528,14 @@ function Astar(map, start, goal, heuristic, cost_f) end for _,next_ in pairs(map.neighbours(current.hex)) do - local new_cost = map_get(path_so_far, current.hex.x, current.hex.y) + cost_f(map, current.hex, next_) - local next_cost = map_get(path_so_far, next_.x, next_.y) + local new_cost = hex_map_get(path_so_far, current.hex.x, current.hex.y) + cost_f(map, current.hex, next_) + local next_cost = hex_map_get(path_so_far, next_.x, next_.y) if not next_cost or new_cost < next_cost then - map_set(path_so_far, next_.x, next_.y, new_cost) + hex_map_set(path_so_far, next_.x, next_.y, new_cost) local priority = new_cost + heuristic(goal, next_) table.insert(frontier, { hex = next_, priority = priority }) - map_set(path, next_.x, next_.y, current) + hex_map_set(path, next_.x, next_.y, current) end end end diff --git a/src/mob.lua b/src/mob.lua index b0aa007..986cf5f 100644 --- a/src/mob.lua +++ b/src/mob.lua @@ -7,7 +7,7 @@ MOB_TYPE = { SPOODER = 2 } -MAX_MOB_SIZE = hex_height(HEX_SIZE, ORIENTATION.FLAT) / 2 +MAX_MOB_SIZE = hex_height(HEX_SIZE, HEX_ORIENTATION.FLAT) / 2 MOB_SIZE = MAX_MOB_SIZE MOB_SPECS = { @@ -63,7 +63,7 @@ end -- check if a the tile at |hex| is passable by |mob| function mob_can_pass_through(mob, hex) - local tile = map_get(state.map, hex) + local tile = hex_map_get(state.map, hex) return tile_is_medium_elevation(tile) end @@ -158,7 +158,7 @@ local function resolve_frame_target_for_mob(mob, mob_index) local frame_target, tile = false, false if mob.path then -- we (should) have an explicitly stored target - local path_entry = map_get(mob.path, mob.hex) + local path_entry = hex_map_get(mob.path, mob.hex) if not path_entry then -- we should be just about to reach the target, delete the path. @@ -180,12 +180,12 @@ local function resolve_frame_target_for_mob(mob, mob_index) if #neighbours > 0 then local first_neighbour = neighbours[1] - tile = map_get(state.map, first_neighbour) + tile = hex_map_get(state.map, first_neighbour) local lowest_cost_hex = first_neighbour local lowest_cost = tile.priority or 0 for _,n in pairs(neighbours) do - tile = map_get(state.map, n) + tile = hex_map_get(state.map, n) if not tile.priority then -- if there's no stored priority, that should mean it's the center tile @@ -247,8 +247,8 @@ local function update_mob_beeper(mob, mob_index) -- or between when we last calculated this target and now -- check for that now if mob_can_pass_through(mob, mob.frame_target) then - local from = map_get(state.map, mob.hex) - local to = map_get(state.map, mob.frame_target) + local from = hex_map_get(state.map, mob.hex) + local to = hex_map_get(state.map, mob.frame_target) local rate = (4 * mob.speed - math.abs(to.elevation - from.elevation)) * am.delta_time mob.position = mob.position + math.normalize(hex_to_pixel(mob.frame_target, vec2(HEX_SIZE)) - mob.position) * rate diff --git a/src/projectile.lua b/src/projectile.lua index b716b95..0b01dc1 100644 --- a/src/projectile.lua +++ b/src/projectile.lua @@ -85,7 +85,7 @@ local function update_projectile_shell(projectile, projectile_index) -- right now, it's just the hex we're on and all of its neighbours. -- this is done to avoid having to check every mob on screen, though maybe it's not necessary. local do_explode = false - local search_hexes = spiral_map(projectile.hex, 1) + local search_hexes = hex_spiral_map(projectile.hex, 1) local mobs = {} for _,hex in pairs(search_hexes) do for index,mob in pairs(mobs_on_hex(hex)) do @@ -103,7 +103,7 @@ local function update_projectile_shell(projectile, projectile_index) end end - local tile = map_get(state.map, projectile.hex) + local tile = hex_map_get(state.map, projectile.hex) if tile and tile.elevation >= projectile.props.z then --do_explode = true @@ -143,7 +143,7 @@ local function update_projectile_laser(projectile, projectile_index) -- get a list of hexes that could have something we could hit on them -- right now, it's just the hex we're on and all of its neighbours. -- this is done to avoid having to check every mob on screen, though maybe it's not necessary. - local search_hexes = spiral_map(projectile.hex, 1) + local search_hexes = hex_spiral_map(projectile.hex, 1) local hit_mob_count = 0 local hit_mobs = {} for _,hex in pairs(search_hexes) do diff --git a/src/tower.lua b/src/tower.lua index 506c662..f55247c 100644 --- a/src/tower.lua +++ b/src/tower.lua @@ -131,9 +131,9 @@ function make_tower_node(tower_type) elseif tower_type == TOWER_TYPE.HOWITZER then return am.group{ - am.circle(vec2(0), HEX_SIZE, COLORS.VERY_DARK_GRAY, 6), + am.circle(vec2(0), HEX_SIZE, COLORS.VERY_DARK_GRAY{a=0.8}, 6), am.rotate(state.time or 0) ^ am.group{ - pack_texture_into_sprite(TEXTURES.CANNON1, HEX_PIXEL_HEIGHT, HEX_PIXEL_WIDTH*2) -- CHONK + pack_texture_into_sprite(TEXTURES.CANNON1, HEX_PIXEL_HEIGHT*2, HEX_PIXEL_WIDTH*3) -- CHONK } } elseif tower_type == TOWER_TYPE.LIGHTHOUSE then @@ -276,11 +276,11 @@ function tower_type_is_buildable_on(hex, tile, tower_type) local has_mountain = false local has_ground = false - for _,h in pairs(spiral_map(hex, get_tower_size(tower_type))) do + for _,h in pairs(hex_spiral_map(hex, get_tower_size(tower_type))) do table.merge(blocking_towers, towers_on_hex(h)) table.merge(blocking_mobs, mobs_on_hex(h)) - local tile = map_get(state.map, h) + local tile = hex_map_get(state.map, h) -- this should always be true, unless it is possible to place a tower -- where part of the tower overflows the edge of the map if tile then @@ -332,7 +332,7 @@ function tower_type_is_buildable_on(hex, tile, tower_type) elseif tower_type == TOWER_TYPE.LIGHTHOUSE then local has_water_neighbour = false for _,h in pairs(hex_neighbours(hex)) do - local tile = map_get(state.map, h) + local tile = hex_map_get(state.map, h) if tile and tile.elevation < -0.5 then has_water_neighbour = true @@ -442,7 +442,7 @@ function update_tower_lighthouse(tower, tower_index) -- is within some angle range...? if the mob is heading directly away from the tower, then -- the lighthouse shouldn't do much - local path, made_it = Astar(state.map, tower.hex, m.hex, grid_heuristic, grid_cost) + local path, made_it = hex_Astar(state.map, tower.hex, m.hex, grid_heuristic, grid_cost) if made_it then m.path = path @@ -485,17 +485,20 @@ function make_and_register_tower(hex, tower_type) if tower.size == 0 then tower.hexes = { tower.hex } else - tower.hexes = spiral_map(tower.hex, tower.size) + tower.hexes = hex_spiral_map(tower.hex, tower.size) end tower.height = spec.height for _,h in pairs(tower.hexes) do - local tile = map_get(state.map, h.x, h.y) + local tile = hex_map_get(state.map, h.x, h.y) tile.elevation = tile.elevation + tower.height end if tower.type == TOWER_TYPE.HOWITZER then tower.props.z = tower.height + + elseif tower.type == TOWER_TYPE.LIGHTHOUSE then + tower.perimeter = hex_ring_map(tower.hex, tower.range) end register_entity(state.towers, tower) diff --git a/texture.lua b/texture.lua index f3bae30..c22c8dd 100644 --- a/texture.lua +++ b/texture.lua @@ -13,6 +13,7 @@ end TEXTURES = { LOGO = load_texture("res/logo.png"), GEM1 = load_texture("res/gem1.png"), + SHADED_HEX = load_texture("res/shaded_hex1.png"), -- gui stuff BUTTON1 = load_texture("res/button1.png"),