Nicholas Hayashi
4 years ago
10 changed files with 335 additions and 253 deletions
-
2README.md
-
BINres/wall_closed.png
-
BINres/wall_open.png
-
6src/colors.lua
-
149src/grid.lua
-
54src/gui.lua
-
59src/hexyz.lua
-
163src/main.lua
-
141src/mob.lua
-
14src/util.lua
After Width: 138 | Height: 138 | Size: 2.0 KiB |
After Width: 137 | Height: 137 | Size: 1.8 KiB |
@ -1,15 +1,17 @@ |
|||||
|
|
||||
COLORS = { |
COLORS = { |
||||
TRANSPARENT = vec4(0.4), |
TRANSPARENT = vec4(0.4), |
||||
--TRANSPARENT = vec4(0.6), |
|
||||
|
|
||||
|
-- tones |
||||
WHITE = vec4(0.8, 0.8, 0.7, 1), |
WHITE = vec4(0.8, 0.8, 0.7, 1), |
||||
BLACK = vec4(0, 0, 0, 1), |
BLACK = vec4(0, 0, 0, 1), |
||||
|
TRUEBLACK = vec4(0, 0, 0, 1), |
||||
|
|
||||
-- hues |
-- hues |
||||
BLUE_STONE = vec4(0.12, 0.3, 0.3, 1), |
BLUE_STONE = vec4(0.12, 0.3, 0.3, 1), |
||||
MYRTLE = vec4(0.10, 0.25, 0.10, 1), |
MYRTLE = vec4(0.10, 0.25, 0.10, 1), |
||||
BROWN_POD = vec4(0.25, 0.20, 0.10, 1), |
BROWN_POD = vec4(0.25, 0.20, 0.10, 1), |
||||
BOTTLE_GREEN = vec4(0.15, 0.30, 0.20, 1) |
|
||||
|
BOTTLE_GREEN = vec4(0.15, 0.30, 0.20, 1), |
||||
|
MAGENTA = vec4(1, 0, 1, 1) |
||||
} |
} |
||||
|
|
@ -1,83 +1,94 @@ |
|||||
|
|
||||
require "colors" |
require "colors" |
||||
|
require "gui" |
||||
|
|
||||
WORLD_GRID_DIMENSIONS = vec2(46, 32) |
|
||||
CELL_SIZE = 20 |
|
||||
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 |
|
||||
|
HEX_SIZE = 20 |
||||
|
HEX_GRID_WIDTH = 65 |
||||
|
HEX_GRID_HEIGHT = 33 |
||||
|
HEX_GRID_DIMENSIONS = vec2(HEX_GRID_WIDTH, HEX_GRID_HEIGHT) |
||||
|
|
||||
-- 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 } |
|
||||
|
-- @NOTE no idea why the y coordinate doesn't need to be transformed here |
||||
|
HEX_GRID_CENTER = vec2(math.floor(HEX_GRID_DIMENSIONS.x/2), 0) |
||||
|
|
||||
elseif elevation < 0 then -- med-low elevation : passable |
|
||||
return COLORS.MYRTLE{ a = (elevation + 1.8) / 2 + 0.2 } |
|
||||
|
-- index is hex coordinates [x][y] |
||||
|
-- { { elevation, sprite, tile } } |
||||
|
HEX_MAP = {} |
||||
|
|
||||
elseif elevation < 0.5 then -- med-high elevation : passable |
|
||||
return COLORS.BROWN_POD{ a = (elevation + 1.6) / 2 + 0.2 } |
|
||||
|
function grid_pixel_dimensions() |
||||
|
local hhs = hex_horizontal_spacing(HEX_SIZE) |
||||
|
local hvs = hex_vertical_spacing(HEX_SIZE) |
||||
|
|
||||
elseif elevation < 1 then -- highest elevation : impassable |
|
||||
return COLORS.BOTTLE_GREEN{ a = (elevation + 1.0) / 2 + 0.2 } |
|
||||
end |
|
||||
|
-- number of 'spacings' on the grid == number of cells - 1 |
||||
|
return vec2((HEX_GRID_DIMENSIONS.x - 1) * hhs |
||||
|
, (HEX_GRID_DIMENSIONS.y - 1) * hvs) |
||||
end |
end |
||||
|
|
||||
function worldspace_coordinate_offset() |
|
||||
return vec2(-hex_height(CELL_SIZE)) |
|
||||
|
GRID_PIXEL_DIMENSIONS = grid_pixel_dimensions() |
||||
|
WORLDSPACE_COORDINATE_OFFSET = -GRID_PIXEL_DIMENSIONS/2 |
||||
|
|
||||
|
-- convience function for when getting a tile at x,y could fail |
||||
|
function get_tile(x, y) |
||||
|
return HEX_MAP[x] and HEX_MAP[x][y] |
||||
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(worldspace_coordinate_offset()) ^ am.group(am.circle(vec2(0), 32, COLORS.WHITE)):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)), CELL_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" |
|
||||
|
-- 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 } |
||||
|
else |
||||
|
log('bad elevation') |
||||
|
return vec4(0) |
||||
|
end |
||||
end |
end |
||||
|
|
||||
|
function random_map(seed, do_seed_rng) |
||||
|
local elevation_map = rectangular_map(HEX_GRID_DIMENSIONS.x, HEX_GRID_DIMENSIONS.y, seed) |
||||
|
|
||||
|
if do_seed_rng then math.randomseed(elevation_map.seed) end |
||||
|
|
||||
|
HEX_MAP = {} |
||||
|
local world = am.group():tag"world" |
||||
|
for i,_ in pairs(elevation_map) do |
||||
|
HEX_MAP[i] = {} |
||||
|
for j,elevation in pairs(elevation_map[i]) do |
||||
|
|
||||
|
local off = hex_to_evenq(vec2(i, j)) |
||||
|
local mask = vec4(0, 0, 0, math.max(((off.x - HEX_GRID_DIMENSIONS.x/2) / HEX_GRID_DIMENSIONS.x) ^ 2 |
||||
|
, ((-off.y - HEX_GRID_DIMENSIONS.y/2) / HEX_GRID_DIMENSIONS.y) ^ 2)) |
||||
|
local color = color_at(elevation) - mask |
||||
|
|
||||
|
local node = am.circle(hex_to_pixel(vec2(i, j)), HEX_SIZE, color, 6) |
||||
|
|
||||
|
HEX_MAP[i][j] = { |
||||
|
elevation = elevation, |
||||
|
sprite = node, |
||||
|
tile = {} |
||||
|
} |
||||
|
|
||||
|
world:append(node) |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
-- the center of the map in some radius is always considered 'passable' terrain and is home base |
||||
|
-- terraform this area to ensure it's passable |
||||
|
-- @NOTE no idea why the y-coord doesn't need to be transformed |
||||
|
local home = spiral_map(HEX_GRID_CENTER, 3) |
||||
|
for _,hex in pairs(home) do |
||||
|
HEX_MAP[hex.x][hex.y].elevation = 0 |
||||
|
HEX_MAP[hex.x][hex.y].sprite.color = color_at(0) |
||||
|
world:append(am.circle(hex_to_pixel(vec2(hex.x, hex.y)), HEX_SIZE/2, COLORS.MAGENTA, 4)) |
||||
|
end |
||||
|
|
||||
|
return am.translate(WORLDSPACE_COORDINATE_OFFSET) |
||||
|
^ world:tag"world" |
||||
|
end |
||||
|
|
@ -1,9 +1,53 @@ |
|||||
|
|
||||
local hot |
|
||||
local active |
|
||||
|
local hot, active = false, false |
||||
|
local widgets = {} |
||||
|
|
||||
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 get_widgets() return widgets end |
||||
|
|
||||
|
function register_widget(id, poll) |
||||
|
widgets[id] = { id = id, poll = poll } |
||||
|
end |
||||
|
|
||||
|
function point_in_rect(point, rect) |
||||
|
return point.x > rect.x1 and point.x < rect.x2 and point.y > rect.y1 and point.y < rect.y2 |
||||
|
end |
||||
|
|
||||
|
function set_hot(id) |
||||
|
if not active then hot = { id = id } end |
||||
|
end |
||||
|
|
||||
|
function register_button_widget(id, rect) |
||||
|
register_widget(id, function() |
||||
|
local click = false |
||||
|
|
||||
|
if active and active.id == id then |
||||
|
if win:mouse_released"left" then |
||||
|
if hot and hot.id == id then click = true end |
||||
|
active = false |
||||
|
end |
||||
|
elseif hot and hot.id == id then |
||||
|
if win:mouse_pressed"left" then active = { id = id } end |
||||
|
end |
||||
|
|
||||
|
if point_in_rect(win:mouse_position(), rect) then set_hot(id) end |
||||
|
|
||||
|
return click |
||||
|
end) |
||||
|
end |
||||
|
|
||||
|
function make_button_widget(id, position, dimensions, text) |
||||
|
local rect = am.rect( |
||||
|
-dimensions.x/2, |
||||
|
dimensions.y/2, |
||||
|
dimensions.x/2, |
||||
|
-dimensions.y/2, |
||||
|
vec4(1, 0, 0, 1) |
||||
|
) |
||||
|
|
||||
|
register_button_widget(id, rect) |
||||
|
return am.group{ |
||||
|
rect, |
||||
|
am.text(text) |
||||
|
}:tag(id) |
||||
end |
end |
||||
|
|
@ -1,79 +1,108 @@ |
|||||
|
|
||||
|
MOBS = {} |
||||
|
|
||||
MOB_HURTBOX_RADIUS = 4 |
|
||||
|
|
||||
|
|
||||
-- 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), MOB_HURTBOX_RADIUS) |
|
||||
world:append(mob"circle":action(coroutine.create(live))) |
|
||||
end |
|
||||
|
-- check if a |tile| is passable by |mob| |
||||
|
function can_pass_through(mob, tile) |
||||
|
return tile and tile.elevation and tile.elevation < 0.5 and tile.elevation > -0.5 |
||||
end |
end |
||||
|
|
||||
-- this function is the coroutine that represents the life-cycle of a mob. |
|
||||
function live(mob) |
|
||||
local dead = false |
|
||||
|
function get_path(mob, starting_hex, goal_hex) |
||||
|
local moves = {} |
||||
|
|
||||
local visited = {} |
local visited = {} |
||||
visited[mob.center.x] = {}; visited[mob.center.x][mob.center.y] = true |
|
||||
|
visited[starting_hex.x] = {} |
||||
|
visited[starting_hex.x][starting_hex.y] = true |
||||
|
|
||||
-- begin life |
|
||||
repeat |
repeat |
||||
local neighbours = hex_neighbours(pixel_to_hex(mob.center)) |
|
||||
|
local neighbours = hex_neighbours(pixel_to_hex(mob.position)) |
||||
local candidates = {} |
local candidates = {} |
||||
|
|
||||
-- get list of candidates: hex positions to consider moving to. |
-- 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 |
|
||||
|
for _,neighbour in pairs(neighbours) do |
||||
|
if can_pass_through(mob, get_tile(neighbour.x, neighbour.y)) then |
||||
|
if not (visited[neighbour.h] and visited[neighbour.x][neighbour.y]) then |
||||
|
table.insert(candidates, neighbour) |
||||
else |
else |
||||
table.insert(candidates, h) |
|
||||
|
--table.insert(candidates, neighbour) |
||||
end |
end |
||||
end |
end |
||||
end |
end |
||||
|
|
||||
-- choose where to move. manhattan distance closest to goal is chosen. |
|
||||
|
-- choose where to move |
||||
local move = candidates[1] |
local move = candidates[1] |
||||
for _,h in pairs(candidates) do |
|
||||
if math.distance(h, home.center) < math.distance(move, home.center) then |
|
||||
move = h |
|
||||
|
for _,hex in pairs(candidates) do |
||||
|
if math.distance(hex, goal_hex) < math.distance(move, goal_hex) then |
||||
|
move = hex |
||||
end |
end |
||||
end |
end |
||||
|
|
||||
if not move then print("can't find anywhere to move to"); return |
|
||||
end -- bug |
|
||||
|
if move then |
||||
|
table.insert(moves, move) |
||||
|
visited[move.x] = {} |
||||
|
visited[move.x][move.y] = true |
||||
|
end |
||||
|
|
||||
|
--if move == goal then log('made it!') return end |
||||
|
until move == goal_hex |
||||
|
|
||||
|
return moves |
||||
|
end |
||||
|
|
||||
|
function get_spawn_hex(mob) |
||||
|
local spawn_hex |
||||
|
repeat |
||||
|
-- ensure we spawn on an random tile along the map's edges |
||||
|
local roll = math.random(HEX_GRID_WIDTH * 2 + HEX_GRID_HEIGHT * 2) - 1 |
||||
|
local x, y |
||||
|
|
||||
|
if roll < HEX_GRID_HEIGHT then |
||||
|
x, y = 0, roll |
||||
|
|
||||
|
elseif roll < (HEX_GRID_WIDTH + HEX_GRID_HEIGHT) then |
||||
|
x, y = roll - HEX_GRID_HEIGHT, HEX_GRID_HEIGHT - 1 |
||||
|
|
||||
|
elseif roll < (HEX_GRID_HEIGHT * 2 + HEX_GRID_WIDTH) then |
||||
|
x, y = HEX_GRID_WIDTH - 1, roll - HEX_GRID_WIDTH - HEX_GRID_HEIGHT |
||||
|
|
||||
|
else |
||||
|
x, y = roll - (HEX_GRID_HEIGHT * 2) - HEX_GRID_WIDTH, 0 |
||||
|
end |
||||
|
|
||||
|
-- @NOTE negate 'y' because hexyz algorithms assume south is positive, in amulet north is positive |
||||
|
spawn_hex = evenq_to_hex(vec2(x, -y)) |
||||
|
local tile = HEX_MAP[spawn_hex.x][spawn_hex.y] |
||||
|
|
||||
|
until can_pass_through(mob, tile) |
||||
|
|
||||
|
return spawn_hex |
||||
|
end |
||||
|
|
||||
|
function make_mob() |
||||
|
local mob = {} |
||||
|
|
||||
|
local spawn_hex = get_spawn_hex(mob) |
||||
|
log(spawn_hex) |
||||
|
local spawn_position = hex_to_pixel(spawn_hex) + WORLDSPACE_COORDINATE_OFFSET |
||||
|
|
||||
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) |
|
||||
|
mob.position = spawn_position |
||||
|
--mob.path = get_path(mob, spawn_hex, HEX_GRID_CENTER) |
||||
|
mob.sprite = am.circle(spawn_position, 18, COLORS.WHITE, 4) |
||||
|
win.scene:append(mob.sprite) |
||||
|
|
||||
|
return mob |
||||
|
end |
||||
|
|
||||
|
local SPAWN_CHANCE = 25 |
||||
|
function do_mob_spawning() |
||||
|
if win:key_pressed"space" then |
||||
|
--if math.random(SPAWN_CHANCE) == 1 then |
||||
|
table.insert(MOBS, make_mob()) |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
function do_mob_updates() |
||||
|
for _,mob in pairs(MOBS) do |
||||
|
|
||||
|
end |
||||
end |
end |
||||
|
|
@ -0,0 +1,14 @@ |
|||||
|
|
||||
|
function table.shift(t, count) |
||||
|
local e = t[1] |
||||
|
t[1] = nil |
||||
|
|
||||
|
for i,e in pairs(t) do |
||||
|
if e then |
||||
|
t[i - 1] = e |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
return e |
||||
|
end |
||||
|
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue