diff --git a/main.lua b/main.lua index 0a0e1f6..1f60f0b 100644 --- a/main.lua +++ b/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) ) diff --git a/res/mainmenuhex.png b/res/mainmenuhex.png new file mode 100644 index 0000000..383e30e Binary files /dev/null and b/res/mainmenuhex.png differ diff --git a/src/extra.lua b/src/extra.lua index 0277579..ab79e92 100644 --- a/src/extra.lua +++ b/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 diff --git a/src/game.lua b/src/game.lua index eacd862..e2d7b66 100644 --- a/src/game.lua +++ b/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 - diff --git a/src/grid.lua b/src/grid.lua index ce140af..0737d16 100644 --- a/src/grid.lua +++ b/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 diff --git a/src/gui.lua b/src/gui.lua index f1a219c..81f8a0f 100644 --- a/src/gui.lua +++ b/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 diff --git a/src/hexyz.lua b/src/hexyz.lua index 02a1790..f3fed50 100644 --- a/src/hexyz.lua +++ b/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/ diff --git a/src/map-editor.lua b/src/map-editor.lua index d6e8214..bc2b8be 100644 --- a/src/map-editor.lua +++ b/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 diff --git a/src/tower.lua b/src/tower.lua index 6b7048c..5bb6923 100644 --- a/src/tower.lua +++ b/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 diff --git a/texture.lua b/texture.lua index 0d64996..3bb5df8 100644 --- a/texture.lua +++ b/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"),