You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
771 lines
26 KiB
771 lines
26 KiB
|
|
game = false -- flag to tell if there is a game running
|
|
game_state = {}
|
|
|
|
local game_scene_menu_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()
|
|
map_editor_init(game_state.map.seed)
|
|
end
|
|
},
|
|
{
|
|
texture = TEXTURES.UNPAUSE_HEX,
|
|
action = function()
|
|
win.scene("context").paused = false
|
|
win.scene:remove("menu")
|
|
end,
|
|
keys = {
|
|
"escape"
|
|
}
|
|
},
|
|
{
|
|
texture = TEXTURES.SETTINGS_HEX,
|
|
action = function()
|
|
gui_alert("not yet :)")
|
|
end
|
|
},
|
|
{
|
|
texture = TEXTURES.QUIT_HEX,
|
|
action = function()
|
|
win:close()
|
|
end,
|
|
keys = {
|
|
"f4"
|
|
}
|
|
},
|
|
false
|
|
}
|
|
|
|
local function get_initial_game_state(seed)
|
|
--local STARTING_MONEY = 75
|
|
local STARTING_MONEY = 10000
|
|
|
|
local map = random_map(seed)
|
|
local world = make_hex_grid_scene(map, true)
|
|
|
|
return {
|
|
map = map, -- map of hex coords map[x][y] to a 'tile'
|
|
world = world, -- the root scene graph node for the game 'world'
|
|
ui = nil, -- unused, root scene graph node for the 'ui' stuff
|
|
|
|
frame_start_time = 0, -- timestamp in seconds that this current frame began on
|
|
time = 0, -- real time since the *current* game started in seconds
|
|
score = 0, -- current game score
|
|
money = STARTING_MONEY, -- current money
|
|
|
|
mobs = {}, -- list of mob entities
|
|
towers = {}, -- list of tower entities
|
|
projectiles = {}, -- list of projectile entities
|
|
|
|
current_wave = 1,
|
|
time_until_next_wave = 15,
|
|
time_until_next_break = 0,
|
|
spawning = false,
|
|
spawn_chance = 0,
|
|
last_mob_spawn_time = 0,
|
|
|
|
selected_tower_type = false,
|
|
selected_toolbelt_button = false,
|
|
selected_top_right_display_type = TRDTS.SEED,
|
|
}
|
|
end
|
|
|
|
local function get_top_right_display_text(hex, evenq, centered_evenq, display_type)
|
|
local str = ""
|
|
if display_type == TRDTS.CENTERED_EVENQ then
|
|
str = centered_evenq.x .. "," .. centered_evenq.y .. " (cevenq)"
|
|
|
|
elseif display_type == TRDTS.EVENQ then
|
|
str = evenq.x .. "," .. evenq.y .. " (evenq)"
|
|
|
|
elseif display_type == TRDTS.HEX then
|
|
str = hex.x .. "," .. hex.y .. " (hex)"
|
|
|
|
elseif display_type == TRDTS.PLATFORM then
|
|
str = string.format("%s %s lang %s", am.platform, am.version, am.language())
|
|
|
|
elseif display_type == TRDTS.PERF then
|
|
str = table.tostring(am.perf_stats())
|
|
|
|
elseif display_type == TRDTS.SEED then
|
|
str = "SEED: " .. game_state.map.seed
|
|
|
|
elseif display_type == TRDTS.TILE then
|
|
str = table.tostring(hex_map_get(game_state.map, hex))
|
|
|
|
elseif display_type == TRDTS.MOUSE then
|
|
local mouse = win:mouse_position()
|
|
str = mouse.x .. "," .. mouse.y .. " (mouse)"
|
|
end
|
|
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
|
|
|
|
local function get_wave_time(current_wave)
|
|
return 45
|
|
end
|
|
|
|
local BASE_BREAK_TIME = 20
|
|
local function get_break_time(current_wave)
|
|
return BASE_BREAK_TIME - math.min(BASE_BREAK_TIME, BASE_BREAK_TIME / (1 / math.log(game_state.current_wave + 1)))
|
|
end
|
|
|
|
local function do_day_night_cycle()
|
|
-- this is a bad idea, atleast with the current bad rendering strategy of not using a single draw call
|
|
-- i get flickering as the light level increases
|
|
--local tstep = (math.sin(game_state.time * am.delta_time) + 1) / 100
|
|
--game_state.world"negative_mask".color = vec4(tstep){a=1}
|
|
end
|
|
|
|
local function game_pause()
|
|
win.scene("context").paused = true
|
|
|
|
win.scene:append(make_scene_menu(game_scene_menu_options, nil, true))
|
|
end
|
|
|
|
local function game_deserialize(json_string)
|
|
local new_game_state = am.parse_json(json_string)
|
|
|
|
if new_game_state.version ~= version then
|
|
gui_alert("loading incompatible old save data.\nstarting a fresh game instead.", nil, 10)
|
|
return get_initial_game_state()
|
|
end
|
|
|
|
new_game_state.map = random_map(new_game_state.seed)
|
|
-- after generating a random map, the random number generator is seeded with the map's seed
|
|
-- additionally, we keep track of how many times we make calls to math.random during runtime
|
|
-- in order to restore the state of the random number generator on game deserialize, we first
|
|
-- seed it with the same seed used in the original state. then, we discard N calls, where N
|
|
-- is the number of calls we counted since seeding the generator last time.
|
|
--
|
|
-- this means it's important that deserializing the rest of the game state doesn't cause any math.random() calls.
|
|
for i = 1, new_game_state.RANDOM_CALLS_COUNT do
|
|
math.random()
|
|
end
|
|
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
|
|
if t then
|
|
new_game_state.towers[i] = tower_deserialize(t)
|
|
|
|
for _,h in pairs(new_game_state.towers[i].hexes) do
|
|
local tile = hex_map_get(new_game_state.map, h.x, h.y)
|
|
tile.elevation = tile.elevation + new_game_state.towers[i].height
|
|
end
|
|
|
|
-- @STATEFUL, shouldn't be done here
|
|
new_game_state.world:append(new_game_state.towers[i].node)
|
|
end
|
|
end
|
|
|
|
-- after we have re-constituted all of the towers and modified the map's elevations accordingly,
|
|
-- we should re-calc the flow-field
|
|
apply_flow_field(new_game_state.map, generate_flow_field(new_game_state.map, HEX_GRID_CENTER), new_game_state.world)
|
|
|
|
for i,m in pairs(new_game_state.mobs) do
|
|
if m then
|
|
new_game_state.mobs[i] = mob_deserialize(m)
|
|
|
|
-- @STATEFUL, shouldn't be done here
|
|
new_game_state.world:append(new_game_state.mobs[i].node)
|
|
end
|
|
end
|
|
|
|
for i,p in pairs(new_game_state.projectiles) do
|
|
if p then
|
|
new_game_state.projectiles[i] = projectile_deserialize(p)
|
|
|
|
-- @STATEFUL, shouldn't be done here
|
|
new_game_state.world:append(new_game_state.projectiles[i].node)
|
|
end
|
|
end
|
|
|
|
return new_game_state
|
|
end
|
|
|
|
local function game_serialize()
|
|
local serialized = table.shallow_copy(game_state)
|
|
serialized.version = version
|
|
serialized.RANDOM_CALLS_COUNT = RANDOM_CALLS_COUNT
|
|
|
|
serialized.seed = game_state.map.seed
|
|
serialized.map = nil -- we re-generate the entire map from the seed on de-serialize
|
|
|
|
-- in order to serialize the game state, we have to convert all relevant userdata into
|
|
-- something else.
|
|
--
|
|
-- this practically means vectors need to become arrays of floats,
|
|
-- 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
|
|
if t then
|
|
serialized.towers[i] = tower_serialize(t)
|
|
end
|
|
end
|
|
|
|
serialized.mobs = {}
|
|
for i,m in pairs(game_state.mobs) do
|
|
if m then
|
|
serialized.mobs[i] = mob_serialize(m)
|
|
end
|
|
end
|
|
|
|
serialized.projectiles = {}
|
|
for i,p in pairs(game_state.projectiles) do
|
|
if p then
|
|
serialized.projectiles[i] = projectile_serialize(p)
|
|
end
|
|
end
|
|
|
|
return am.to_json(serialized)
|
|
end
|
|
|
|
local function deselect_tile()
|
|
win.scene:remove("tile_select_box")
|
|
end
|
|
|
|
local function game_action(scene)
|
|
game_state.frame_start_time = am.current_time()
|
|
if game_state.score < 0 then
|
|
game_end()
|
|
return true
|
|
end
|
|
|
|
game_state.time = game_state.time + am.delta_time
|
|
game_state.score = game_state.score + am.delta_time
|
|
|
|
if game_state.spawning then
|
|
game_state.time_until_next_break = game_state.time_until_next_break - am.delta_time
|
|
|
|
if game_state.time_until_next_break <= 0 then
|
|
game_state.time_until_next_break = 0
|
|
game_state.current_wave = game_state.current_wave + 1
|
|
|
|
game_state.spawning = false
|
|
|
|
game_state.time_until_next_wave = get_break_time(game_state.current_wave)
|
|
end
|
|
else
|
|
game_state.time_until_next_wave = game_state.time_until_next_wave - am.delta_time
|
|
|
|
if game_state.time_until_next_wave <= 0 then
|
|
game_state.time_until_next_wave = 0
|
|
|
|
game_state.spawning = true
|
|
|
|
-- calculate spawn chance for next wave
|
|
game_state.spawn_chance = math.log(game_state.current_wave)/80 + 0.002
|
|
|
|
game_state.time_until_next_break = get_wave_time(game_state.current_wave)
|
|
end
|
|
end
|
|
|
|
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 centered_evenq = evenq{ y = -evenq.y } - vec2(math.floor(HEX_GRID_WIDTH/2)
|
|
, math.floor(HEX_GRID_HEIGHT/2))
|
|
local tile = hex_map_get(game_state.map, hex)
|
|
|
|
local interactable = evenq_is_in_interactable_region(evenq{ y = -evenq.y })
|
|
local buildable = tower_type_is_buildable_on(hex, tile, game_state.selected_tower_type)
|
|
|
|
if win:mouse_pressed"left" then
|
|
deselect_tile()
|
|
|
|
if interactable then
|
|
if buildable then
|
|
local broken, flow_field = building_tower_breaks_flow_field(game_state.selected_tower_type, hex)
|
|
local cost = get_tower_cost(game_state.selected_tower_type)
|
|
|
|
if broken then
|
|
local node = win.scene("cursor"):child(2)
|
|
node.color = COLORS.CLARET
|
|
node:action(am.tween(0.1, { color = COLORS.TRANSPARENT3 }))
|
|
play_sfx(SOUNDS.BIRD2)
|
|
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.TRANSPARENT3 }))
|
|
play_sfx(SOUNDS.BIRD2)
|
|
gui_alert("not enough money")
|
|
|
|
else
|
|
update_money(-cost)
|
|
build_tower(hex, game_state.selected_tower_type, flow_field)
|
|
|
|
if flow_field then
|
|
apply_flow_field(game_state.map, flow_field, game_state.world)
|
|
end
|
|
end
|
|
else
|
|
-- interactable tile, but no tower type selected
|
|
-- depending on what's under the cursor, we can show some information.
|
|
local towers = towers_on_hex(hex)
|
|
local tcount = table.count(towers)
|
|
|
|
if tcount > 0 then
|
|
play_sfx(SOUNDS.SELECT1)
|
|
win.scene:remove("tile_select_box")
|
|
win.scene:append((
|
|
am.translate(rounded_mouse)
|
|
^ pack_texture_into_sprite(TEXTURES.SELECT_BOX, HEX_SIZE*2, HEX_SIZE*2, COLORS.SUNRAY)
|
|
)
|
|
:tag"tile_select_box"
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if win:mouse_pressed"middle" then
|
|
win.scene("world_scale").scale2d = vec2(1)
|
|
|
|
elseif win:key_down"lctrl" then
|
|
local mwd = win:mouse_wheel_delta()
|
|
win.scene("world_scale").scale2d = win.scene("world_scale").scale2d + vec2(mwd.y) / 100
|
|
end
|
|
|
|
if win:key_pressed"escape" then
|
|
game_pause()
|
|
|
|
elseif win:key_pressed"f1" then
|
|
game_state.selected_top_right_display_type = (game_state.selected_top_right_display_type + 1) % #table.keys(TRDTS)
|
|
|
|
elseif win:key_pressed"f2" then
|
|
game_state.world"flow_field".hidden = not game_state.world"flow_field".hidden
|
|
|
|
elseif win:key_pressed"f3" then
|
|
game_save()
|
|
|
|
elseif win:key_pressed"tab" then
|
|
if win:key_down"lshift" then
|
|
select_toolbelt_button((game_state.selected_toolbelt_button + table.count(TOWER_TYPE) - 2) % table.count(TOWER_TYPE) + 1)
|
|
else
|
|
select_toolbelt_button((game_state.selected_toolbelt_button) % table.count(TOWER_TYPE) + 1)
|
|
end
|
|
elseif win:key_pressed"1" then select_toolbelt_button( 1)
|
|
elseif win:key_pressed"2" then select_toolbelt_button( 2)
|
|
elseif win:key_pressed"3" then select_toolbelt_button( 3)
|
|
elseif win:key_pressed"4" then select_toolbelt_button( 4)
|
|
elseif win:key_pressed"q" then select_toolbelt_button( 5)
|
|
elseif win:key_pressed"w" then select_toolbelt_button( 6)
|
|
elseif win:key_pressed"e" then select_toolbelt_button( 7)
|
|
elseif win:key_pressed"r" then select_toolbelt_button( 8)
|
|
elseif win:key_pressed"a" then select_toolbelt_button( 9)
|
|
elseif win:key_pressed"s" then select_toolbelt_button(10)
|
|
elseif win:key_pressed"d" then select_toolbelt_button(11)
|
|
elseif win:key_pressed"f" then select_toolbelt_button(12)
|
|
end
|
|
|
|
do_entity_updates()
|
|
do_mob_spawning(game_state.spawn_chance)
|
|
do_day_night_cycle()
|
|
|
|
-- update the cursor
|
|
if not interactable then
|
|
win.scene("cursor").hidden = true
|
|
|
|
else
|
|
if game_state.selected_tower_type then
|
|
if buildable then
|
|
win.scene("cursor").hidden = false
|
|
|
|
else
|
|
win.scene("cursor").hidden = true
|
|
end
|
|
else
|
|
-- if we don't have a tower selected, but the tile is interactable, then show the 'select' cursor
|
|
win.scene("cursor").hidden = false
|
|
end
|
|
win.scene("cursor_translate").position2d = rounded_mouse
|
|
end
|
|
|
|
win.scene("score").text = string.format("SCORE: %.2f", game_state.score)
|
|
win.scene("money").text = string.format("MONEY: $%d", game_state.money)
|
|
win.scene("wave_timer").text = get_wave_timer_text()
|
|
win.scene("top_right_display").text = get_top_right_display_text(hex, evenq, centered_evenq, game_state.selected_top_right_display_type)
|
|
|
|
check_if_can_collect_garbage_for_free(game_state.frame_start_time, 60)
|
|
end
|
|
|
|
local function make_game_toolbelt()
|
|
local toolbelt_height = win.height * 0.07
|
|
local tower_tooltip_text_position = vec2(win.left + 10, win.bottom + toolbelt_height + 20)
|
|
local keys = { '1', '2', '3', '4', 'q', 'w', 'e', 'r', 'a', 's', 'd', 'f' }
|
|
--local keys = { '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=' }
|
|
|
|
local function get_tower_tooltip_text_node(tower_type)
|
|
local name = get_tower_name(tower_type)
|
|
local placement_rules = get_tower_placement_rules_text(tower_type)
|
|
local short_desc = get_tower_short_description(tower_type)
|
|
local cost = get_tower_cost(tower_type)
|
|
|
|
local color = COLORS.WHITE
|
|
return (am.translate(tower_tooltip_text_position)
|
|
^ am.scale(1)
|
|
^ am.group(
|
|
am.translate(0, 40)
|
|
^ am.text(string.format("%s - %s", name, short_desc), color, "left"):tag"tower_name",
|
|
|
|
am.translate(0, 20)
|
|
^ am.text(placement_rules, color, "left"):tag"tower_placement_rules",
|
|
|
|
am.translate(0, 0)
|
|
^ am.text(string.format("$%d", cost), color, "left"):tag"tower_cost"
|
|
)
|
|
)
|
|
:tag"tower_tooltip_text"
|
|
end
|
|
|
|
local padding = 12
|
|
local offset = vec2(win.left, win.bottom + padding/3)
|
|
local size = toolbelt_height - padding
|
|
local half_size = size/2
|
|
|
|
local function toolbelt_button(i)
|
|
local texture = get_tower_icon_texture(i)
|
|
local button =
|
|
am.translate(vec2(size + padding, 0) * i + offset)
|
|
^ am.group(
|
|
am.translate(0, half_size)
|
|
^ pack_texture_into_sprite(TEXTURES.BUTTON1, size, size),
|
|
|
|
am.translate(0, half_size)
|
|
^ pack_texture_into_sprite(texture, size, size),
|
|
|
|
am.translate(vec2(half_size))
|
|
^ am.group(
|
|
pack_texture_into_sprite(TEXTURES.BUTTON1, half_size, half_size, vec4(0.4, 0.4, 0.4, 1)),
|
|
am.scale(2)
|
|
^ am.text(keys[i], COLORS.WHITE)
|
|
)
|
|
)
|
|
|
|
local x1 = (size + padding) * i + offset.x - half_size
|
|
local y1 = offset.y
|
|
local x2 = (size + padding) * i + offset.x + size - half_size
|
|
local y2 = offset.y + size
|
|
local rect = { x1 = x1, y1 = y1, x2 = x2, y2 = y2 }
|
|
|
|
return button, rect
|
|
end
|
|
|
|
local toolbelt = am.group(
|
|
am.group():tag"tower_tooltip_text",
|
|
am.rect(win.left, win.bottom, win.right, win.bottom + toolbelt_height, COLORS.TRANSPARENT3)
|
|
)
|
|
:tag"toolbelt"
|
|
|
|
local tower_select_square = (
|
|
am.translate(vec2(size + padding, half_size) + offset)
|
|
^ am.rect(-size/2-3, -size/2-3, size/2+3, size/2+3, COLORS.SUNRAY)
|
|
)
|
|
:tag"toolbelt_select_square"
|
|
tower_select_square.hidden = true
|
|
toolbelt:append(tower_select_square)
|
|
|
|
local toolbelt_buttons = {}
|
|
for i = 1, #TOWER_SPECS do
|
|
local button, rect = toolbelt_button(i)
|
|
table.insert(toolbelt_buttons, { node = button, rect = rect })
|
|
toolbelt:append(button)
|
|
end
|
|
|
|
toolbelt:action(function(self)
|
|
local mouse = win:mouse_position()
|
|
|
|
if mouse.y <= (win.bottom + toolbelt_height) then
|
|
for i,b in pairs(toolbelt_buttons) do
|
|
if point_in_rect(mouse, b.rect) then
|
|
win.scene:replace("tower_tooltip_text", get_tower_tooltip_text_node(i))
|
|
|
|
if win:mouse_pressed("left") then
|
|
select_toolbelt_button(i)
|
|
end
|
|
break
|
|
end
|
|
end
|
|
else
|
|
if game_state.selected_tower_type then
|
|
win.scene:replace("tower_tooltip_text", get_tower_tooltip_text_node(game_state.selected_tower_type))
|
|
else
|
|
win.scene:replace("tower_tooltip_text", am.group():tag"tower_tooltip_text")
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- make the 'escape/pause/settings' button in the lower right
|
|
local settings_button_position = vec2(win.right - half_size - 20, win.bottom + half_size + padding/3)
|
|
local settings_button_rect = {
|
|
x1 = settings_button_position.x - size/2,
|
|
y1 = settings_button_position.y - size/2,
|
|
x2 = settings_button_position.x + size/2,
|
|
y2 = settings_button_position.y + size/2
|
|
}
|
|
toolbelt:append(
|
|
am.translate(settings_button_position)
|
|
^ am.group(
|
|
pack_texture_into_sprite(TEXTURES.BUTTON1, size, size),
|
|
pack_texture_into_sprite(TEXTURES.GEAR, size - padding, size - padding)
|
|
)
|
|
:action(function(self)
|
|
if point_in_rect(win:mouse_position(), settings_button_rect) then
|
|
if win:mouse_pressed("left") then
|
|
game_pause()
|
|
end
|
|
end
|
|
end)
|
|
)
|
|
|
|
select_tower_type = function(tower_type)
|
|
game_state.selected_tower_type = tower_type
|
|
|
|
if get_tower_spec(tower_type) then
|
|
win.scene:replace(
|
|
"toolbelt_tooltip_text",
|
|
get_tower_tooltip_text_node(tower_type)
|
|
)
|
|
|
|
local new_position = vec2((size + padding) * tower_type, size/2) + offset
|
|
if toolbelt("toolbelt_select_square").hidden then
|
|
toolbelt("toolbelt_select_square").position2d = new_position
|
|
toolbelt("toolbelt_select_square").hidden = false
|
|
|
|
else
|
|
toolbelt("toolbelt_select_square"):action(am.tween(0.1, { position2d = new_position }))
|
|
end
|
|
|
|
win.scene:replace("cursor", get_tower_cursor(tower_type))
|
|
|
|
play_sfx(SOUNDS.SELECT1)
|
|
else
|
|
deselect_tile()
|
|
|
|
-- de-selecting currently selected tower if any
|
|
toolbelt("toolbelt_select_square").hidden = true
|
|
|
|
win.scene:replace("cursor", make_hex_cursor_node(0, COLORS.TRANSPARENT3))
|
|
end
|
|
end
|
|
|
|
select_toolbelt_button = function(i)
|
|
game_state.selected_toolbelt_button = i
|
|
|
|
if get_tower_spec(i) then
|
|
select_tower_type(i)
|
|
else
|
|
select_tower_type(nil)
|
|
end
|
|
end
|
|
|
|
return toolbelt
|
|
end
|
|
|
|
local function game_scene()
|
|
local score =
|
|
am.translate(win.left + 10, win.top - 15)
|
|
^ am.text("", "left", "top"):tag"score"
|
|
|
|
local money =
|
|
am.translate(win.left + 10, win.top - 35)
|
|
^ am.text("", "left", "top"):tag"money"
|
|
|
|
local wave_timer =
|
|
am.translate(0, win.top - 20)
|
|
^ am.text(get_wave_timer_text()):tag"wave_timer"
|
|
|
|
local send_now_button_position = vec2(0, win.top - 40)
|
|
local send_now_button_dimensions = vec2(200, 20)
|
|
local send_now_button_rect = {
|
|
x1 = -send_now_button_dimensions.x/2 + send_now_button_position.x,
|
|
y1 = -send_now_button_dimensions.y/2 + send_now_button_position.y,
|
|
x2 = send_now_button_dimensions.x/2 + send_now_button_position.x,
|
|
y2 = send_now_button_dimensions.y/2 + send_now_button_position.y
|
|
}
|
|
local send_now_button =
|
|
am.translate(send_now_button_position)
|
|
^ am.text("> SEND NOW <")
|
|
:tag"send_now_button"
|
|
:action(function(self)
|
|
local mouse = win:mouse_position()
|
|
|
|
if point_in_rect(mouse, send_now_button_rect) then
|
|
self.color = COLORS.SUNRAY
|
|
|
|
if win:mouse_pressed("left") then
|
|
if game_state.spawning then
|
|
-- in this case, we don't exactly just send the next wave, we turn the current wave into the next
|
|
-- wave, and add the amount of time it would have lasted to the amount of remaining time in the
|
|
-- current wave
|
|
game_state.current_wave = game_state.current_wave + 1
|
|
|
|
-- calculate spawn chance for next wave
|
|
game_state.spawn_chance = math.log(game_state.current_wave)/100 + 0.002
|
|
|
|
game_state.time_until_next_break = game_state.time_until_next_break + get_break_time(game_state.current_wave)
|
|
|
|
play_sfx(SOUNDS.EXPLOSION4)
|
|
else
|
|
game_state.time_until_next_wave = 0
|
|
|
|
play_sfx(SOUNDS.EXPLOSION4)
|
|
end
|
|
end
|
|
else
|
|
self.color = COLORS.WHITE
|
|
end
|
|
end)
|
|
|
|
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)))
|
|
win.scene:remove(curtain)
|
|
return true
|
|
end))
|
|
|
|
local scene = am.group(
|
|
am.scale(1):tag"world_scale" ^ game_state.world,
|
|
am.translate(HEX_GRID_CENTER):tag"cursor_translate" ^ make_hex_cursor_node(0, COLORS.TRANSPARENT3),
|
|
score,
|
|
money,
|
|
wave_timer,
|
|
send_now_button,
|
|
make_top_right_display_node(),
|
|
make_game_toolbelt(),
|
|
curtain
|
|
)
|
|
:tag"game"
|
|
|
|
-- dangling actions run before the main action
|
|
scene:late_action(game_action)
|
|
|
|
--play_track(SOUNDS.MAIN_THEME)
|
|
|
|
return scene
|
|
end
|
|
|
|
-- |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, min_radius)
|
|
local color = type(color_f) == "userdata" and color_f or COLORS.TRANSPARENT3
|
|
local group = am.group()
|
|
|
|
if not min_radius then
|
|
local map = hex_spiral_map(vec2(0), radius)
|
|
|
|
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
|
|
else
|
|
for i = min_radius, radius do
|
|
local map = hex_ring_map(vec2(0), i)
|
|
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
|
|
end
|
|
end
|
|
|
|
if action_f then
|
|
group:action(action_f)
|
|
end
|
|
|
|
return group:tag"cursor"
|
|
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 or 0, htower or 0, hprojectile or 0
|
|
), COLORS.WHITE, 1000)
|
|
|
|
game_state = {}
|
|
game = false
|
|
collectgarbage("restart")
|
|
end
|
|
|
|
function game_save()
|
|
am.save_state("save", game_serialize(), "json")
|
|
gui_alert("succesfully saved!")
|
|
end
|
|
|
|
function game_init(saved_state, seed)
|
|
if saved_state then
|
|
game_state = game_deserialize(saved_state)
|
|
|
|
if not game_state then
|
|
-- failed to load a save
|
|
log("failed to load a save :(")
|
|
win.scene:append(main_scene(true, true))
|
|
return
|
|
end
|
|
|
|
-- @HACK fixes a bug where loading game state with a tower type selected,
|
|
-- but you don't have a built tower cursor node, so hovering a buildable tile throws an error
|
|
select_tower_type(nil)
|
|
else
|
|
game_state = get_initial_game_state(seed)
|
|
end
|
|
|
|
game = true
|
|
switch_context(game_scene())
|
|
|
|
collectgarbage("stop")
|
|
end
|
|
|