diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..9e9c26d --- /dev/null +++ b/TODO.md @@ -0,0 +1,24 @@ + +- fix lighthouse pathing recusrive bug thing +- make more towers +- make/fix radar tower +- do waves +- make mob difficulty scale + + + +towers: +1 - wall +2 - howitzer +3 - +4 - redeye +q - +w - moat +e - +r - radar +a - +s - lighthouse + +d - not a tower, deselects current selection +f - + diff --git a/res/bagel.jpg b/res/bagel.jpg new file mode 100644 index 0000000..0d92e5b Binary files /dev/null and b/res/bagel.jpg differ diff --git a/res/satelite.png b/res/tower_radar.png similarity index 100% rename from res/satelite.png rename to res/tower_radar.png diff --git a/res/tower_radar_icon.png b/res/tower_radar_icon.png new file mode 100644 index 0000000..e8e27a1 Binary files /dev/null and b/res/tower_radar_icon.png differ diff --git a/src/extra.lua b/src/extra.lua index 743bb22..dc0c20f 100644 --- a/src/extra.lua +++ b/src/extra.lua @@ -1,4 +1,14 @@ + +-- @TODO make it work with functions that return multiple values +-- right now it discards returned values beyond the first +function fprofile(f, ...) + local t1 = am.current_time() + local result = f(...) + log("%f", am.current_time() - t1) + return result +end + function booltostring(bool) return bool and "true" or "false" end diff --git a/src/game.lua b/src/game.lua index 5b20492..ab809a9 100644 --- a/src/game.lua +++ b/src/game.lua @@ -77,7 +77,7 @@ local function can_do_build(hex, tile, tower_type) end if not tower_is_buildable_on(hex, tile, tower_type) then - local node = WIN.scene("cursor"):child(1) + local node = WIN.scene("cursor"):child(1):child(2) node.color = COLORS.CLARET node:action(am.tween(0.1, { color = COLORS.TRANSPARENT })) @@ -150,6 +150,7 @@ local function game_action(scene) if WIN:mouse_pressed"left" then if state.hot and state.selected_tower_type and can_do_build(state.hex, state.tile, state.selected_tower_type) then + update_money(-get_tower_cost(state.selected_tower_type)) build_tower(state.hex, state.selected_tower_type) end end @@ -230,7 +231,7 @@ local function make_game_toolbelt() 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 base_cost = get_tower_base_cost(tower_type) + local base_cost = get_tower_cost(tower_type) if not (name or placement_rules or short_desc or base_cost) then return am.group():tag"tower_tooltip_text" @@ -245,7 +246,7 @@ local function make_game_toolbelt() }):tag"tower_tooltip_text" end local toolbelt = am.group{ - get_tower_tooltip_text_node(state.selected_tower_type), + am.group():tag"tower_tooltip_text", am.rect(WIN.left, WIN.bottom, WIN.right, WIN.bottom + toolbelt_height, COLORS.TRANSPARENT) }:tag"toolbelt" @@ -265,19 +266,23 @@ local function make_game_toolbelt() tower_select_square.hidden = true toolbelt:append(tower_select_square) + local keys = { '1', '2', '3', '4', 'q', 'w', 'e', 'r', 'a', 's', 'd', 'f' } + -- order of this array is the order of towers on the toolbelt. local tower_type_values = { - TOWER_TYPE.REDEYE, - TOWER_TYPE.LIGHTHOUSE, TOWER_TYPE.WALL, - TOWER_TYPE.MOAT + TOWER_TYPE.HOWITZER, + TOWER_TYPE.REDEYE, + TOWER_TYPE.MOAT, + TOWER_TYPE.RADAR, + TOWER_TYPE.LIGHTHOUSE } - local keys = { '1', '2', '3', '4', 'q', 'w', 'e', 'r', 'a', 's', 'd', 'f' } for i = 1, #keys do + local icon_texture = get_tower_icon_texture(tower_type_values[i]) toolbelt:append( toolbelt_button( size, half_size, - get_tower_icon_texture(tower_type_values[i]), + icon_texture, padding, i, offset, @@ -289,7 +294,7 @@ local function make_game_toolbelt() select_tower_type = function(tower_type) state.selected_tower_type = tower_type - if TOWER_SPECS[state.selected_tower_type] then + if get_tower_spec(tower_type) then WIN.scene:replace("tower_tooltip_text", get_tower_tooltip_text_node(tower_type)) local new_position = vec2((size + padding) * tower_type, size/2) + offset @@ -314,19 +319,25 @@ local function make_game_toolbelt() end -- |color_f| can be a function that takes a hex and returns a color, or just a color -function make_hex_cursor(position, radius, color_f) +-- |action_f| should be an action that operates on the group node or nil +function make_hex_cursor(position, radius, color_f, action_f) local color = type(color_f) == "userdata" and color_f or nil local map = spiral_map(vec2(0), radius) local group = am.group() + for _,h in pairs(map) do - group:append(am.circle(hex_to_pixel(h), HEX_SIZE, color or color_f(h), 6)) + local hexagon = am.circle(hex_to_pixel(h), HEX_SIZE, color or color_f(h), 6) + group:append(hexagon) end + + if action_f then + group:action(action_f) + end + return (am.translate(position) ^ group):tag"cursor" end function game_scene() - - local score = am.translate(WIN.left + 10, WIN.top - 20) ^ am.text("", "left"):tag"score" local money = am.translate(WIN.left + 10, WIN.top - 40) ^ am.text("", "left"):tag"money" local top_right_display = am.translate(WIN.right - 10, WIN.top - 20) ^ am.text("", "right", "top"):tag"top_right_display" @@ -354,6 +365,7 @@ end function game_init() state = get_initial_game_state() + build_tower(HEX_GRID_CENTER, TOWER_TYPE.RADAR) WIN.scene:remove"game" WIN.scene:append(game_scene()) diff --git a/src/grid.lua b/src/grid.lua index cbd6aec..7573f84 100644 --- a/src/grid.lua +++ b/src/grid.lua @@ -17,7 +17,7 @@ HEX_GRID_DIMENSIONS = vec2(HEX_GRID_WIDTH, HEX_GRID_HEIGHT) -- leaving y == 0 makes this the center in hex coordinates HEX_GRID_CENTER = vec2(math.floor(HEX_GRID_WIDTH/2) , 0) - -- math.floor(HEX_GRID_HEIGHT/2)) + -- , math.floor(HEX_GRID_HEIGHT/2)) HEX_GRID_MINIMUM_ELEVATION = -1 HEX_GRID_MAXIMUM_ELEVATION = 1 @@ -76,7 +76,7 @@ end function making_hex_unwalkable_breaks_flow_field(hex, tile) local original_elevation = tile.elevation -- making the tile's elevation very large *should* make it unwalkable - tile.elevation = 10 + tile.elevation = 999 local flow_field = generate_flow_field(state.map, HEX_GRID_CENTER) local result = not hex_map_get(flow_field, 0, 0) @@ -121,8 +121,6 @@ function generate_and_apply_flow_field(map, start, world) ^ am.text(string.format("%.1f", flow.priority * 10))) else map[i][j].priority = nil - log('fire') - -- should fire exactly once end end end @@ -200,9 +198,6 @@ function random_map(seed) generate_and_apply_flow_field(map, HEX_GRID_CENTER, world) - world:append(am.translate(hex_to_pixel(HEX_GRID_CENTER)) - ^ pack_texture_into_sprite(TEXTURES.SATELLITE, HEX_PIXEL_WIDTH, HEX_PIXEL_HEIGHT)) - return map, am.translate(WORLDSPACE_COORDINATE_OFFSET) ^ world end diff --git a/src/mob.lua b/src/mob.lua index a074b64..2e134bb 100644 --- a/src/mob.lua +++ b/src/mob.lua @@ -24,7 +24,7 @@ end -- check if a the tile at |hex| is passable by |mob| function mob_can_pass_through(mob, hex) - local tile = HEX_MAP.get(hex.x, hex.y) + local tile = state.map.get(hex.x, hex.y) return tile and tile_is_medium_elevation(tile) end @@ -84,7 +84,7 @@ local function update_mob(mob, mob_index) -- figure out movement if last_frame_hex ~= mob.hex or not mob.frame_target then - local frame_target, tile = nil, nil + local frame_target, tile = false, false if mob.path then --log('A*') -- we have an explicitly stored target @@ -92,8 +92,8 @@ local function update_mob(mob, mob_index) if not path_entry then -- we should be just about to reach the target, delete the path. - mob.path = nil - mob.frame_target = nil + mob.path = false + mob.frame_target = false return end @@ -104,7 +104,7 @@ local function update_mob(mob, mob_index) if last_frame_hex ~= mob.hex and not mob_can_pass_through(mob, mob.frame_target) then log('recalc') mob.path = get_mob_path(mob, HEX_MAP, mob.hex, HEX_GRID_CENTER) - mob.frame_target = nil + mob.frame_target = false end else -- use the map's flow field - gotta find the the best neighbour @@ -139,12 +139,19 @@ local function update_mob(mob, mob_index) -- do movement if mob.frame_target then - -- this is supposed to achieve frame rate independence, but i have no idea if it actually does - -- the constant multiplier at the beginning is how many pixels we want a mob with speed 1 to move in one frame - local rate = 4 * mob.speed / state.perf.avg_fps - - mob.position = mob.position + math.normalize(hex_to_pixel(mob.frame_target) - mob.position) * rate - mob.node.position2d = mob.position + -- it's totally possible that the target we have was invalidated by a tower placed this frame, + -- or between when we last calculated this target and now + -- check for that now + if mob_can_pass_through(mob, mob.frame_target) then + -- this is supposed to achieve frame rate independence, but i have no idea if it actually does + -- the constant multiplier at the beginning is how many pixels we want a mob with speed 1 to move in one frame + local rate = 4 * mob.speed / state.perf.avg_fps + + mob.position = mob.position + math.normalize(hex_to_pixel(mob.frame_target) - mob.position) * rate + mob.node.position2d = mob.position + else + mob.frame_target = false + end else log('no target') end diff --git a/src/tower.lua b/src/tower.lua index 8c5f2c7..5f4fb56 100644 --- a/src/tower.lua +++ b/src/tower.lua @@ -3,36 +3,48 @@ TOWERS = {} TOWER_TYPE = { - REDEYE = 1, - LIGHTHOUSE = 2, - WALL = 3, + WALL = 1, + HOWITZER = 2, + -- = 3, + REDEYE = 3, + -- = 5, MOAT = 4, + -- = 7, + RADAR = 5, + -- = 9, + LIGHTHOUSE = 6 } -TOWER_SPECS = { - [TOWER_TYPE.REDEYE] = { - name = "Redeye", - placement_rules_text = "Place on mountains or on Walls", - short_description = "Long range laser tower", - texture = TEXTURES.TOWER_REDEYE, - icon_texture = TEXTURES.TOWER_REDEYE_ICON, - base_cost = 25, - }, - [TOWER_TYPE.LIGHTHOUSE] = { - name = "Lighthouse", - placement_rules_text = "Place next to - but not on - water or moats", - short_description = "Attracts and distracts mobs", - texture = TEXTURES.TOWER_LIGHTHOUSE, - icon_texture = TEXTURES.TOWER_LIGHTHOUSE_ICON, - base_cost = 25 - }, +local TOWER_SPECS = { [TOWER_TYPE.WALL] = { name = "Wall", placement_rules_text = "Place on grass or dirt", short_description = "Restricts movement", texture = TEXTURES.TOWER_WALL, icon_texture = TEXTURES.TOWER_WALL_ICON, - base_cost = 5, + cost = 10, + range = 0, + props = {} + }, + [TOWER_TYPE.HOWITZER] = { + name = "HOWITZER", + placement_rules_text = "Place on non-Water", + short_description = "Fires artillery. Range and cost increase with elevation of terrain underneath.", + texture = TEXTURES.TOWER_SHELL, + icon_texture = TEXTURES.TOWER_SHELL_ICON, + cost = 20, + range = 10, + props = {} + }, + [TOWER_TYPE.REDEYE] = { + name = "Redeye", + placement_rules_text = "Place on Mountains or on Walls", + short_description = "Long-range, single-target laser tower", + texture = TEXTURES.TOWER_REDEYE, + icon_texture = TEXTURES.TOWER_REDEYE_ICON, + cost = 20, + range = 12, + props = {} }, [TOWER_TYPE.MOAT] = { name = "Moat", @@ -40,27 +52,55 @@ TOWER_SPECS = { short_description = "Restricts movement", texture = TEXTURES.TOWER_MOAT, icon_texture = TEXTURES.TOWER_MOAT_ICON, - base_cost = 5, - } + cost = 10, + range = 0, + props = {} + }, + [TOWER_TYPE.RADAR] = { + name = "Radar", + placement_rules_text = "Place on any non-water", + short_description = "Provides information about incoming waves.", + texture = TEXTURES.TOWER_RADAR, + icon_texture = TEXTURES.TOWER_RADAR_ICON, + cost = 20, + range = 0, + props = {} + }, + [TOWER_TYPE.LIGHTHOUSE] = { + name = "Lighthouse", + placement_rules_text = "Place next to - but not on - Water or Moats", + short_description = "Attracts nearby mobs; temporarily redirects their path", + texture = TEXTURES.TOWER_LIGHTHOUSE, + icon_texture = TEXTURES.TOWER_LIGHTHOUSE_ICON, + cost = 20, + range = 8, + props = {} + }, } +function get_tower_spec(tower_type) + return TOWER_SPECS[tower_type] +end function get_tower_name(tower_type) - return TOWER_SPECS[tower_type] and TOWER_SPECS[tower_type].name + return TOWER_SPECS[tower_type].name end function get_tower_placement_rules_text(tower_type) - return TOWER_SPECS[tower_type] and TOWER_SPECS[tower_type].placement_rules_text + return TOWER_SPECS[tower_type].placement_rules_text end function get_tower_short_description(tower_type) - return TOWER_SPECS[tower_type] and TOWER_SPECS[tower_type].short_description + return TOWER_SPECS[tower_type].short_description end function get_tower_texture(tower_type) - return TOWER_SPECS[tower_type] and TOWER_SPECS[tower_type].texture + return TOWER_SPECS[tower_type].texture end function get_tower_icon_texture(tower_type) return TOWER_SPECS[tower_type] and TOWER_SPECS[tower_type].icon_texture end -function get_tower_base_cost(tower_type) - return TOWER_SPECS[tower_type] and TOWER_SPECS[tower_type].base_cost +function get_tower_cost(tower_type) + return TOWER_SPECS[tower_type].cost +end +function get_tower_range(tower_type) + return TOWER_SPECS[tower_type].range end local function make_tower_sprite(tower_type) @@ -70,8 +110,28 @@ end do local tower_cursors = {} for _,i in pairs(TOWER_TYPE) do - tower_cursors[i] = make_tower_sprite(i) - tower_cursors[i].color = COLORS.TRANSPARENT + local tower_sprite = make_tower_sprite(i) + tower_sprite.color = COLORS.TRANSPARENT + + local coroutine_ = coroutine.create(function(node) + local flash_on = {} + local flash_off = {} + while true do + for _,n in node:child_pairs() do + table.insert(flash_on, am.tween(n, 1, { color = vec4(0.4) })) + table.insert(flash_off, am.tween(n, 1, { color = vec4(0) })) + end + am.wait(am.parallel(flash_on)) + am.wait(am.parallel(flash_off)) + flash_on = {} + flash_off = {} + end + end) + + tower_cursors[i] = am.group{ + make_hex_cursor(vec2(0), get_tower_range(i), vec4(0), coroutine_), + tower_sprite + } end function get_tower_cursor(tower_type) @@ -85,18 +145,42 @@ local function make_tower_node(tower_type) elseif tower_type == TOWER_TYPE.LIGHTHOUSE then return am.group( - make_tower_sprite(tower_type) + make_tower_sprite(tower_type), + am.particles2d{ + source_pos = vec2(0, 12), + source_pos_var = vec2(2), + start_size = 1, + start_size_var = 1, + end_size = 1, + end_size_var = 1, + angle = 0, + angle_var = math.pi, + speed = 1, + speed_var = 2, + life = 10, + life_var = 1, + start_color = COLORS.WHITE, + start_color_var = vec4(0.1, 0.1, 0.1, 1), + end_color = COLORS.SUNRAY, + end_color_var = vec4(0.1), + emission_rate = 4, + start_particles = 4, + max_particles = 200 + } ) elseif tower_type == TOWER_TYPE.WALL then return am.circle(vec2(0), HEX_SIZE, COLORS.VERY_DARK_GRAY, 6) elseif tower_type == TOWER_TYPE.MOAT then return am.circle(vec2(0), HEX_SIZE, (COLORS.WATER){a=1}, 6) + + elseif tower_type == TOWER_TYPE.RADAR then + return make_tower_sprite(tower_type) end end function can_afford_tower(money, tower_type) - local cost = get_tower_base_cost(tower_type) + local cost = get_tower_cost(tower_type) return (money - cost) >= 0 end @@ -109,7 +193,6 @@ local function get_tower_update_function(tower_type) end end - function towers_on_hex(hex) local t = {} for tower_index,tower in pairs(TOWERS) do @@ -120,16 +203,11 @@ function towers_on_hex(hex) return t end - function tower_on_hex(hex) - return table.find(TOWERS, function(tower) - return tower.hex == hex - end) + return table.find(TOWERS, function(tower) return tower.hex == hex end) end function tower_is_buildable_on(hex, tile, tower_type) - if hex == HEX_GRID_CENTER then return false end - local blocking_towers = towers_on_hex(hex) local blocking_mobs = mobs_on_hex(hex) @@ -209,18 +287,22 @@ function update_tower_lighthouse(tower, tower_index) for _,m in pairs(mobs) do if not m.path then - local path, made_it = Astar(HEX_MAP, tower.hex, m.hex, grid_heuristic, grid_cost) + -- @TODO only attract the mob if its frame target (direction vector) + -- is within some angle range...? if the mob is heading directly away from the tower, then + -- the lighthouse shouldn't do much + + local path, made_it = Astar(state.map, tower.hex, m.hex, grid_heuristic, grid_cost) if made_it then m.path = path local area = spiral_map(tower.hex, tower.range) for _,h in pairs(area) do - local node = HEX_MAP[h.x][h.y].node"circle" + local node = state.map[h.x][h.y].node"circle" local initial_color = node.color local d = math.distance(h, tower.hex) - local target_color = COLORS.SUNRAY{ a = 1/(d/tower.range) } + local target_color = COLORS.SUNRAY{ a = 1/(d/tower.range) + 0.9 } node:late_action(am.series{ am.tween(node, 0.3, { color = target_color }), am.tween(node, 0.3, { color = initial_color }) @@ -239,62 +321,29 @@ function make_and_register_tower(hex, tower_type) get_tower_update_function(tower_type) ) - local need_to_regen_flow_field = true tower.type = tower_type - if tower_type == TOWER_TYPE.REDEYE then - tower.range = 7 - tower.last_shot_time = tower.TOB - tower.target_index = false + tower.cost = get_tower_cost(tower_type) + tower.range = get_tower_range(tower_type) + tower.last_shot_time = tower.TOB -- a tower has never shot if its TOB == its last_shot_time - state.map[hex.x][hex.y].elevation = 2 + for k,v in pairs(TOWER_SPECS[tower_type].props) do + tower[k] = v + end + if tower_type == TOWER_TYPE.REDEYE then elseif tower_type == TOWER_TYPE.LIGHTHOUSE then - tower.range = 5 tower.perimeter = ring_map(tower.hex, tower.range) - --[[ - tower.node:append( - am.particles2d{ - source_pos = vec2(0, 12), - source_pos_var = vec2(2), - start_size = 1, - start_size_var = 1, - end_size = 1, - end_size_var = 1, - angle = 0, - angle_var = math.pi, - speed = 1, - speed_var = 2, - life = 10, - life_var = 1, - start_color = COLORS.WHITE, - start_color_var = vec4(0.1, 0.1, 0.1, 1), - end_color = COLORS.SUNRAY, - end_color_var = vec4(0.1), - emission_rate = 4, - start_particles = 4, - max_particles = 200 - } - ) - ]] - -- @HACK - need_to_regen_flow_field = false elseif tower_type == TOWER_TYPE.WALL then - state.map[hex.x][hex.y].elevation = 1 - elseif tower_type == TOWER_TYPE.MOAT then - state.map[hex.x][hex.y].elevation = -1 - end - - if need_to_regen_flow_field then - generate_and_apply_flow_field(state.map, HEX_GRID_CENTER, state.world) + elseif tower_type == TOWER_TYPE.RADAR then end + generate_and_apply_flow_field(state.map, HEX_GRID_CENTER, state.world) register_entity(TOWERS, tower) end function build_tower(hex, tower_type) - update_money(-get_tower_base_cost(tower_type)) make_and_register_tower(hex, tower_type) vplay_sfx(SOUNDS.EXPLOSION4) end diff --git a/texture.lua b/texture.lua index 999d8f0..6566d4b 100644 --- a/texture.lua +++ b/texture.lua @@ -1,24 +1,37 @@ +local function load_texture(filepath) + local status, texture = pcall(am.texture2d, filepath) + + if status then + return texture + else + return am.texture2d("res/bagel.jpg") + end +end TEXTURES = { - LOGO = am.texture2d("res/logo.png"), - - BUTTON1 = am.texture2d("res/button1.png"), - WIDER_BUTTON1 = am.texture2d("res/wider_button1.png"), - TAB_ICON = am.texture2d("res/tab_icon.png"), - SATELLITE = am.texture2d("res/satelite.png"), - - TOWER_REDEYE = am.texture2d("res/tower_redeye.png"), - TOWER_LIGHTHOUSE = am.texture2d("res/tower_lighthouse.png"), - TOWER_WALL = am.texture2d("res/tower_wall.png"), - TOWER_MOAT = am.texture2d("res/tower_moat.png"), - - TOWER_REDEYE_ICON = am.texture2d("res/tower_redeye_icon.png"), - TOWER_LIGHTHOUSE_ICON = am.texture2d("res/tower_lighthouse_icon.png"), - TOWER_WALL_ICON = am.texture2d("res/tower_wall_icon.png"), - TOWER_MOAT_ICON = am.texture2d("res/tower_moat_icon.png"), - - MOB_BEEPER = am.texture2d("res/mob_beeper.png"), + LOGO = load_texture("res/logo.png"), + + BUTTON1 = load_texture("res/button1.png"), + WIDER_BUTTON1 = load_texture("res/wider_button1.png"), + TAB_ICON = load_texture("res/tab_icon.png"), + + -- tower stuff + TOWER_WALL = load_texture("res/tower_wall.png"), + TOWER_WALL_ICON = load_texture("res/tower_wall_icon.png"), + TOWER_HOWITZER = load_texture("res/tower_howitzer.png"), + TOWER_HOWITZER_ICON = load_texture("res/tower_howitzer_icon.png"), + TOWER_REDEYE = load_texture("res/tower_redeye.png"), + TOWER_REDEYE_ICON = load_texture("res/tower_redeye_icon.png"), + TOWER_MOAT = load_texture("res/tower_moat.png"), + TOWER_MOAT_ICON = load_texture("res/tower_moat_icon.png"), + TOWER_RADAR = load_texture("res/tower_radar.png"), + TOWER_RADAR_ICON = load_texture("res/tower_radar_icon.png"), + TOWER_LIGHTHOUSE = load_texture("res/tower_lighthouse.png"), + TOWER_LIGHTHOUSE_ICON = load_texture("res/tower_lighthouse_icon.png"), + + -- mob stuff + MOB_BEEPER = load_texture("res/mob_beeper.png"), } function pack_texture_into_sprite(texture, width, height)