Created
July 20, 2018 20:04
-
-
Save jsimmons/0e518c43205609c1f579028b214a47fe to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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