Browse Source

heavy refactor in progress

master
Nicholas Hayashi 4 years ago
parent
commit
d035d0bb08
  1. 15
      src/colors.lua
  2. 78
      src/grid.lua
  3. 9
      src/gui.lua
  4. 79
      src/hexyz.lua
  5. 263
      src/main.lua
  6. 77
      src/mob.lua

15
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)
}

78
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

9
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

79
src/hexyz.lua

@ -1,11 +1,38 @@
-- 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) }
@ -14,12 +41,10 @@ HEX_DIRECTIONS = {vec2( 1 , -1), vec2( 1 , 0), vec2(0 , 1),
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 = {}
@ -29,9 +54,10 @@ function hex_neighbours(hex)
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 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)
@ -45,7 +71,9 @@ local function hex_round(x, y, z)
elseif ydelta > zdelta then
ry = -rx - rz
else
rz = -rx - ry end
rz = -rx - ry
end
return vec2(rx, ry)
end
--[[==========================================================================--
@ -53,31 +81,34 @@ end
]]
-- 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 ),
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}
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),
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}
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)
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]
@ -85,14 +116,12 @@ function pixel_to_hex(pix, size, orientation_W)
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
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 = {}
@ -104,13 +133,9 @@ function hex_corners(hex, size, orientation)
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
-- 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
@ -144,7 +169,6 @@ function spiral_map(center, radius)
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)
@ -230,7 +254,6 @@ function hexagonal_map(radius, seed)
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)

263
src/main.lua

@ -1,179 +1,28 @@
math.randomseed(os.time()); math.random(); math.random(); math.random()
--[[=I==========================================================================]]
-- Imports
require "hexyz"
require "grid"
math.randomseed(os.time()); math.random(); math.random(); math.random()
--[[============================================================================
--[[============================================================================]]
-- Globals
]]
--
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)
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)
@ -182,78 +31,57 @@ function poll_mouse()
win.scene"coords".text = ""
else
if win:mouse_down"left" then -- check if mouse clicked
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))
win.scene"world":append(am.circle(hex_to_pixel(hex), get_default_hex_size(), COLORS.BLACK, 6))
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)
win.scene"hex_cursor".center = hex_to_pixel(hex) + vec2(-278, -318)
end
else -- mouse is over background bar, (or outside window!!!!)
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())
end
function update_mobs()
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)
function main_action(main_scene)
update_score()
poll_mouse()
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 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
}
local main_scene = am.group{random_map(9), bg, buttons, score, coords, selected}
:action(am.series
main_scene: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
am.tween(curtain, 0.8, { x2 = win.left }, am.ease.bounce),
main_action
})
win.scene = main_scene
end
-- TITLE SCREEN
function init()
function draw_menu()
local map = hexagonal_map(15, 9)
local backdrop = am.group()
@ -265,9 +93,9 @@ function init()
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))
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
@ -275,14 +103,15 @@ function init()
backdrop,
title_text
}
:action(function(s)
:action(function(self)
local mouse = win:mouse_position()
if math.length(mouse) < 100 then
s"b":action(am.series
self"button":action(am.series
{
am.tween(0.1, {color = vec4(0.8, 0.8, 0.7, 1)}),
am.tween(0.1, { color = COLORS.WHITE }),
am.tween(0.1, { color = vec4(0.6) })
})
if win:mouse_pressed"left" then
game_init()
end
@ -290,6 +119,10 @@ function init()
end)
end
function init()
draw_menu()
end
init()
noglobals()

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