Browse Source

cleaning...

master
churchianity 6 years ago
parent
commit
9c958164ae
  1. 15
      README.md
  2. 159
      src/hexyz.lua
  3. 317
      src/main.lua
  4. 16
      src/sprites.lua

15
README.md

@ -3,24 +3,21 @@
This is a small and simple library for using hexagonal grids in amulet + lua. I wrote it for a tower defense game I'm making.
It's not really well documented. If you want an actual good resource, go to [amit's guide to hexagonal grids](https://redblobgames.com/grids/hexagons).
So much of what is here I derived from amit's work.
It's not really well documented. If you want an actual good resource, go to [amit's guide to hexagonal grids](https://redblobgames.com/grids/hexagons). So much of what is here I derived from amit's work.
## CONVENTIONS & TERMINOLOGY
If you have read amit's guide to hexagon grid, a lot of the terminology will be familiar to you - I utilize many conventions he does in his guide. That being said,
because so many similar kinds of data structures with different goals are used in this library it can be hard to remember precisely what they all refer to.
The following table shows what each table/vector/array refers to in the code:
If you have read amit's guide to hexagon grids, a lot of the terminology will be familiar to you - I utilize many conventions he does in his guide. That being said, I use some he doesn't, and because so many similar kinds of data structures with different goals are used in this library it can be hard to remember precisely what they all refer to. The following table shows what each table/vector/array refers to in the code:
| NAME | REFERS TO |
| ---- | ------------------------------------------------------------ |
| hex | xyz, *vector* used for most tasks, with constraint x+y+z=0 |
| pix | xy, *vector* true screen pixel coordinates |
| off | xy, 'offset', *vector* used for UI implementations |
| map | xy, *table* of unit hexagon centerpoints arranged in a shape |
| map | *table* of unit hexagon centerpoints arranged in a shape |
* note that 'hex' here is a catch-all term for cube/axial, as they can often be used interchangeably.
* note that 'hex' here is a catch-all term for cube/axial coordinates, as they can often be used interchangeably.
## MAPS & MAP STORAGE
@ -36,7 +33,7 @@ The storage system used is based on the map shape - see chart:
| hexagonal | unordered, hash-like | vec2(i, j) | simplex noise |
| triangular | unordered, hash-like | vec2(i, j) | simplex noise |
* note that a spiral map is just a hexagonal one with a particular order.
* note that a spiral map is just a hexagonal one with a particular order.
The noise values on the hashmaps are seeded. You can optionally provide a seed after the map's dimensions as an argument, otherwise it's a random seed.
@ -44,8 +41,6 @@ The noise values on the hashmaps are seeded. You can optionally provide a seed a
## RESOURCES
* [Hex Map 1](https://catlikecoding.com/unity/tutorials/hex-map/) - unity tutorial for hexagon grids with some useful generalized math.
* [Hexagonal Grids](https://redblobgames.com/grids/hexagons) - THE resource on hexagonal grids on the internet.
* [Amulet Docs](http://amulet.xyz/doc) - amulet documentation.

159
src/hexyz.lua

@ -1,8 +1,6 @@
-- Rounds Numbers.
local function round(n)
return n % 1 >= 0.5 and math.ceil(n) or math.floor(n)
end
local function round(n) return n % 1 >= 0.5 and math.ceil(n) or math.floor(n) end
--[[============================================================================
----- HEX CONSTANTS AND UTILITY FUNCTIONS -----
@ -13,35 +11,31 @@ function hex_equals(a, b) return a[1] == b[1] and a[2] == b[2] end
function hex_not_equals(a, b) return not hex_equals(a, b) end
-- All Possible Vector Directions from a Given Hex by Edge
local HEX_DIRECTIONS = {vec2( 0 , 1), vec2( 1 , 0), vec2( 1 , -1),
vec2( 0 , -1), vec2(-1 , 0), vec2(-1 , 1)}
-- 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)}
-- Return Hex Vector Direction via Integer Index |direction|.
-- Return Hex Vector Direction via Integer Index |direction|
function hex_direction(direction)
return HEX_DIRECTIONS[(direction % 6) % 6 + 1]
end
return HEX_DIRECTIONS[(direction % 6) % 6 + 1] end
-- Return Hexagon Adjacent to |hex| in Integer Index |direction|.
-- Return Hexagon Adjacent to |hex| in Integer Index |direction|
function hex_neighbour(hex, direction)
return hex + HEX_DIRECTIONS[(direction % 6) % 6 + 1]
end
-- Return Hex 60deg away to the Left; Counter-Clockwise
function hex_rotate_left(hex)
return vec2(hex.x + hex.y, -hex.x)
end
return hex + HEX_DIRECTIONS[(direction % 6) % 6 + 1] end
-- Return Hex 60deg away to the Right; Clockwise
function hex_rotate_right(hex)
return vec2(-hex.y, hex.x + hex.y)
-- 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
end
-- NOT a General 3D Vector Round - Only Returns a vec2!
-- 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)
@ -56,8 +50,7 @@ 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
@ -65,7 +58,6 @@ end
----- 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),
@ -76,7 +68,6 @@ 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
@ -104,54 +95,36 @@ 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))
return vec2(size[1] * math.cos(angle), size[2] * math.sin(angle))
end
-- TODO this thing
-- 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
end
-- Offset Coordinates are Useful for UI-Implementations
-- 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
-- ... Back to Cube Coordinates
function offset_to_hex(off)
return vec2(off[1], off[2] - off[1] * (off[1] % 2) / 2)
end
return vec2(off[1], off[2] - math.floor((off[1] - 1 * (off[1] % 2))) / 2) end
--[[============================================================================
----- MAPS & STORAGE -----
You are not to draw using the coordinates stored in your map.
You are to draw using the hex_to_pixel of those coordinates.
If you wish to draw a hexagon to the screen, you must first use hex_to_pixel
to retrieve the center of the hexagon on each set of cube coordinates stored
in your map. Then, depending on how you are going to draw, either call
am.circle with |sides| = 6, or gather the vertices with hex_corners and
use am.draw - TODO, haven't used am.draw yet.
Maps have metatables containing information about their dimensions, and
seed (if applicable), so you can retrieve information about maps after they
are created.
----- NOISE -----
To simplify terrain generation, unordered, hash-like maps automatically
calculate and store seeded simplex noise as their values. You can provide
a seed if you wish. The default is a randomized seed.
TODO Pointy Hex testing and support.
============================================================================]]--
-- Returns Ordered Ring-Shaped Map of |radius| from |center|
function ring_map(center, radius)
local map = {}
@ -181,6 +154,17 @@ function spiral_map(center, radius)
end
-- Used to Retrieve Noise Values in Hashmap; t[vec2(x, y)] will always find nil
function hash_retrieve(map, hex)
for h,n in pairs(map) do
if hex_equals(hex, h) then
return n
end
end
return nil
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)
@ -200,7 +184,7 @@ function parallelogram_map(width, height, seed)
local pos = vec2(idelta + seed * width, jdelta + seed * height)
noise = noise + f * math.simplex(pos * l)
end
map[vec2(i, j)] = noise -- Straightforward Iteration Produces a Parallelogram
map[vec2(i, j)] = noise
end
end
setmetatable(map, {__index={width=width, height=height, seed=seed}})
@ -208,9 +192,9 @@ function parallelogram_map(width, height, seed)
end
-- Returns Unordered Triangular Map of |size| with Simplex Noise
-- Returns Unordered Triangular (Equilateral) Map of |size| with Simplex Noise
function triangular_map(size, seed)
local seed = seed or math.random(size)
local seed = seed or math.random(size * math.cos(size) / 2)
local map = {}
for i = 0, size do
@ -227,7 +211,6 @@ function triangular_map(size, seed)
local pos = vec2(idelta + seed * size, jdelta + seed * size)
noise = noise + f * math.simplex(pos * l)
end
map[vec2(i, j)] = noise
end
end
@ -238,9 +221,10 @@ end
-- Returns Unordered Hexagonal Map of |radius| with Simplex Noise
function hexagonal_map(radius, seed)
local seed = seed or math.random(radius * 2 + 1)
local seed = seed or math.random(radius * 2 * math.pi)
local map = {}
local mt = {__index={radius=radius, seed=seed}}
for i = -radius, radius do
local j1 = math.max(-radius, -i - radius)
local j2 = math.min(radius, -i + radius)
@ -253,17 +237,15 @@ function hexagonal_map(radius, seed)
local noise = 0
for oct = 1, 6 do
local f = 2/3^oct -- NOTE, for some reason, I found 2/3 produces better looking noise maps. As far as I am aware, this is weird.
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[vec2(i, j)] = noise
end
end
setmetatable(map, {__index={radius=radius, seed=seed}})
setmetatable(map, mt)
return map
end
@ -273,27 +255,26 @@ function rectangular_map(width, height, seed)
local seed = seed or math.random(width * height)
local map = {}
local mt = {__index={width=width, height=height, seed=seed}}
for i = 0, width do
for j = 0, height do
-- Calculate Noise
-- 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
-- Store Hex in the Map Paired with its Associated Noise Value
map[vec2(i, j - math.floor(i/2))] = noise
end
end
setmetatable(map, {__index={width=width, height=height, seed=seed}})
setmetatable(map, mt)
return map
end
@ -301,41 +282,7 @@ end
----- PATHFINDING -----
============================================================================]]--
-- big ol' TODO
-- first try
function search(map, start)
local neighbours
for i = 1, 6 do
neighbours[#neighbours + 1] = hex_neighbour(start, i)
end
end
--
function breadth_first_search(map, start)
local frontier = {start}
local visited = {start = true}
while next(frontier) ~= nil do
local current = next(frontier)
local neighbours
for i = 1, 6 do
neighbours[#neighnours + 1] = hex_neighbour(current, i)
end
for _,n in neighbours do
if visited[n] ~= true then
visited[n] = true
end
end
end
end

317
src/main.lua

@ -3,297 +3,136 @@ require"hexyz"
math.randomseed(os.time())
--[[============================================================================
----- GLOBALS -----
============================================================================]]--
-- Ethan Schoonover Solarized Colorscheme w/ Eigengrau
local EIGENGRAU = vec4(0.08, 0.08, 0.11, 1)
local BASE03 = vec4(0 , 0.16, 0.21, 1)
local BASE02 = vec4(0.02, 0.21, 0.25, 1)
local BASE01 = vec4(0.34, 0.43, 0.45, 1)
local BASE00 = vec4(0.39, 0.48, 0.51, 1)
local BASE0 = vec4(0.51, 0.58, 0.58, 1)
local BASE1 = vec4(0.57, 0.63, 0.63, 1)
local BASE2 = vec4(0.93, 0.90, 0.83, 1)
local BASE3 = vec4(0.99, 0.96, 0.89, 1)
local YELLOW = vec4(0.70, 0.53, 0, 1)
local ORANGE = vec4(0.79, 0.29, 0.08, 1)
local RED = vec4(0.86, 0.19, 0.18, 1)
local MAGENTA = vec4(0.82, 0.21, 0.50, 1)
local VIOLET = vec4(0.42, 0.44, 0.76, 1)
local BLUE = vec4(0.14, 0.54, 0.82, 1)
local CYAN = vec4(0.16, 0.63, 0.59, 1)
local GREEN = vec4(0.52, 0.60, 0 , 1)
local win = am.window
{
-- Base Resolution = 3/4 * WXGA standard 16:10
width = 1280 * 3/4, -- 960px
height = 800 * 3/4, -- 600px
clear_color = EIGENGRAU
}
local curtain = am.rect(win.left, win.top, win.right, win.bottom
, vec4(0, 0, 0, 0.7)) -- Color
local map
local world
local home
local home_node
--local log = io.open(os.date("log %c.txt"), "w")
--[[============================================================================
----- ROUTINES -----
============================================================================]]--
function find_home()
home = spiral_map(vec2(23, 4), 2)
home_node = am.group()
repeat
local happy = true
-- Template for Buttons
function rect_button(text)
return am.group
{
am.rect(-150, -20, 150, 20, vec4(0)):action(am.tween(1, {color=BASE2{a=0.4}})),
am.text(text, vec4(0)):action(am.tween(1, {color=BASE02}))
}
end
for i,h in pairs(home) do
local elevation = hash_retrieve(map, h)
--
function text_field(tag)
local field = am.text(""):action(function()
-- Special Cases
if win:key_pressed("backspace") then
field.text = field.text:sub(1, -2)
elseif win:key_pressed("enter") then
field.text = ""
-- I Only Use This To Get Seeds At The Moment
elseif win:key_pressed("0") then field.text = field.text .. 0
elseif win:key_pressed("1") then field.text = field.text .. 1
elseif win:key_pressed("2") then field.text = field.text .. 2
elseif win:key_pressed("3") then field.text = field.text .. 3
elseif win:key_pressed("4") then field.text = field.text .. 4
elseif win:key_pressed("5") then field.text = field.text .. 5
elseif win:key_pressed("6") then field.text = field.text .. 6
elseif win:key_pressed("7") then field.text = field.text .. 7
elseif win:key_pressed("8") then field.text = field.text .. 8
elseif win:key_pressed("9") then field.text = field.text .. 9
end
end)
return field:tag(tag)
end
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, 2)
home_node = am.group()
-- Tween Multiple Nodes of the Same Type
function multitween(nodes, time, values)
for _,node in pairs(nodes) do
node:action(am.tween(time, values))
else
local center = hex_to_pixel(h, vec2(11))
local color = vec4(0.5)
local node = am.circle(center, 4, color, 4)
home_node:append(node)
end
end
until happy
return home_node
end
-- Returns Appropriate Tile Color for Specified Elevation
function color_at(elevation)
if elevation < -0.5 then -- Lowest Elevation : Impassable
return vec4(0.10, 0.30, 0.40, (elevation + 1.2) / 2)
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)
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)
else -- Highest Elevation : Impassable
return vec4(0.15, 0.30, 0.20, (elevation + 1.0) / 2)
end
end
-- Handler for Scoring
function keep_score()
local offset = am.current_time()
win.scene:action(function()
win.scene:remove("time")
local time_str = string.format("%.2f", am.current_time() - offset)
win.scene:append(am.translate(-374, win.top - 10)
^ am.text(time_str):tag"time")
end)
end
return vec4(0.25, 0.20, 0.10, (elevation + 1.6) / 2 + 0.2)
-- In-Game Pause Menu
function pause()
win.scene:append(curtain:tag"curtain")
win.scene:append(am.group
{
am.rect(-200, 150, 200, -150, BASE03{a=0.9})
}:tag"menu")
elseif elevation < 1 then -- Highest Elevation : Impassable
return vec4(0.15, 0.30, 0.20, (elevation + 1.0) / 2 + 0.2)
-- Event Handler
win.scene:action(function()
-- Back to Main Game
if win:key_pressed("escape") then
win.scene:remove("curtain"); win.scene:remove("menu")
game_action(); return true
else
return vec4(0.5, 0.5, 0.5, 1)
end
end)
end
-- Returns Node
function draw_(map)
local world = am.group():tag"world"
world = am.group()
for hex,elevation in pairs(map) do
local off = hex_to_offset(hex)
local mask = vec4(0, 0, 0, math.max(((off.x - 23) / 45) ^ 2,
((-off.y - 16) / 31) ^ 2))
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(hex, vec2(11)), 11, vec4(0), 6)
node:action(am.tween(0.3, {color = color})) -- fade in
world:append(node:tag(tostring(hex))) -- unique identifier
end
return world
end
--
function ghandler(game)
win.scene:remove("coords"); win.scene:remove("selected")
-- Pause Game
if win:key_pressed("escape") then
pause(); return true
end
local hex = pixel_to_hex(win:mouse_position(), vec2(11)) - vec2(10, 10)
local mouse = hex_to_offset(hex)
-- Check if Mouse is Within Bounds of Game Map
if mouse.x > 0 and mouse.x < map.width and
-mouse.y > 0 and -mouse.y < map.height then -- North is Positive
local text = am.text(string.format("%d,%d", mouse.x, mouse.y))
game:append(am.translate(450, 290) ^ text:tag"coords")
local color = vec4(0, 0, 0, 0.4)
local pix = hex_to_pixel(hex, vec2(11))
game:append(am.circle(pix, 11, color, 6):tag"selected")
:action(am.tween(5, {color = color}, am.ease.out(am.ease.hyperbola)))
world:append(node:tag(tostring(hex)))
end
end
world:append(find_home())
-- Begin Game - From Seed or Random Seed (if ommitted)
function game_init(seed)
local game = am.group()
game:append(am.rect(win.left, win.top, -268, win.bottom, BASE03))
map = rectangular_map(45, 31, seed)
game:action(ghandler)
win.scene = am.group(am.translate(-268, -300) ^ draw_(map), game)
return am.translate(-278, -318) ^ world:tag"world"
end
-- Title Action
function thandler(title)
local mouse = win:mouse_position()
if mouse.x > -150 and mouse.x < 150 then
-- Button 1
if mouse.y > -70 and mouse.y < -30 then
title"button1""rect":action(am.tween(0.5, {color = BASE2{a=1}}))
if win:mouse_pressed("left") then
win.scene:action(am.play(am.sfxr_synth(57784609)))
game_init(map.seed)
return true
end
-- Button 2
elseif mouse.y > -120 and mouse.y < -80 then
title"button2""rect":action(am.tween(0.5, {color = BASE2{a=1}}))
if win:mouse_pressed("left") then
win.scene:action(am.play(am.sfxr_synth(91266909)))
end
-- Button 3
elseif mouse.y > -170 and mouse.y < -130 then
function spawner(world)
title"button3""rect":action(am.tween(0.5, {color = BASE2{a=1}}))
if win:mouse_pressed("left") then
local synth_seed = math.random(100000000)
win.scene:action(am.play(am.sfxr_synth(synth_seed)))
print(synth_seed)
if math.random(10) == 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) * 47
else
y = math.random(0, 1) * 33
end
-- Button 4
elseif mouse.y > -220 and mouse.y < -180 then
title"button4""rect":action(am.tween(0.5, {color = BASE2{a=1}}))
if win:mouse_pressed("left") then
win.scene:action(am.play(am.sfxr_synth(36002209)))
spawn_position = offset_to_hex(vec2(x, y))
-- ensure that we spawn somewhere that is passable: mid-elevation
local e = hash_retrieve(map, spawn_position)
until e and e < 0.5 and e > -0.5
local mob = am.circle(hex_to_pixel(spawn_position, vec2(11)), 4, vec4(1), 6)
:action(coroutine.create(function(mob)
local dead = false
repeat
local neighbours = hex_neighbours(pixel_to_hex(mob.center, vec2(11)))
local candidates = {}
for _,h in pairs(neighbours) do
local e = hash_retrieve(map, h)
if e and e < 0.5 and e > -0.5 then
table.insert(candidates, h)
end
else
multitween({title"button1""rect", title"button2""rect",
title"button3""rect", title"button4""rect"},
0.5, {color = BASE2{a=0.4}})
end
else
multitween({title"button1""rect", title"button2""rect",
title"button3""rect", title"button4""rect"},
0.5, {color = BASE2{a=0.4}})
local move = candidates[math.random(#candidates)]
am.wait(am.tween(mob, 1, {center=hex_to_pixel(move, vec2(11))}))
until dead
end))
world:append(mob)
end
end
-- Setup and Display Title Screen
function title_init()
local title = am.group()
title:append(am.translate(0, -50) ^ rect_button("NEW SCENARIO"):tag"button1")
title:append(am.translate(0, -100) ^ rect_button("LOREMIPSUM"):tag"button2")
title:append(am.translate(0, -150) ^ rect_button("FUN BUTTON"):tag"button3")
title:append(am.translate(0, -200) ^ rect_button("SETTINGS"):tag"button4")
map = hexagonal_map(45)
backdrop = am.scale(1.3) ^ am.rotate(0) ^ draw_(map)
backdrop:action(function()
backdrop"rotate".angle = am.frame_time / 40 + 45
end)
-- Event Handler
title:action(thandler)
win.scene = am.group(backdrop, title)
end
function game_init(seed)
local bg = am.rect(-480, 300, -268, -300, vec4(0.12, 0.3, 0.3, 1))
map = rectangular_map(46, 33)
-- Alias
function init()
title_init()
math.randomseed(map.seed)
win.scene = am.group(draw_(map):action(spawner), bg)
end
--=============================================================================
----- Main -----
init()
--
game_init()

16
src/sprites.lua

@ -1,16 +0,0 @@
settings =
[[
.
.
.
.
.
.
.
.
]]
Loading…
Cancel
Save