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