Skip to content

Instantly share code, notes, and snippets.

@jsimmons
Created July 20, 2018 20:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jsimmons/0e518c43205609c1f579028b214a47fe to your computer and use it in GitHub Desktop.
Save jsimmons/0e518c43205609c1f579028b214a47fe to your computer and use it in GitHub Desktop.
local sdl = require 'SDL'
local image = require 'SDL.image'
local ttf = require 'SDL.ttf'
local table, math, string = table, math, string
local status, err = sdl.init {
sdl.flags.Video
}
if not status then
error(err)
end
local required_formats = {
image.flags.PNG
}
local formats, ret, err = image.init(required_formats)
for _, v in pairs(required_formats) do
if not formats[v] then
error(err)
end
end
local status, err = ttf.init()
if not status then
error(err)
end
local conf_tile_size = 16
local conf_screen_w = 640
local conf_screen_h = 480
local conf_map_w = math.floor(conf_screen_w / conf_tile_size)
local conf_map_h = math.floor(conf_screen_h / conf_tile_size)
local conf_map_num = conf_map_w * conf_map_h
local conf_tilemap = 'data/overworld.png'
local conf_charmap = 'data/characters.png'
local conf_font = 'data/Roboto-Regular.ttf'
local conf_house_population_max = 4
local conf_move_speed = 0.1
local conf_tiles = {
'grass',
'water',
'rocks',
'wheat_0',
'wheat_1',
'wheat_2',
'wheat_3',
'wheat_4',
'gold_0',
'gold_1',
'gold_2',
'gold_3',
'gold_4',
'bridge',
}
local conf_tile_sprites = {
grass = {{x = 272, y = 464, w = 16, h = 16} , {x = 288, y = 464, w = 16, h = 16} , {x = 272, y = 480, w = 16, h = 16} , {x = 288, y = 480, w = 16, h = 16}};
water = {{x = 0 , y = 16 , w = 16, h = 16} , {x = 16 , y = 16 , w = 16, h = 16} , {x = 32 , y = 16 , w = 16, h = 16} , {x = 48 , y = 16 , w = 16, h = 16}};
rocks = {{x = 192, y = 224, w = 16, h = 16}};
building = {{x = 368, y = 16 , w = 16, h = 16}};
wheat_0 = {{x = 112, y = 560, w = 16, h = 16}};
wheat_1 = {{x = 128, y = 560, w = 16, h = 16}};
wheat_2 = {{x = 144, y = 560, w = 16, h = 16}};
wheat_3 = {{x = 160, y = 560, w = 16, h = 16}};
wheat_4 = {{x = 176, y = 560, w = 16, h = 16}};
gold_0 = {{x = 32 , y = 560, w = 16, h = 16}};
gold_1 = {{x = 48 , y = 560, w = 16, h = 16}};
gold_2 = {{x = 64 , y = 560, w = 16, h = 16}};
gold_3 = {{x = 80 , y = 560, w = 16, h = 16}};
gold_4 = {{x = 96 , y = 560, w = 16, h = 16}};
bridge = {{x = 144, y = 112, w = 16, h = 16}};
}
local conf_team_sprites = {
red = {x = 624, y = 560, w = 16, h = 16};
blue = {x = 608, y = 560, w = 16, h = 16};
}
local conf_buildings = {
'keep',
'house',
'wall',
'school',
'barracks',
'guild',
'tavern'
}
local conf_building_sprites = {
keep = {x = 208, y = 560, w = 16, h = 16};
house = {x = 224, y = 560, w = 16, h = 16};
wall = {x = 240, y = 560, w = 16, h = 16};
school = {x = 256, y = 560, w = 16, h = 16};
barracks = {x = 272, y = 560, w = 16, h = 16};
guild = {x = 288, y = 560, w = 16, h = 16};
tavern = {x = 304, y = 560, w = 16, h = 16};
}
local conf_people = {
'peasant',
'worker',
'knight',
'wizard',
}
local conf_people_sprites = {
peasant = {x = 16, y = 0, w = 16, h = 16};
worker = {x = 64, y = 0, w = 16, h = 16};
knight = {x = 112, y = 0, w = 16, h = 16};
wizard = {x = 160, y = 0, w = 16, h = 16};
}
local conf_tool_costs = {
build_wall = 1,
build_school = 10,
build_barracks = 10,
build_guild = 10,
build_bridge = 3,
delete = 1,
}
local conf_map = ([[
rrrrrrrrrrrrrrrrrrwwwwwrrrrrrrrrrrrrrrrr
r..g...........rrrwwwwrrrrgg......g....r
r.................wwwww..............2.r
rg...........i....wwwww........i.......r
r.................wwww.................r
r...i......i......wwww......i..........r
r.................wwwww...............gr
r....i............wwwww..............rrr
r..................wwww...i..........rrr
r.................wwwww.........i......r
rg......i.........wwwww...i...........gr
r.............i....wwww................r
r..................wwww...............gr
r..................wwww................r
r..................wwww.......i.......rr
rr...i............wwww................rr
rrr...........i...wwww.............i...r
r.................wwww.................r
r.................wwww...............rrr
r......i..........wwwww......i.......rrr
rr................wwwww...............rr
rr..........i.....wwwww................r
r..................wwww............i...r
rg................wwww.................r
r......i..........wwwww....i..........gr
r............i....wwwww...i............r
r.................wwwww...........i....r
r..1....rg........wwwww...gg.......r...r
r.....rrrrr..ggg..wwwww..rrr.....rrrr..r
rrrrrrrrrrrrrrrrrrwwwwwrrrrrrrrrrrrrrrrr
]]):gsub('\n','')
local g_map = {}
local g_window
local g_renderer
local g_tilemap
local g_charmap
local g_buildings = {}
local g_people = {}
local g_actions = {}
local g_bullets = {}
local g_player_1 = {
gold = 75,
population = 0,
team = 'red'
}
local g_player_2 = {
gold = 75,
population = 0,
team = 'blue'
}
local g_players = {red = g_player_1, blue = g_player_2}
local g_water_border_indexes = {}
local g_first_water = {}
local function shuffle(t)
local random = math.random
local len = #t
for i = len, 1, -1 do
local r = random(len)
t[i], t[r] = t[r], t[i]
end
return t
end
local function make_xy(index)
local x = (index - 1) % conf_map_w
local y = math.floor((index - 1) / conf_map_w)
return x + 1, y + 1
end
local function make_index(x, y)
x = math.floor(x) - 1
y = math.floor(y) - 1
return (y * conf_map_w + x) + 1
end
local function get_tile(x, y)
if x < 1 or x > conf_map_w then
return nil
end
if y < 1 or y > conf_map_h then
return nil
end
local index = make_index(x, y)
return g_map[index]
end
local function create_building(building_type, team, x, y)
local building = {
building_type = building_type,
team = team,
x = x,
y = y,
population = 1,
hp = 40
}
if building_type == 'wall' then
building.hp = 15
elseif building_type == 'keep' then
building.hp = 100
end
table.insert(g_buildings, building)
return building
end
local action_move_to
local action_fight
local action_smash
local person_wander
local person_train_worker
local person_train_warrior
local person_train_wizard
local person_farm
local person_deliver
local person_attack
local person_defend
local function person_resume(person, reason)
local status, result = coroutine.resume(person.ai, person, reason)
if not status then
error(result)
end
table.insert(g_actions, result)
end
action_move_to = function(person, target_x, target_y, timeout)
return coroutine.yield {action_type = 'move_to', person = person, x = target_x, y = target_y, timeout = timeout}
end
action_fight = function(person, target_x, target_y, timeout)
return coroutine.yield {action_type = 'fight', person = person, x = target_x, y = target_y, timeout = timeout}
end
action_smash = function(person, target_x, target_y, timeout)
return coroutine.yield {action_type = 'smash', person = person, x = target_x, y = target_y, timeout = timeout}
end
local function person_wander(person)
while true do
local x = person.x + conf_screen_w / 2 - (math.random(conf_screen_w))
local y = person.y + conf_screen_h / 2 - (math.random(conf_screen_h))
local timeout = math.random(4 * 60, 10 * 60)
action_move_to(person, x, y, timeout)
end
end
person_train_worker = function()
end
person_train_warrior = function()
end
person_train_wizard = function()
end
person_farm = function()
end
person_deliver = function()
end
person_attack = function()
end
person_defend = function()
end
local function create_person(home, x, y)
local person = {
person_type = 'peasant',
x = (x - 1) * 16,
y = (y - 1) * 16,
hp = 5,
home = home,
team = home.team,
ai = coroutine.create(person_wander),
}
table.insert(g_people, person)
person_resume(person)
return person
end
local function create_bullet(bullet_type, x, y, dx, dy)
local bullet = {
bullet_type = bullet_type,
x = x,
y = y,
dx = dx,
dy = dy,
hp = 10
}
if bullet_type == 'wizard' or bullet_type == 'smoke' then
bullet.hp = 30
elseif bullet_type == 'smack' then
bullet.hp = 3
end
table.insert(g_bullets, bullet)
return bullet
end
local function clear_tile_teams()
for i = 1, conf_map_num do
local tile = g_map[i]
local tile_type = tile.tile_type
if tile_type ~= 'water' and
tile_type ~= 'bridge' and
tile_type ~= 'rocks' and
tile_type ~= 'building' then
local x, y = make_xy(i)
if x < g_first_water[y] then
tile.team = 'red'
else
tile.team = 'blue'
end
end
end
end
local flood_fill
flood_fill = function(x, y, team, touched)
local index = make_index(x, y)
if touched[index] then return end
touched[index] = true
local tile = get_tile(x, y)
if tile == nil then return end
local tile_type = tile.tile_type
if tile_type == 'water' or
tile_type == 'bridge' or
tile_type == 'rocks' then
return
end
if tile_type ~= 'building' then
tile.team = team
end
local flood_fill = flood_fill
flood_fill(x - 1, y - 1, team, touched)
flood_fill(x + 1, y + 1, team, touched)
flood_fill(x + 1, y - 1, team, touched)
flood_fill(x - 1, y + 1, team, touched)
flood_fill(x , y - 1, team, touched)
flood_fill(x , y + 1, team, touched)
flood_fill(x - 1, y , team, touched)
return flood_fill(x + 1, y, team, touched)
end
local function recalculate_tile_teams()
clear_tile_teams()
touched = {}
for i = 1, #g_water_border_indexes do
local x, y = make_xy(g_water_border_indexes[i])
flood_fill(x, y, nil, touched)
end
end
local function add_house(team)
print('add house', team)
local keeps_and_houses = {}
for i, building in ipairs(g_buildings) do
if building.team == team and
(building.building_type == 'house' or
building.building_type == 'keep') then
table.insert(keeps_and_houses, building)
end
end
shuffle(keeps_and_houses)
local neighbours = {
{x = -1, y = -1},
{x = 1, y = 1},
{x = 1, y = -1},
{x = -1, y = 1},
{x = 0, y = -1},
{x = 0, y = 1},
{x = -1, y = 0},
{x = 1, y = 0},
}
shuffle(neighbours)
for _, building in ipairs(keeps_and_houses) do
for _, neighbour in ipairs(neighbours) do
local x = building.x + neighbour.x
local y = building.y + neighbour.y
local adjacent = get_tile(x, y)
if adjacent ~= nil and
adjacent.building == nil and
adjacent.tile_type == 'grass' then
local new_building = create_building('house', team, x, y)
adjacent.tile_type = 'building'
adjacent.building = new_building
adjacent.team = team
adjacent.index = 1
create_person(adjacent, x, y)
g_players[team].population = g_players[team].population + 1
return
end
end
end
print 'failed to add house'
end
local function grow_population(team)
local houses = {}
for i, building in ipairs(g_buildings) do
if building.team == team and
building.building_type == 'house' and
building.population < conf_house_population_max then
table.insert(houses, building)
end
end
if #houses == 0 then
return add_house(team)
end
local choice = houses[math.random(#houses)]
choice.population = choice.population + 1
create_person(choice, choice.x, choice.y)
g_players[team].population = g_players[team].population + 1
print('grew population', team, g_players[team].population)
end
local function init()
local window, err = sdl.createWindow {
title = 'CASTLE SMASHERS',
width = conf_screen_w,
height = conf_screen_h,
}
if window == nil then
error(err)
end
local renderer, err = sdl.createRenderer(window, -1, {
sdl.rendererFlags.Accelerated,
sdl.rendererFlags.PresentVSYNC,
})
if renderer == nil then
error(err)
end
local tilemap, err = image.load(conf_tilemap)
if tilemap == nil then
error(err)
end
local tilemap, err = renderer:createTextureFromSurface(tilemap)
if tilemap == nil then
error(err)
end
local charmap, err = image.load(conf_charmap)
if charmap == nil then
error(err)
end
local charmap, err = renderer:createTextureFromSurface(charmap)
if charmap == nil then
error(err)
end
local font, err = ttf.open(conf_font, 14)
if font == nil then
error(err)
end
g_window = window
g_renderer = renderer
g_tilemap = tilemap
g_charmap = charmap
g_font = font
for i = 1, conf_map_num do
local tile = {}
local tile_type = conf_map:sub(i, i)
if tile_type == '.' then
tile.tile_type = 'grass'
tile.index = math.random(#conf_tile_sprites['grass'])
elseif tile_type == 'w' then
tile.tile_type = 'water'
elseif tile_type == 'r' then
tile.tile_type = 'rocks'
elseif tile_type == 'i' then
tile.tile_type = 'wheat_4'
elseif tile_type == 'g' then
tile.tile_type = 'gold_4'
elseif tile_type == '1' then
local x, y = make_xy(i)
tile.tile_type = 'building'
tile.team = 'red'
tile.building = create_building('keep', 'red', x, y)
elseif tile_type == '2' then
local x, y = make_xy(i)
tile.tile_type = 'building'
tile.team = 'blue'
tile.building = create_building('keep', 'blue', x, y)
else
error(('invalid tile %s at %d'):format(tile_type, i))
end
g_map[i] = tile
end
for i = 1, conf_map_num do
local tile = g_map[i]
if tile.tile_type == 'water' then
local x, y = make_xy(i)
local left = get_tile(x - 1, y)
local right = get_tile(x + 1, y)
if left ~= nil then
g_first_water[y] = x
if left.tile_type == 'grass' then
table.insert(g_water_border_indexes, make_index(x - 1, y))
end
end
if right and right.tile_type == 'grass' then
table.insert(g_water_border_indexes, make_index(x + 1, y))
end
end
end
for i = 1, 2000 do
grow_population('red')
grow_population('blue')
end
recalculate_tile_teams()
g_renderer:setDrawColor(0xffffff)
end
local function person_can_walk(person, x, y)
local floor = math.floor
local team = person.team
local xm = floor((x ) / 16) + 1
local xp = floor((x + 16) / 16) + 1
local ym = floor((y ) / 16) + 1
local yp = floor((y + 16) / 16) + 1
local top_l = get_tile(xm, ym)
local top_r = get_tile(xp, ym)
local bot_l = get_tile(xm, yp)
local bot_r = get_tile(xp, yp)
if top_l == nil or top_r == nil or bot_l == nil or bot_r == nil then
return false
end
local tiles = {top_l, top_r, bot_l, bot_r}
for _, tile in ipairs(tiles) do
local tile_type = tile.tile_type
if tile_type == 'rocks' or
tile_type == 'water' or
(tile_type == 'building' and tile.team ~= team) then
return false
end
end
return true
end
local function update(frame)
local old_actions = {}
for i = 1, #g_actions do
table.insert(old_actions, table.remove(g_actions))
end
for _, action in ipairs(old_actions) do
local action_type = action.action_type
local person = action.person
local timeout = action.timeout - 1
if timeout == 0 then
person_resume(person, 'timeout')
else
action.timeout = timeout
if action_type == 'move_to' then
local x = person.x
local y = person.y
local target_x = action.x
local target_y = action.y
local dx = target_x - x
local dy = target_y - y
local d = math.sqrt(dx * dx + dy * dy)
if d < 1 then
person.x = target_x
person.y = target_y
person_resume(person, 'success')
else
dx = (dx / d)
dy = (dy / d)
local new_x = x + dx * conf_move_speed
local new_y = y + dy * conf_move_speed
if person_can_walk(person, new_x, new_y) then
person.x = new_x
person.y = new_y
table.insert(g_actions, action)
else
person_resume(person, 'failure')
end
end
end
end
end
recalculate_tile_teams()
end
local function render()
g_renderer:clear()
for i = 1, conf_map_num do
local tile = g_map[i]
local x, y = make_xy(i)
local tile_type = tile.tile_type
local index = tile.index or 1
local tile_rect = conf_tile_sprites[tile_type][index]
local dest_rect = {x = (x - 1) * 16, y = (y - 1) * 16, w = 16, h = 16}
g_renderer:copy(g_tilemap, tile_rect, dest_rect)
if tile.building ~= nil then
local building_type = tile.building.building_type
local building_rect = conf_building_sprites[building_type]
g_renderer:copy(g_tilemap, building_rect, dest_rect)
end
if tile.team ~= nil then
local team_rect = conf_team_sprites[tile.team]
g_renderer:copy(g_tilemap, team_rect, dest_rect)
end
end
for _, person in ipairs(g_people) do
local dest_rect = {x = math.floor(person.x), y = math.floor(person.y), w = 16, h = 16}
local person_rect = conf_people_sprites[person.person_type]
g_renderer:copy(g_charmap, person_rect, dest_rect)
end
for _, bullet in ipairs(g_bullets) do
end
end
math.randomseed(os.time())
init()
local game_start = os.clock()
local target_dt = 0.0016
local next_frame = game_start + target_dt
local frame = 1
local running = true
while running do
for e in sdl.pollEvent() do
if e.type == sdl.event.Quit then
running = false
elseif e.type == sdl.event.KeyDown then
if e.keysym.sym == sdl.key.Escape then
running = false
end
elseif e.type == sdl.event.KeyUp then
end
end
if os.clock() >= next_frame then
frame = frame + 1
local frame_start = os.clock()
update(frame)
render()
local frame_end = os.clock()
local frame_time = frame_end - frame_start
local frame_time_str = string.format('%.2fms', frame_time * 1000)
local textSurface, err = g_font:renderUtf8(frame_time_str, 'solid', 0xff0000)
if textSurface == nil then
error(err)
end
local textRect = textSurface:getClipRect()
local text = g_renderer:createTextureFromSurface(textSurface)
g_renderer:copy(text, textRect, {x = 0, y = 0, w = textRect.w, h = textRect.h})
g_renderer:present()
next_frame = os.clock() + target_dt
end
end
sdl.quit()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment