Browse Source

more map editor stuff, end game stats

master
Nicholas Hayashi 3 years ago
parent
commit
e016f4769d
  1. 150
      main.lua
  2. BIN
      res/mainmenuhex.png
  3. 14
      src/extra.lua
  4. 173
      src/game.lua
  5. 51
      src/grid.lua
  6. 11
      src/gui.lua
  7. 2
      src/hexyz.lua
  8. 73
      src/map-editor.lua
  9. 2
      src/tower.lua
  10. 1
      texture.lua

150
main.lua

@ -69,6 +69,25 @@ win = am.window{
show_cursor = true,
}
-- top right display types
-- different scenes overlay different content in the top right of the screen
-- f1 toggles what is displayed in the top right of the screen in some scenes
TRDTS = {
NOTHING = 0,
CENTERED_EVENQ = 1,
EVENQ = 2,
HEX = 3,
PLATFORM = 4,
PERF = 5,
SEED = 6,
TILE = 7,
}
function make_top_right_display_node()
return am.translate(win.right - 10, win.top - 15)
^ am.text("", "right", "top"):tag"top_right_display"
end
-- asset interfaces and/or trivial code
require "conf"
require "color"
@ -89,7 +108,7 @@ require "src/tower"
require "src/map-editor"
local sound_toggle_node_tag = "sound-on-off-icon"
local sound_toggle_node_tag = "sound_on_off_icon"
local function make_sound_toggle_node(on)
local sprite
if on then
@ -101,7 +120,7 @@ local function make_sound_toggle_node(on)
return (am.translate(win.right - 30, win.top - 60) ^ sprite)
:tag(sound_toggle_node_tag)
:action(function()
-- @TODO click me!
end)
end
@ -126,22 +145,6 @@ local function toggle_mute()
win.scene:replace(sound_toggle_node_tag, make_sound_toggle_node(settings.sound_on))
end
-- text popup in the middle of the screen that dissapates, call from anywhere
function alert(message, color)
win.scene:append(
am.scale(3) ^ am.text(message, color or COLORS.WHITE)
:action(coroutine.create(function(self)
am.wait(am.tween(self, 1, { color = vec4(0) }, am.ease_out))
win.scene:remove(self)
end))
)
end
function unpause(root_node)
win.scene("game").paused = false
win.scene:remove(root_node)
end
function main_action(self)
if win:key_pressed("escape") then
if game then
@ -161,61 +164,7 @@ function main_action(self)
end
end
function make_main_scene_toolbelt()
local include_save_option = game
local include_unpause_option = game
local options = {
false,
{
texture = TEXTURES.NEW_GAME_HEX,
action = function()
win.scene:remove"menu"
game_init()
end
},
false,
include_save_option and {
texture = TEXTURES.SAVE_GAME_HEX,
action = function()
game_save()
alert("succesfully saved!")
end
} or false,
false,
{
texture = TEXTURES.LOAD_GAME_HEX,
action = function()
local save = am.load_state("save", "json")
if save then
win.scene:remove("menu")
game_init(save)
else
alert("no saved games")
end
end
},
{
texture = TEXTURES.MAP_EDITOR_HEX,
action = function()
win.scene:remove("menu")
map_editor_init()
end
},
include_unpause_option and {
texture = TEXTURES.UNPAUSE_HEX,
action = function() unpause(win.scene("menu")) end
} or false,
false and {
texture = TEXTURES.SETTINGS_HEX,
action = function() alert("not yet :)") end
},
{
texture = TEXTURES.QUIT_HEX,
action = function() win:close() end
},
false
}
function make_scene_menu(scene_options)
-- calculate the dimensions of the whole grid
local spacing = 150
@ -227,6 +176,7 @@ function make_main_scene_toolbelt()
local grid_pixel_height = grid_height * hvs
local pixel_offset = vec2(-grid_pixel_width/2, win.bottom + hex_height(spacing)/2 + 20)
-- generate a map of hexagons (the menu is made up of two rows of hexes) and populate their locations with buttons from the provided options
local map = hex_rectangular_map(grid_width, grid_height, HEX_ORIENTATION.POINTY)
local group = am.group()
local option_index = 1
@ -234,7 +184,7 @@ function make_main_scene_toolbelt()
for j,_ in pairs(map[i]) do
local hex = vec2(i, j)
local position = hex_to_pixel(hex, vec2(spacing), HEX_ORIENTATION.POINTY)
local option = options[option_index]
local option = scene_options[option_index]
local texture = option and option.texture or TEXTURES.SHADED_HEX
local color = option and COLORS.TRANSPARENT or vec4(0.3)
local node = am.translate(position)
@ -277,7 +227,6 @@ function make_main_scene_toolbelt()
return am.translate(pixel_offset) ^ group
end
function main_scene(do_backdrop, do_logo)
local group = am.group()
@ -338,17 +287,58 @@ function main_scene(do_backdrop, do_logo)
group:append(logo)
end
group:append(make_main_scene_toolbelt())
local main_scene_options = {
false,
{
texture = TEXTURES.NEW_GAME_HEX,
action = function()
win.scene:remove"map_editor"
win.scene:remove"menu"
game_init()
end
},
false,
false,
false,
{
texture = TEXTURES.LOAD_GAME_HEX,
action = function()
local save = am.load_state("save", "json")
if save then
win.scene:remove("menu")
game_init(save)
else
gui_alert("no saved games")
end
end
},
{
texture = TEXTURES.MAP_EDITOR_HEX,
action = function()
win.scene:remove("menu")
map_editor_init()
end
},
false,
{
texture = TEXTURES.SETTINGS_HEX,
action = function() gui_alert("not yet :)") end
},
{
texture = TEXTURES.QUIT_HEX,
action = function() win:close() end
},
false
}
group:append(make_scene_menu(main_scene_options))
group:action(main_action)
return group:tag"menu"
end
function switch_scene(scene)
win.scene = am.group(scene)
end
win.scene = am.group(
main_scene(true, true)
)

BIN
res/mainmenuhex.png

After

Width: 282  |  Height: 290  |  Size: 59 KiB

14
src/extra.lua

@ -29,6 +29,20 @@ function table.count(t)
return count
end
function table.highest_index(t)
local highest = nil
for i,v in pairs(t) do
if i and not highest then
highest = i
end
if i > highest then
highest = i
end
end
return highest
end
function table.find(t, predicate)
for i,v in pairs(t) do
if predicate(v) then

173
src/game.lua

@ -2,24 +2,11 @@
game = false -- flag to tell if there is a game running
game_state = {}
-- top right display types
-- f1 toggles what is displayed in the top right of the screen
local TRDTS = {
NOTHING = 0,
CENTERED_EVENQ = 1,
EVENQ = 2,
HEX = 3,
PLATFORM = 4,
PERF = 5,
SEED = 6,
TILE = 7,
}
local function get_initial_game_state(seed)
local STARTING_MONEY = 75
local map = random_map(seed)
local world = make_hex_grid_scene(map)
local world = make_hex_grid_scene(map, true)
return {
map = map, -- map of hex coords map[x][y] to a 'tile'
@ -47,14 +34,6 @@ local function get_initial_game_state(seed)
}
end
local function get_wave_timer_text()
if game_state.spawning then
return string.format("WAVE (%d) OVER: %.2f", game_state.current_wave, game_state.time_until_next_break)
else
return string.format("NEXT WAVE (%d): %.2f", game_state.current_wave, game_state.time_until_next_wave)
end
end
local function get_top_right_display_text(hex, evenq, centered_evenq, display_type)
local str = ""
if display_type == TRDTS.CENTERED_EVENQ then
@ -81,6 +60,14 @@ local function get_top_right_display_text(hex, evenq, centered_evenq, display_ty
return str
end
local function get_wave_timer_text()
if game_state.spawning then
return string.format("WAVE (%d) OVER: %.2f", game_state.current_wave, game_state.time_until_next_break)
else
return string.format("NEXT WAVE (%d): %.2f", game_state.current_wave, game_state.time_until_next_wave)
end
end
-- initialized later, as part of the init of the toolbelt
local function select_tower_type(tower_type) end
local function select_toolbelt_button(i) end
@ -104,7 +91,64 @@ end
local function game_pause()
win.scene("game").paused = true
win.scene:append(main_scene(false, false))
local game_scene_options = {
false,
{
texture = TEXTURES.NEW_GAME_HEX,
action = function()
game_init()
end
},
false,
{
texture = TEXTURES.SAVE_GAME_HEX,
action = function()
game_save()
gui_alert("succesfully saved!")
end
},
false,
{
texture = TEXTURES.LOAD_GAME_HEX,
action = function()
local save = am.load_state("save", "json")
if save then
game_init(save)
else
gui_alert("no saved games")
end
end
},
{
texture = TEXTURES.MAP_EDITOR_HEX,
action = function()
win.scene:remove("game")
map_editor_init(game_state.map.seed)
end
},
{
texture = TEXTURES.UNPAUSE_HEX,
action = function()
end
},
{
texture = TEXTURES.SETTINGS_HEX,
action = function()
gui_alert("not yet :)")
end
},
{
texture = TEXTURES.QUIT_HEX,
action = function()
win:close()
end
},
false
}
win.scene:append(make_scene_menu(game_scene_options))
end
local function game_deserialize(json_string)
@ -116,7 +160,7 @@ local function game_deserialize(json_string)
end
new_game_state.map = random_map(new_game_state.seed)
new_game_state.world = make_hex_grid_scene(new_game_state.map)
new_game_state.world = make_hex_grid_scene(new_game_state.map, true)
new_game_state.seed = nil
for i,t in pairs(new_game_state.towers) do
@ -159,7 +203,7 @@ local function game_deserialize(json_string)
end
local function game_serialize()
local serialized = table.shallow_copy(state)
local serialized = table.shallow_copy(game_state)
serialized.version = version
serialized.seed = game_state.map.seed
@ -172,6 +216,7 @@ local function game_serialize()
-- and the scene graph needs to be re-constituted at load time
--
-- this is dumb and if i forsaw this i would have probably used float arrays instead of vectors
-- (the scene graph bit makes sense though)
serialized.towers = {}
for i,t in pairs(game_state.towers) do
@ -201,10 +246,16 @@ local function deselect_tile()
win.scene:remove("tile_select_box")
end
local function game_pause_menu()
end
local function game_action(scene)
if game_state.score < 0 then game_end() return true end
if game_state.score < 0 then
game_end()
return true
end
local perf = am.perf_stats()
game_state.time = game_state.time + am.delta_time
game_state.score = game_state.score + am.delta_time
@ -258,14 +309,14 @@ local function game_action(scene)
node.color = COLORS.CLARET
node:action(am.tween(0.1, { color = COLORS.TRANSPARENT }))
play_sfx(SOUNDS.BIRD2)
alert("closes the circle")
gui_alert("closes the circle")
elseif cost > game_state.money then
local node = win.scene("cursor"):child(2)
node.color = COLORS.CLARET
node:action(am.tween(0.1, { color = COLORS.TRANSPARENT }))
play_sfx(SOUNDS.BIRD2)
alert("not enough money")
gui_alert("not enough money")
else
update_money(-cost)
@ -532,7 +583,7 @@ local function make_game_toolbelt()
-- de-selecting currently selected tower if any
toolbelt("toolbelt_select_square").hidden = true
win.scene:replace("cursor", make_hex_cursor(0, COLORS.TRANSPARENT):tag"cursor")
win.scene:replace("cursor", make_hex_cursor_node(0, COLORS.TRANSPARENT):tag"cursor")
end
end
@ -604,14 +655,6 @@ local function game_scene()
end
end)
local top_right_display =
am.translate(win.right - 10, win.top - 15)
^ am.text("", "right", "top"):tag"top_right_display"
local bottom_right_display =
am.translate(win.right - 10, win.bottom + win.height * 0.07 + 20)
^ am.text("", "right", "bottom"):tag"bottom_right_display"
local curtain = am.rect(win.left, win.bottom, win.right, win.top, COLORS.TRUE_BLACK)
curtain:action(coroutine.create(function()
am.wait(am.tween(curtain, 3, { color = vec4(0) }, am.ease.out(am.ease.hyperbola)))
@ -621,12 +664,12 @@ local function game_scene()
local scene = am.group(
am.scale(1):tag"world_scale" ^ game_state.world,
am.translate(HEX_GRID_CENTER):tag"cursor_translate" ^ make_hex_cursor(0, COLORS.TRANSPARENT):tag"cursor",
am.translate(HEX_GRID_CENTER):tag"cursor_translate" ^ make_hex_cursor_node(0, COLORS.TRANSPARENT):tag"cursor",
score,
money,
wave_timer,
send_now_button,
top_right_display,
make_top_right_display_node(),
make_game_toolbelt(),
curtain
)
@ -638,17 +681,45 @@ local function game_scene()
return scene
end
-- this is a stupid name, it just returns a scene node group of hexagons in a hexagonal shape centered at 0,0, of size |radius|
-- |color_f| can be a function that takes a hex and returns a color, or just a color
-- optionally, |action_f| is a function that operates on the group node every frame
function make_hex_cursor_node(radius, color_f, action_f)
local color = type(color_f) == "userdata" and color_f or nil
local map = hex_spiral_map(vec2(0), radius)
local group = am.group()
for _,h in pairs(map) do
local hexagon = am.circle(hex_to_pixel(h, vec2(HEX_SIZE)), HEX_SIZE, color or color_f(h), 6)
group:append(hexagon)
end
if action_f then
group:action(action_f)
end
return group
end
function update_score(diff) game_state.score = game_state.score + diff end
function update_money(diff) game_state.money = game_state.money + diff end
function game_end()
local hmob = table.highest_index(game_state.mobs)
local htower = table.highest_index(game_state.towers)
local hprojectile = table.highest_index(game_state.projectiles)
gui_alert(string.format(
"\nmobs spawned: %d\ntowers built: %d\nprojectiles spawned: %d\n",
hmob, htower, hprojectile
), COLORS.WHITE, 1000)
game_state = {}
game = false
end
function game_save()
am.save_state("save", game_serialize(), "json")
alert("succesfully saved!")
gui_alert("succesfully saved!")
end
function game_init(saved_state)
@ -673,23 +744,3 @@ function game_init(saved_state)
win.scene:append(game_scene())
end
-- this is a stupid name, it just returns a scene node group of hexagons in a hexagonal shape centered at 0,0, of size |radius|
-- |color_f| can be a function that takes a hex and returns a color, or just a color
-- optionally, |action_f| is a function that operates on the group node every frame
function make_hex_cursor(radius, color_f, action_f)
local color = type(color_f) == "userdata" and color_f or nil
local map = hex_spiral_map(vec2(0), radius)
local group = am.group()
for _,h in pairs(map) do
local hexagon = am.circle(hex_to_pixel(h, vec2(HEX_SIZE)), HEX_SIZE, color or color_f(h), 6)
group:append(hexagon)
end
if action_f then
group:action(action_f)
end
return group
end

51
src/grid.lua

@ -53,6 +53,25 @@ function evenq_is_in_interactable_region(evenq)
})
end
function map_elevation_to_tile_type(elevation)
if elevation < -0.5 then -- lowest elevation
return "Water"
elseif elevation < 0 then -- med-low elevation
return "Ground - Grass"
elseif elevation < 0.5 then -- med-high elevation
return "Ground - Dirt"
elseif elevation < 1 then -- high elevation
return "Mountain"
else
-- not a normal elevation? not sure when this happens
return "n/a"
end
end
function is_water_elevation(elevation) return elevation < -0.5 end
function is_mountain_elevation(elevation) return elevation >= 0.5 end
@ -174,7 +193,22 @@ function map_elevation_color(elevation)
end
end
function make_hex_grid_scene(map)
function make_hex_node(hex, tile, color)
if not color then
local evenq = hex_to_evenq(vec2(hex.x, hex.y))
-- light shading on edge cells
local mask = vec4(0, 0, 0, math.max(((evenq.x - HEX_GRID_WIDTH/2) / HEX_GRID_WIDTH) ^ 2
, ((-evenq.y - HEX_GRID_HEIGHT/2) / HEX_GRID_HEIGHT) ^ 2))
color = map_elevation_color(tile.elevation) - mask
end
return am.translate(hex_to_pixel(vec2(hex.x, hex.y), vec2(HEX_SIZE)))
^ am.circle(vec2(0), HEX_SIZE, color, 6)
end
function make_hex_grid_scene(map, do_generate_flow_field)
-- the world's appearance relies largely on a backdrop which can be scaled in
-- tone to give the appearance of light or darkness
-- @NOTE replace this with a shader program
@ -191,16 +225,7 @@ function make_hex_grid_scene(map)
local world = am.group(neg_mask):tag"world"
for i,_ in pairs(map) do
for j,tile in pairs(map[i]) do
local evenq = hex_to_evenq(vec2(i, j))
-- light shading on edge cells
local mask = vec4(0, 0, 0, math.max(((evenq.x - HEX_GRID_WIDTH/2) / HEX_GRID_WIDTH) ^ 2
, ((-evenq.y - HEX_GRID_HEIGHT/2) / HEX_GRID_HEIGHT) ^ 2))
local color = map_elevation_color(tile.elevation) - mask
local node = am.translate(hex_to_pixel(vec2(i, j), vec2(HEX_SIZE)))
^ am.circle(vec2(0), HEX_SIZE, color, 6)
local node = make_hex_node(vec2(i, j), tile)
hex_map_set(map, i, j, {
elevation = tile.elevation,
@ -217,7 +242,9 @@ function make_hex_grid_scene(map)
^ pack_texture_into_sprite(TEXTURES.GEM1, HEX_SIZE, HEX_SIZE*1.1)
)
apply_flow_field(map, generate_flow_field(map, HEX_GRID_CENTER), world)
if do_generate_flow_field then
apply_flow_field(map, generate_flow_field(map, HEX_GRID_CENTER), world)
end
return am.translate(WORLDSPACE_COORDINATE_OFFSET) ^ world
end

11
src/gui.lua

@ -1,4 +1,15 @@
-- text popup in the middle of the screen that dissapates
function gui_alert(message, color, decay_time)
win.scene:append(
am.scale(3) ^ am.text(message, color or COLORS.WHITE)
:action(coroutine.create(function(self)
am.wait(am.tween(self, decay_time or 1, { color = vec4(0) }, am.ease_in_out))
win.scene:remove(self)
end))
)
end
function gui_numberfield(dimensions, opts)
end

2
src/hexyz.lua

@ -1,5 +1,5 @@
-- this is a single file with no dependencies which is meant to perform a bunch of mathy stuff
-- this is a single file which is meant to perform a bunch of mathy stuff
-- related to hexagons, grids of them, and pathfinding on them
--
-- it basically owes its entire existence to this resource: https://www.redblobgames.com/grids/hexagons/

73
src/map-editor.lua

@ -1,25 +1,33 @@
local map_editor_state = {
map = {},
world = {}
world = {},
selected_tile = false
}
local function deselect_tile()
map_editor_state.selected_tile = false
win.scene:remove("tile_select_box")
end
function map_editor_action()
local mouse = win:mouse_position()
local hex = pixel_to_hex(mouse - WORLDSPACE_COORDINATE_OFFSET, vec2(HEX_SIZE))
local rounded_mouse = hex_to_pixel(hex, vec2(HEX_SIZE)) + WORLDSPACE_COORDINATE_OFFSET
local evenq = hex_to_evenq(hex)
local tile = hex_map_get(map_editor_state.map, hex)
local interactable = evenq_is_in_interactable_region(evenq{ y = -evenq.y })
if win:mouse_pressed"left" then
local mouse = win:mouse_position()
local hex = pixel_to_hex(mouse - WORLDSPACE_COORDINATE_OFFSET, vec2(HEX_SIZE))
local rounded_mouse = hex_to_pixel(hex, vec2(HEX_SIZE)) + WORLDSPACE_COORDINATE_OFFSET
local evenq = hex_to_evenq(hex)
local tile = hex_map_get(map_editor_state.map, hex)
local interactable = evenq_is_in_interactable_region(evenq{ y = -evenq.y })
if win:key_pressed"escape" then
win.scene("map_editor").paused = true
win.scene:append(make_scene_menu(map_editor_scene_options))
end
if win:mouse_down"left" then
deselect_tile()
win.scene:remove("tile_select_box")
map_editor_state.selected_tile = tile
win.scene:append((
am.translate(rounded_mouse)
^ pack_texture_into_sprite(TEXTURES.SELECT_BOX, HEX_SIZE*2, HEX_SIZE*2, COLORS.SUNRAY)
@ -28,6 +36,36 @@ function map_editor_action()
)
end
if map_editor_state.selected_tile then
if win:key_pressed"a" then
-- make the selected tile 'mountain'
map_editor_state.selected_tile.elevation = 0.75
map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation)
elseif win:key_pressed"w" then
-- make the selected tile 'water'
map_editor_state.selected_tile.elevation = -0.75
map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation)
elseif win:key_pressed"d" then
-- make the selected tile 'dirt'
map_editor_state.selected_tile.elevation = 0.25
map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation)
elseif win:key_pressed"g" then
-- make the selected tile 'grass'
map_editor_state.selected_tile.elevation = -0.25
map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation)
end
-- fine tune tile's elevation with mouse wheel
local mouse_wheel_delta = win:mouse_wheel_delta().y / 100
if map_editor_state.selected_tile and mouse_wheel_delta ~= 0 then
map_editor_state.selected_tile.elevation = math.clamp(map_editor_state.selected_tile.elevation + mouse_wheel_delta, -1, 1)
map_editor_state.selected_tile.node("circle").color = map_elevation_color(map_editor_state.selected_tile.elevation)
end
end
-- update the cursor
if not interactable then
win.scene("cursor").hidden = true
@ -36,13 +74,20 @@ function map_editor_action()
win.scene("cursor").hidden = false
win.scene("cursor_translate").position2d = rounded_mouse
end
if tile then
win.scene("top_right_display").text = string.format(
"raw elevation value: %.2f\ntile type: %s",
tile.elevation, map_elevation_to_tile_type(tile.elevation)
)
end
end
function map_editor_init()
local map_editor_scene = am.group():tag"map_editor"
map_editor_state.map = default_map_editor_map()
map_editor_state.world = make_hex_grid_scene(map_editor_state.map)
map_editor_state.map = default_map_editor_map(1)
map_editor_state.world = make_hex_grid_scene(map_editor_state.map, false)
map_editor_scene:append(map_editor_state.world)
@ -50,9 +95,11 @@ function map_editor_init()
win.scene:append(map_editor_scene)
win.scene:append(
am.translate(HEX_GRID_CENTER):tag"cursor_translate"
^ make_hex_cursor(0, COLORS.TRANSPARENT):tag"cursor"
^ make_hex_cursor_node(0, COLORS.TRANSPARENT):tag"cursor"
)
win.scene:append(make_top_right_display_node())
win.scene:late_action(map_editor_action)
end

2
src/tower.lua

@ -211,7 +211,7 @@ do
end)
tower_cursors[i] = am.group{
make_hex_cursor(get_tower_range(i), vec4(0), coroutine_),
make_hex_cursor_node(get_tower_range(i), vec4(0), coroutine_),
tower_sprite
}
end

1
texture.lua

@ -26,6 +26,7 @@ TEXTURES = {
ABOUT_HEX = load_texture("res/abouthex.png"),
QUIT_HEX = load_texture("res/quithex.png"),
UNPAUSE_HEX = load_texture("res/unpausehex.png"),
MAIN_MENU_HEX = load_texture("res/mainmenuhex.png"),
CURTAIN = load_texture("res/curtain1.png"),

Loading…
Cancel
Save