diff --git a/src/colors.lua b/src/colors.lua new file mode 100644 index 0000000..1653e88 --- /dev/null +++ b/src/colors.lua @@ -0,0 +1,15 @@ + +COLORS = { + TRANSPARENT = vec4(0.4), + --TRANSPARENT = vec4(0.6), + + WHITE = vec4(0.8, 0.8, 0.7, 1), + BLACK = vec4(0, 0, 0, 1), + + -- hues + BLUE_STONE = vec4(0.12, 0.3, 0.3, 1), + MYRTLE = vec4(0.10, 0.25, 0.10, 1), + BROWN_POD = vec4(0.25, 0.20, 0.10, 1), + BOTTLE_GREEN = vec4(0.15, 0.30, 0.20, 1) +} + diff --git a/src/grid.lua b/src/grid.lua new file mode 100644 index 0000000..1c11d30 --- /dev/null +++ b/src/grid.lua @@ -0,0 +1,78 @@ + +require "colors" + +local WORLD_GRID_DIMENSIONS = vec2(46, 32) +local world_grid_map + +-- ensure home-base is somewhat of an open area. +function find_home(preferred_radius) + home = spiral_map(vec2(23, 4), preferred_radius or 2) + local home_node = am.group() + + repeat + local happy = true + + for i,h in pairs(home) do + local elevation = map[h.x][h.y] + + if not elevation then -- hex not in map + elseif elevation > 0.5 or elevation < -0.5 then + happy = false + + elseif not happy then + home = spiral_map(h, preferred_radius or 1) + home_node = am.group() + + else + local center = hex_to_pixel(h) + local color = vec4(1, 0, 0.5, 1) + local node = am.circle(center, 4, color, 4) + home_node:append(node) + end + end + until happy + return home_node +end + +-- map elevation to appropriate tile color. +function color_at(elevation) + if elevation < -0.5 then -- lowest elevation : impassable + return COLORS.BLUE_STONE{ a = (elevation + 1.4) / 2 + 0.2 } + + elseif elevation < 0 then -- med-low elevation : passable + return COLORS.MYRTLE{ a = (elevation + 1.8) / 2 + 0.2 } + + elseif elevation < 0.5 then -- med-high elevation : passable + return COLORS.BROWN_POD{ a = (elevation + 1.6) / 2 + 0.2 } + + elseif elevation < 1 then -- highest elevation : impassable + return COLORS.BOTTLE_GREEN{ a = (elevation + 1.0) / 2 + 0.2 } + end +end + +function random_map(seed) + world_grid_map = rectangular_map(WORLD_GRID_DIMENSIONS.x, WORLD_GRID_DIMENSIONS.y, seed); + math.randomseed(world_grid_map.seed) + local world = am.translate(vec2(win.left + 200, win.bottom -60)) ^ am.group():tag"world" + + for i,_ in pairs(world_grid_map) do + for j,elevation in pairs(world_grid_map[i]) do + + -- subtly shade map edges + local off = hex_to_offset(vec2(i, j)) + local mask = vec4(0, 0, 0, math.max(((off.x - WORLD_GRID_DIMENSIONS.x/2) / WORLD_GRID_DIMENSIONS.x) ^ 2, + ((-off.y - WORLD_GRID_DIMENSIONS.y/2) / WORLD_GRID_DIMENSIONS.y) ^ 2)) + local color = color_at(elevation) - mask + + local node = am.circle(hex_to_pixel(vec2(i, j)), get_default_hex_size(), vec4(0), 6) + :action(am.tween(2, { color=color }, am.ease.out(am.ease.hyperbola))) + + world:append(node) + end + end + --world:append(find_home(2)) + --world:action(spawner) + return world:tag"world" +end + + diff --git a/src/gui.lua b/src/gui.lua new file mode 100644 index 0000000..ff62ef2 --- /dev/null +++ b/src/gui.lua @@ -0,0 +1,9 @@ + +local hot +local active + +function button(x, y) + local color = (x + y) % 2 == 0 and vec4(0.4, 0.4, 0.5, 1) or vec4(0.5, 0.4, 0.4, 1) + return am.translate(x * 80, y * 80) ^ am.rect(-40, 40, 40, -40, color) +end + diff --git a/src/hexyz.lua b/src/hexyz.lua index 0d8d174..cf754b5 100644 --- a/src/hexyz.lua +++ b/src/hexyz.lua @@ -1,267 +1,290 @@ --- Rounds Numbers. -local function round(n) return n % 1 >= 0.5 and math.ceil(n) or math.floor(n) end --[[============================================================================ -- HEX CONSTANTS AND UTILITY FUNCTIONS ]] +-- 'size' here is distance from the centerpoint to any vertex in pixel +local DEFAULT_HEX_SIZE = 20 + +-- wherever orientation appears as an argument, if it isn't provided, use this +local orientation = FLAT + +function get_default_hex_size() + return DEFAULT_HEX_SIZE +end + +function hex_width(size, orientation) + if orientation == FLAT then + return size * 2 + + elseif orientation == POINTY then + return math.sqrt(3) * size + end +end + +function hex_height(size, orientation) + return hex_width(size, orientation == FLAT and POINTY or FLAT) +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) + return vec2(hex_width(size, orientation), hex_height(size, orientation)) +end + -- All Non-Diagonal Vector Directions from a Given Hex by Edge -HEX_DIRECTIONS = {vec2( 1 , -1), vec2( 1 , 0), vec2(0 , 1), - vec2(-1 , 1), vec2(-1 , 0), vec2(0 , -1)} +HEX_DIRECTIONS = { vec2( 1 , -1), vec2( 1 , 0), vec2(0 , 1), + vec2(-1 , 1), vec2(-1 , 0), vec2(0 , -1) } -- Return Hex Vector Direction via Integer Index |direction| function hex_direction(direction) return HEX_DIRECTIONS[(direction % 6) % 6 + 1] end - -- Return Hexagon Adjacent to |hex| in Integer Index |direction| function hex_neighbour(hex, direction) return hex + HEX_DIRECTIONS[(direction % 6) % 6 + 1] end - -- Collect All 6 Neighbours in a Table function hex_neighbours(hex) - local neighbours = {} - for i = 1, 6 do - table.insert(neighbours, hex_neighbour(hex, i)) - end - return neighbours + local neighbours = {} + for i = 1, 6 do + table.insert(neighbours, hex_neighbour(hex, i)) + end + return neighbours end - -- Returns a vec2 Which is the Nearest |x, y| to Float Trio |x, y, z| local function hex_round(x, y, z) - local rx = round(x) - local ry = round(y) - local rz = round(z) or round(-x - y) - - local xdelta = math.abs(rx - x) - local ydelta = math.abs(ry - y) - local zdelta = math.abs(rz - z or round(-x - y)) - - if xdelta > ydelta and xdelta > zdelta then - rx = -ry - rz - elseif ydelta > zdelta then - ry = -rx - rz - else - rz = -rx - ry end - return vec2(rx, ry) + local function round(n) return math.floor(n + 0.5) end + + local rx = round(x) + local ry = round(y) + local rz = round(z) or round(-x - y) + + local xdelta = math.abs(rx - x) + local ydelta = math.abs(ry - y) + local zdelta = math.abs(rz - z or round(-x - y)) + + if xdelta > ydelta and xdelta > zdelta then + rx = -ry - rz + elseif ydelta > zdelta then + ry = -rx - rz + else + rz = -rx - ry + end + + return vec2(rx, ry) end --[[==========================================================================-- - -- ORIENTATION & LAYOUT +-- ORIENTATION & LAYOUT ]] -- Forward & Inverse Matrices used for the Flat Orientation -local FLAT = {M = mat2(3.0/2.0, 0.0, 3.0^0.5/2.0, 3.0^0.5 ), - W = mat2(2.0/3.0, 0.0, -1.0/3.0 , 3.0^0.5/3.0), - angle = 0.0} +local FLAT = { + M = mat2(3.0/2.0, 0.0, 3.0^0.5/2.0, 3.0^0.5 ), + W = mat2(2.0/3.0, 0.0, -1.0/3.0 , 3.0^0.5/3.0), + angle = 0.0 +} -- Forward & Inverse Matrices used for the Pointy Orientation -local POINTY = {M = mat2(3.0^0.5, 3.0^0.5/2.0, 0.0, 3.0/2.0), - W = mat2(3.0^0.5/3.0, -1.0/3.0, 0.0, 2.0/3.0), - angle = 0.5} +local POINTY = { + M = mat2(3.0^0.5, 3.0^0.5/2.0, 0.0, 3.0/2.0), + W = mat2(3.0^0.5/3.0, -1.0/3.0, 0.0, 2.0/3.0), + angle = 0.5 +} -- Hex to Screen -- Orientation Must be Either POINTY or FLAT -function hex_to_pixel(hex, size, orientation_M) - local M = orientation_M or FLAT.M +function hex_to_pixel(hex, size, orientation) + local M = orientation.M - local x = (M[1][1] * hex[1] + M[1][2] * hex[2]) * (size and size[1] or 11) - local y = (M[2][1] * hex[1] + M[2][2] * hex[2]) * (size and size[2] or 11) + local x = (M[1][1] * hex[1] + M[1][2] * hex[2]) * (size and size[1] or DEFAULT_HEX_SIZE) + local y = (M[2][1] * hex[1] + M[2][2] * hex[2]) * (size and size[2] or DEFAULT_HEX_SIZE) - return vec2(x, y) + return vec2(x, y) end - -- Screen to Hex -- Orientation Must be Either POINTY or FLAT -function pixel_to_hex(pix, size, orientation_W) - local W = orientation_W or FLAT.W +function pixel_to_hex(pix, size, orientation) + local W = orientation.W - local pix = pix / (size or vec2(11)) + local pix = pix / (size or vec2(DEFAULT_HEX_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] + local x = W[1][1] * pix[1] + W[1][2] * pix[2] + local y = W[2][1] * pix[1] + W[2][2] * pix[2] - return hex_round(x, y, -x - y) + return hex_round(x, y, -x - y) end - -- TODO test, learn am.draw -function hex_corner_offset(corner, size, orientation_angle) - local angle = 2.0 * math.pi * orientation_angle or FLAT.angle + corner / 6 - return vec2(size[1] * math.cos(angle), size[2] * math.sin(angle)) +function hex_corner_offset(corner, size, 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 corners = {} - local center = hex_to_pixel(hex, size, orientation) - for i = 0, 5 do - local offset = hex_corner_offset(i, size, orientation) - table.insert(corners, center + offset) - end - return corners + local corners = {} + local center = hex_to_pixel(hex, size, orientation) + for i = 0, 5 do + local offset = hex_corner_offset(i, size, orientation) + table.insert(corners, center + offset) + end + return corners end - --- Offset Coordinates Look Nice / are Useful for UI-Implementations function hex_to_offset(hex) - return vec2(hex[1], -hex[1] - hex[2] + (hex[1] + (hex[1] % 2)) / 2) end + return vec2(hex[1], -hex[1] - hex[2] + (hex[1] + (hex[1] % 2)) / 2) end - --- Back to Cube Coordinates function offset_to_hex(off) - return vec2(off[1], off[2] - math.floor((off[1] - 1 * (off[1] % 2))) / 2) end + return vec2(off[1], off[2] - math.floor((off[1] - 1 * (off[1] % 2))) / 2) end --[[============================================================================ - -- MAPS & STORAGE +-- MAPS & STORAGE ]] -- Returns Ordered Ring-Shaped Map of |radius| from |center| function ring_map(center, radius) - local map = {} + local map = {} - local walk = center + HEX_DIRECTIONS[6] * radius + local walk = center + HEX_DIRECTIONS[6] * radius - for i = 1, 6 do - for j = 1, radius do - table.insert(map, walk) - walk = hex_neighbour(walk, i) - end - end - return setmetatable(map, {__index={center=center, radius=radius}}) + for i = 1, 6 do + for j = 1, radius do + table.insert(map, walk) + walk = hex_neighbour(walk, i) + end + end + return setmetatable(map, {__index={center=center, radius=radius}}) end -- Returns Ordered Spiral Hexagonal Map of |radius| Rings from |center| function spiral_map(center, radius) - local map = {center} + local map = {center} - for i = 1, radius do - table.append(map, ring_map(center, i)) - end - return setmetatable(map, {__index={center=center, radius=radius}}) + for i = 1, radius do + table.append(map, ring_map(center, i)) + end + return setmetatable(map, {__index={center=center, radius=radius}}) end - -- Returns Unordered Parallelogram-Shaped Map of |width| and |height| with Simplex Noise function parallelogram_map(width, height, seed) - local seed = seed or math.random(width * height) - - local map = {} - for i = 0, width do - map[i] = {} - for j = 0, height do - - -- Calculate Noise - local idelta = i / width - local jdelta = j / height - local noise = 0 - - for oct = 1, 6 do - local f = 1/4^oct - local l = 2^oct - local pos = vec2(idelta + seed * width, jdelta + seed * height) - noise = noise + f * math.simplex(pos * l) - end - map[i][j] = noise - end - end - return setmetatable(map, {__index={width=width, height=height, seed=seed}}) + local seed = seed or math.random(width * height) + + local map = {} + for i = 0, width do + map[i] = {} + for j = 0, height do + + -- Calculate Noise + local idelta = i / width + local jdelta = j / height + local noise = 0 + + for oct = 1, 6 do + local f = 1/4^oct + local l = 2^oct + local pos = vec2(idelta + seed * width, jdelta + seed * height) + noise = noise + f * math.simplex(pos * l) + end + map[i][j] = noise + end + end + return setmetatable(map, {__index={width=width, height=height, seed=seed}}) end -- Returns Unordered Triangular (Equilateral) Map of |size| with Simplex Noise function triangular_map(size, seed) - local seed = seed or math.random(size * math.cos(size) / 2) - - local map = {} - for i = 0, size do - map[i] = {} - for j = size - i, size do - - -- Generate Noise - local idelta = i / size - local jdelta = j / size - local noise = 0 - - for oct = 1, 6 do - local f = 1/3^oct - local l = 2^oct - local pos = vec2(idelta + seed * size, jdelta + seed * size) - noise = noise + f * math.simplex(pos * l) - end - map[i][j] = noise - end - end - return setmetatable(map, {__index={size=size, seed=seed}}) + local seed = seed or math.random(size * math.cos(size) / 2) + + local map = {} + for i = 0, size do + map[i] = {} + for j = size - i, size do + + -- Generate Noise + local idelta = i / size + local jdelta = j / size + local noise = 0 + + for oct = 1, 6 do + local f = 1/3^oct + local l = 2^oct + local pos = vec2(idelta + seed * size, jdelta + seed * size) + noise = noise + f * math.simplex(pos * l) + end + map[i][j] = noise + end + end + return setmetatable(map, {__index={size=size, seed=seed}}) end -- Returns Unordered Hexagonal Map of |radius| with Simplex Noise function hexagonal_map(radius, seed) - local seed = seed or math.random(radius * 2 * math.pi) + local seed = seed or math.random(radius * 2 * math.pi) - local map = {} - for i = -radius, radius do - map[i] = {} + local map = {} + for i = -radius, radius do + map[i] = {} - local j1 = math.max(-radius, -i - radius) - local j2 = math.min(radius, -i + radius) + local j1 = math.max(-radius, -i - radius) + local j2 = math.min(radius, -i + radius) - for j = j1, j2 do + for j = j1, j2 do - -- Calculate Noise - local idelta = i / radius - local jdelta = j / radius - local noise = 0 + -- Calculate Noise + local idelta = i / radius + local jdelta = j / radius + local noise = 0 - for oct = 1, 6 do - local f = 2/3^oct - local l = 2^oct - local pos = vec2(idelta + seed * radius, jdelta + seed * radius) + for oct = 1, 6 do + local f = 2/3^oct + local l = 2^oct + local pos = vec2(idelta + seed * radius, jdelta + seed * radius) - noise = noise + f * math.simplex(pos * l) - end - map[i][j] = noise - end - end - return setmetatable(map, {__index={radius=radius, seed=seed}}) + noise = noise + f * math.simplex(pos * l) + end + map[i][j] = noise + end + end + return setmetatable(map, {__index={radius=radius, seed=seed}}) end - -- Returns Unordered Rectangular Map of |width| and |height| with Simplex Noise function rectangular_map(width, height, seed) - local seed = seed or math.random(width * height) - - local map = {} - for i = 0, width do - map[i] = {} - for j = 0, height 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 - - -- store two dimensions as a single number - map[i][j] = noise - end - end - return setmetatable(map, {__index={width=width, height=height, seed=seed}}) + local seed = seed or math.random(width * height) + + local map = {} + for i = 0, width do + map[i] = {} + for j = 0, height 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 + + -- store two dimensions as a single number + map[i][j] = noise + end + end + return setmetatable(map, {__index={width=width, height=height, seed=seed}}) end --[[==========================================================================-- - ----- PATHFINDING ----- +----- PATHFINDING ----- ============================================================================]]-- -- big ol' TODO diff --git a/src/main.lua b/src/main.lua index 04f0373..a8475e8 100644 --- a/src/main.lua +++ b/src/main.lua @@ -1,293 +1,126 @@ -require"hexyz" - math.randomseed(os.time()); math.random(); math.random(); math.random() ---[[============================================================================ +--[[=I==========================================================================]] +-- Imports +require "hexyz" +require "grid" -]] --- -win = am.window -{ -- Base Resolution = 3/4 * WXGA standard 16:10 -- 960px, 600px - width = 1280 * 3/4, height = 800 * 3/4, - clear_color = vec4(0.08, 0.08, 0.11, 1) -} +--[[============================================================================]] +-- Globals +win = am.window{ + width = 1920, + height = 1080, + resizable = false +} +--[[============================================================================]] +-- Local 'Globals' -local map local home -local spawn_chance = 25 - ---[[============================================================================ - - -]] --- ensure home-base is somewhat of an open area. -function find_home(preferred_radius) - home = spiral_map(vec2(23, 4), preferred_radius or 2) - local home_node = am.group() - - repeat - local happy = true - - for i,h in pairs(home) do - local elevation = map[h.x][h.y] - - if not elevation then -- hex not in map - elseif elevation > 0.5 or elevation < -0.5 then - happy = false - - elseif not happy then - home = spiral_map(h, preferred_radius or 1) - home_node = am.group() - - else - local center = hex_to_pixel(h) - local color = vec4(1, 0, 0.5, 1) - local node = am.circle(center, 4, color, 4) - home_node:append(node) - end - end - until happy - return home_node -end - - --- map elevation to appropriate tile color. -function color_at(elevation) - if elevation < -0.5 then -- lowest elevation : impassable - return vec4(0.10, 0.30, 0.40, (elevation + 1.4) / 2 + 0.2) - - elseif elevation < 0 then -- med-low elevation : passable - return vec4(0.10, 0.25, 0.10, (elevation + 1.8) / 2 + 0.2) - - elseif elevation < 0.5 then -- med-high elevation : passable - return vec4(0.25, 0.20, 0.10, (elevation + 1.6) / 2 + 0.2) - - elseif elevation < 1 then -- highest elevation : impassable - return vec4(0.15, 0.30, 0.20, (elevation + 1.0) / 2 + 0.2) - end -end - - --- -function random_map(seed) - map = rectangular_map(46, 33, seed); math.randomseed(map.seed) - local world = am.translate(vec2(-278, -318)) ^ am.group():tag"world" - - for i,_ in pairs(map) do - for j,elevation in pairs(map[i]) do - - -- subtly shade map edges - local off = hex_to_offset(vec2(i, j)) - local mask = vec4(0, 0, 0, math.max(((off.x - 23.5) / 46) ^ 2, - ((-off.y - 16.5) / 32) ^ 2)) - local color = color_at(elevation) - mask - - local node = am.circle(hex_to_pixel(vec2(i, j)), 11, vec4(0), 6) - :action(am.tween(2, {color=color}, am.ease.out(am.ease.hyperbola))) - world:append(node) - end - end - world:append(find_home(2)) - world:action(spawner) - return world:tag"world" -end - - --- determines when, where, and how often to spawn mobs. -function spawner(world) - if math.random(spawn_chance) == 1 then -- chance to spawn - local spawn_position - repeat - -- ensure we spawn on an random tile along the map's edges - local x,y = math.random(46), math.random(33) - if math.random() < 0.5 then - x = math.random(0, 1) * 46 - else - y = math.random(0, 1) * 33 - end - spawn_position = offset_to_hex(vec2(x, y)) - - -- ensure that we spawn somewhere that is passable: mid-elevation - local e = map[spawn_position.x][spawn_position.y] - until e and e < 0.5 and e > -0.5 - - local mob = am.translate(-278, -318) ^ am.circle(hex_to_pixel(spawn_position), 4) - world:append(mob"circle":action(coroutine.create(live))) - end -end - --- this function is the coroutine that represents the life-cycle of a mob. -function live(mob) - local dead = false - local visited = {} - visited[mob.center.x] = {}; visited[mob.center.x][mob.center.y] = true - - -- begin life - repeat - local neighbours = hex_neighbours(pixel_to_hex(mob.center)) - local candidates = {} - - -- get list of candidates: hex positions to consider moving to. - for _,h in pairs(neighbours) do - - local e - if map[h.x] then - e = map[h.x][h.y] - end - - if e and e < 0.5 and e > -0.5 then - if visited[h.x] then - if not visited[h.x][h.y] then - table.insert(candidates, h) - end - else - table.insert(candidates, h) - end - end - end - - -- choose where to move. manhattan distance closest to goal is chosen. - local move = candidates[1] - for _,h in pairs(candidates) do - if math.distance(h, home.center) < math.distance(move, home.center) then - move = h - end - end - - if not move then print("can't find anywhere to move to"); return - end -- bug - - local speed = map[move.x][move.y] ^ 2 + 0.5 - am.wait(am.tween(mob, speed, {center=hex_to_pixel(move)})) - visited[move.x] = {}; visited[move.x][move.y] = true - if move == home.center then dead = true end - until dead - win.scene:remove(mob) -end - - --- POLL MOUSE function poll_mouse() - if win:mouse_position().x > -268 then -- mouse is inside game map - -- get info about mouse position - local hex = pixel_to_hex(win:mouse_position() - vec2(-278, -318)) - local off = hex_to_offset(hex) + if win:mouse_position().x > -268 then -- mouse is inside game map + + local hex = pixel_to_hex(win:mouse_position() - vec2(-278, -318)) + local off = hex_to_offset(hex) - -- check if cursor location outside of map bounds - if off.x <= 1 or -off.y <= 1 or off.x >= 46 or -off.y >= 32 then - win.scene"coords".text = "" + -- check if cursor location outside of map bounds + if off.x <= 1 or -off.y <= 1 or off.x >= 46 or -off.y >= 32 then + win.scene"coords".text = "" - else - if win:mouse_down"left" then -- check if mouse clicked - if map[hex.x][hex.y] <= -0.5 or map[hex.x][hex.y] >= 0.5 then + else + if win:mouse_down"left" then + if map[hex.x][hex.y] <= -0.5 or map[hex.x][hex.y] >= 0.5 then - else - map[hex.x][hex.y] = 2 - win.scene"world":append(am.circle(hex_to_pixel(hex), 11, vec4(0, 0, 0, 1), 6)) + else + map[hex.x][hex.y] = 2 + win.scene"world":append(am.circle(hex_to_pixel(hex), get_default_hex_size(), COLORS.BLACK, 6)) + end end - end - win.scene"coords".text = string.format("%2d,%2d", off.x, -off.y) - win.scene"selected".center = hex_to_pixel(hex) + vec2(-278, -318) - end - else -- mouse is over background bar, (or outside window!!!!) - if win:key_pressed"escape" then - init() - end - end + win.scene"coords".text = string.format("%2d,%2d", off.x, -off.y) + win.scene"hex_cursor".center = hex_to_pixel(hex) + vec2(-278, -318) + end + else -- mouse is over background bar, (or outside window!) + if win:key_pressed"escape" then + init() + end + end end - --- function update_score() - win.scene"score".text = string.format("SCORE: %.2f", am.current_time()) + win.scene"score".text = string.format("SCORE: %.2f", am.current_time()) end - -function update_mobs() - - +function main_action(main_scene) + update_score() + poll_mouse() end - - --- -function button(x, y) - local color = (x + y) % 2 == 0 and vec4(0.4, 0.4, 0.5, 1) or vec4(0.5, 0.4, 0.4, 1) - return am.translate(x * 80, y * 80) ^ am.rect(-40, 40, 40, -40, color) -end - - --- GAME INITIALIZATION FUNCTION function game_init() - local score = am.translate(-264, 290) ^ am.text("", "left"):tag"score" - local coords = am.translate(440, 290) ^ am.text(""):tag"coords" - local selected = am.circle(vec2(win.left, win.top), 11, vec4(0.4), 6):tag"selected" - local bg = am.rect(win.left, win.top, win.right, win.bottom, vec4(0.12, 0.3, 0.3, 1)):tag"curtain" - - local buttons = am.translate(-500, -300) ^ am.group() - for i = 1, 2 do - for j = 1, 6 do - buttons:append(button(i, j)) - end - end - - local main_scene = am.group{random_map(9), bg, buttons, score, coords, selected} - :action(am.series - { - am.tween(bg, 0.8, {x2 = -268}, am.ease.bounce), -- INTRO TRANSITION - - function(scene) -- MAIN ACTION - update_score() - -- update mobs - -- update towers - -- update environment - poll_mouse() -- check if player did anything - end - }) - win.scene = main_scene + local score = am.translate(-264, win.top - 50) ^ am.text("", "left"):tag"score" + local coords = am.translate(440, win.top - 50) ^ am.text(""):tag"coords" + local hex_cursor = am.circle(vec2(win.left, win.top), get_default_hex_size(), vec4(0.4), 6):tag"hex_cursor" + local curtain = am.rect(win.left, win.top, win.right, win.bottom, COLORS.BLUE_STONE):tag"curtain" + + local main_scene = am.group{ + random_map(), + score, + coords, + curtain, + hex_cursor + } + + main_scene:action(am.series + { + am.tween(curtain, 0.8, { x2 = win.left }, am.ease.bounce), + main_action + }) + + win.scene = main_scene end +function draw_menu() + local map = hexagonal_map(15, 9) + local backdrop = am.group() + + for i,_ in pairs(map) do + for j,e in pairs(map[i]) do + backdrop:append(am.circle(hex_to_pixel(vec2(i, j)), 11, color_at(e), 6)) + end + end + + local title_text = am.group + { + am.translate(0, 200) ^ am.scale(5) ^ am.text("hexyz", COLORS.WHITE, "right"), + am.translate(0, 130) ^ am.scale(4) ^ am.text("a tower defense", COLORS.WHITE, 1), + am.circle(vec2(0), 100, vec4(0.6), 6):tag"button", am.scale(4) ^ am.text("START", COLORS.BLACK) + } + + win.scene = am.group + { + backdrop, + title_text + } + :action(function(self) + local mouse = win:mouse_position() + if math.length(mouse) < 100 then + self"button":action(am.series + { + am.tween(0.1, { color = COLORS.WHITE }), + am.tween(0.1, { color = vec4(0.6) }) + }) + + if win:mouse_pressed"left" then + game_init() + end + end + end) +end --- TITLE SCREEN function init() - local map = hexagonal_map(15, 9) - local backdrop = am.group() - - for i,_ in pairs(map) do - for j,e in pairs(map[i]) do - backdrop:append(am.circle(hex_to_pixel(vec2(i, j)), 11, color_at(e), 6)) - end - end - - local title_text = am.group - { - am.translate(0, 200) ^ am.scale(5) ^ am.text("hexyz", vec4(0.8, 0.8, 0.7, 1), "right"), - am.translate(0, 130) ^ am.scale(4) ^ am.text("a tower defense", vec4(0.8, 0.8, 0.7, 1)), - am.circle(vec2(0), 100, vec4(0.6), 6):tag"b", am.scale(4) ^ am.text("START", vec4(0, 0, 0, 1)) - } - - win.scene = am.group - { - backdrop, - title_text - } - :action(function(s) - local mouse = win:mouse_position() - if math.length(mouse) < 100 then - s"b":action(am.series - { - am.tween(0.1, {color = vec4(0.8, 0.8, 0.7, 1)}), - am.tween(0.1, {color = vec4(0.6)}) - }) - if win:mouse_pressed"left" then - game_init() - end - end - end) + draw_menu() end init() diff --git a/src/mob.lua b/src/mob.lua new file mode 100644 index 0000000..7d89b9b --- /dev/null +++ b/src/mob.lua @@ -0,0 +1,77 @@ + + + +-- determines when, where, and how often to spawn mobs. +function spawner(world) + local SPAWN_CHANCE = 25 + if math.random(SPAWN_CHANCE) == 1 then -- chance to spawn + local spawn_position + repeat + -- ensure we spawn on an random tile along the map's edges + local x,y = math.random(46), math.random(33) + if math.random() < 0.5 then + x = math.random(0, 1) * 46 + else + y = math.random(0, 1) * 33 + end + spawn_position = offset_to_hex(vec2(x, y)) + + -- ensure that we spawn somewhere that is passable: mid-elevation + local e = map[spawn_position.x][spawn_position.y] + until e and e < 0.5 and e > -0.5 + + local mob = am.translate(-278, -318) ^ am.circle(hex_to_pixel(spawn_position), 4) + world:append(mob"circle":action(coroutine.create(live))) + end +end + +-- this function is the coroutine that represents the life-cycle of a mob. +function live(mob) + local dead = false + + local visited = {} + visited[mob.center.x] = {}; visited[mob.center.x][mob.center.y] = true + + -- begin life + repeat + local neighbours = hex_neighbours(pixel_to_hex(mob.center)) + local candidates = {} + + -- get list of candidates: hex positions to consider moving to. + for _,h in pairs(neighbours) do + + local e + if map[h.x] then + e = map[h.x][h.y] + end + + if e and e < 0.5 and e > -0.5 then + if visited[h.x] then + if not visited[h.x][h.y] then + table.insert(candidates, h) + end + else + table.insert(candidates, h) + end + end + end + + -- choose where to move. manhattan distance closest to goal is chosen. + local move = candidates[1] + for _,h in pairs(candidates) do + if math.distance(h, home.center) < math.distance(move, home.center) then + move = h + end + end + + if not move then print("can't find anywhere to move to"); return + end -- bug + + local speed = map[move.x][move.y] ^ 2 + 0.5 + am.wait(am.tween(mob, speed, {center=hex_to_pixel(move)})) + visited[move.x] = {}; visited[move.x][move.y] = true + if move == home.center then dead = true end + until dead + win.scene:remove(mob) +end +