From 570af1b536b68e6be753d9e4ae219282f29e42da Mon Sep 17 00:00:00 2001 From: Nicholas Hayashi Date: Sat, 9 Jan 2021 21:31:10 -0500 Subject: [PATCH] stuff --- NOTES.md | 35 ++++++++++ color.lua | 18 ++--- main.lua | 27 ++++++-- res/moat1.png | Bin 0 -> 1819 bytes res/wall_closed.png | Bin 2004 -> 1949 bytes src/entity.lua | 118 ++++++++++++++++---------------- src/extra.lua | 3 + src/geometry.lua | 13 +++- src/grid.lua | 39 ++++++----- src/hexyz.lua | 107 ++++++++++++++++++++++------- src/mob.lua | 161 ++++++++++++++++++++++++++------------------ src/projectile.lua | 92 +++++++++++++++++-------- src/tower.lua | 126 +++++++++++++++++++++++----------- texture.lua | 1 + 14 files changed, 492 insertions(+), 248 deletions(-) create mode 100644 NOTES.md create mode 100644 res/moat1.png diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..6fb7f9d --- /dev/null +++ b/NOTES.md @@ -0,0 +1,35 @@ + + +todoooos & notes + +@TODO test optimizing pathfinding via breadth first search/djikstra + +i think i want more or less no such thing as 'impassable' terrain + +all mobs always can always traverse everything, though they initially will give the impression that certain tiles are impassable by prefering certain tiles. + +the illusion is likely to be broken when you attempt to fully wall-off an area, and mobs begin deciding to climb over mountains or swim through lakes. this will come at great cost to them, but they will be capable of it + +MAP RESOURCES +- spawn diamonds or special floating resources that give you bonuses for building on, whether it's score, money, or boosting the effectiveness of the tower you place on top, etc. +- killing certain mobs may cause these resources to spawn on the hex they died on + + +towers: +0 - wall + some fraction of the height of the tallest mountain + makes mob pathing more difficult + + upgrades: + - +height - making the tower taller makes it more difficult/costly for mobs to climb over it + - spikes - mobs take damage when climbing + +1 - moat + some fraction of the depth of the deepest lake + makes mob pathing more difficult + + upgrades: + - +depth - making the moat deeper makes it more difficult/costly for mobs to swim through it + - alligators - mobs take damage while swimming + + diff --git a/color.lua b/color.lua index 582d3fd..1c60a36 100644 --- a/color.lua +++ b/color.lua @@ -6,24 +6,26 @@ COLORS = { -- tones WHITE = vec4(0.8, 0.8, 0.7, 1), BLACK = vec4(0, 0, 0.02, 1), + VERY_DARK_GRAY = vec4(45/255, 45/255, 35/255, 1), TRUE_BLACK = vec4(0, 0, 0, 1), -- non-standard hues WATER = vec4(0.12, 0.3, 0.3, 1), GRASS = vec4(0.10, 0.25, 0.10, 1), DIRT = vec4(0.22, 0.20, 0.10, 1), - MOUNTAIN = vec4(0.45, 0.30, 0.20, 1), + MOUNTAIN = vec4(0.95, 0.30, 0.20, 1), -- hues - MAGENTA = vec4(1, 0, 1, 1), - TEAL = vec4(16/255, 126/255, 124/244, 1), - YALE_BLUE = vec4(4/255, 75/255, 127/255, 1), - OLIVE = vec4(111/255, 124/254, 18/255, 1), + SUNRAY = vec4(228/255, 179/255, 99/255, 1), + MAGENTA = vec4( 1, 0, 1, 1), + TEAL = vec4( 16/255, 126/255, 124/244, 1), + YALE_BLUE = vec4( 4/255, 75/255, 127/255, 1), + OLIVE = vec4(111/255, 124/254, 18/255, 1), LIGHT_CYAN = vec4(224/255, 251/255, 252/255, 1), PALE_SILVER = vec4(193/255, 178/255, 171/255, 1), - CLARET = vec4(139/255, 30/255, 63/255, 1), - BISTRO = vec4(73/255, 44/255, 29/255, 1), - DEEP_SPACE_SPARKLE = vec4(61/255, 90/255, 108/255, 1), + CLARET = vec4(139/255, 30/255, 63/255, 1), + BISTRO = vec4( 73/255, 44/255, 29/255, 1), + DEEP_SPACE_SPARKLE = vec4( 61/255, 90/255, 108/255, 1), WHEAT = vec4(225/255, 202/255, 150/255, 1) } diff --git a/main.lua b/main.lua index 8770881..b5e77a4 100644 --- a/main.lua +++ b/main.lua @@ -25,7 +25,8 @@ WIN = am.window{ width = 1920, height = 1080, title = "hexyz", - + highdpi = true, + letterbox = true } OFF_SCREEN = vec2(WIN.width * 2) -- arbitrary pixel position that is garunteed to be off screen @@ -40,6 +41,9 @@ MOUSE = false -- position of the mouse at the start of every frame, if an action MUSIC_VOLUME = 0.1 SFX_VOLUME = 0.1 +-- game stuff +SELECTED_TOWER_TYPE = TOWER_TYPE.REDEYE + -- top right display types local TRDTS = { NOTHING = -1, @@ -52,6 +56,11 @@ local TRDTS = { } local TRDT = TRDTS.SEED +local function select_tower(tower_type) + SELECTED_TOWER_TYPE = tower_type + WIN.scene"tower_tooltip".text = tower_type_tostring(tower_type) +end + local function game_action(scene) if SCORE < 0 then game_end() end @@ -72,12 +81,18 @@ local function game_action(scene) if WIN:mouse_pressed"left" then if hot and is_buildable(hex, tile, nil) then - make_and_register_tower(hex) + make_and_register_tower(hex, SELECTED_TOWER_TYPE) end end - if WIN:key_pressed"escape" then game_end() - elseif WIN:key_pressed"f1" then TRDT = (TRDT + 1) % #table.keys(TRDTS) + if WIN:key_pressed"escape" then + game_end() + + elseif WIN:key_pressed"f1" then + TRDT = (TRDT + 1) % #table.keys(TRDTS) + + elseif WIN:key_pressed"tab" then + select_tower((SELECTED_TOWER_TYPE + 1) % #table.keys(TOWER_TYPE)) end if tile and hot then @@ -129,7 +144,10 @@ end local function toolbelt() local toolbelt_height = hex_height(HEX_SIZE) * 2 + local tower_tooltip = am.translate(WIN.left + 10, WIN.bottom + toolbelt_height + 10) + ^ am.text(tower_type_tostring(SELECTED_TOWER_TYPE), "left"):tag"tower_tooltip" local toolbelt = am.group{ + tower_tooltip, am.rect(WIN.left, WIN.bottom, WIN.right, WIN.bottom + toolbelt_height, COLORS.TRANSPARENT) } @@ -154,6 +172,7 @@ function game_scene() local coords = am.translate(WIN.right - 10, WIN.top - 20) ^ am.text("", "right", "top"):tag"coords" local hex_cursor = am.circle(OFF_SCREEN, HEX_SIZE, COLORS.TRANSPARENT, 6):tag"hex_cursor" + 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))) diff --git a/res/moat1.png b/res/moat1.png new file mode 100644 index 0000000000000000000000000000000000000000..7a9701cf6076bfa289436ef16a7fe76fbfd8a3c6 GIT binary patch literal 1819 zcmaJ?`#;l*8=u?B{jgEOPHdkRzUWY{>4M#KSXnW2P;(znGtK3sobS$YnKVXN5nqgq zxh$6~BJ5>ED#Z~-7uQ^tS#gBE^ADWo^?E<==k|J@=a=X8(9h#A8mjuLAP`8y4U5Ff zn7#+)A7ok&FzA<&3K{Dg3j(P@_8#`3Ly+K>w}1U%J8pkhabJo+1*~__uq?)(6I@**4i9B-@kef)(t?w0M6sVn9sD1) z?d|#RGU%SBTKe}A6;uA|$GK_Cycazpk?1N%YpzNw^Gplpp!}7V%mwjzV;y5~9ckN)=J$@ zJJ*rlcLJfu@@9&CtvcjepBNIM=01w&2kD44Zm z6s>>jg#W%cNE?_=40ZQsKh6}ubG*mDxiC$L;q zFPIgR@N>;9ddiKA52~Lf!ea__O3{F=WVJOYNxskrHT0e&d3}`#+;q=44+r;)VrQZq zqlhV3E-4U`f*li_S(9 zbLs3w3p(#*Lw&A0fX8f39g~N=!zPgd^oO=P=>Uuu+Dthg<(=~9KKUer)T_kA;R z8*(^5U*OXa2Kh>T%=tN`GO`kiK>psX_-zbPSK;_@~^4KyCCjghFQ_) zO8=>H%94(zHP7eMo=zqZoP;~sFH$uS;9Y!g(^R~0bKv?53ne-2VzmMFo$2@0iZ*p< zlV3ejSI269bbPQ+p}Qw<{PZKVQP!aNXZ9b8oj790jNR_oyq%|qOV5XQDhpOk5F3*& zlq~!-Nqv&HF}=t-EicE+Uwb()=-_ENFfhVq5tgdbo|}D7Z17#Vpjfs|X-C}S26%qq z^lvE-;FR&++K6!}8ek|8w2YOdu7zF&;V@@F^IUb`5@f{0=fO#`aE#d_2+-W0u9YYj za$)Y*>`Xa^Bf)V#hBRMjuXT;(D-;pF28qvvuRR0*_uxB1<3B7F)=V?cjlwy}%Ah~F zaYGXkcea(rHK#K-R1O=_u~ERy8OCisQCSg>2`C9Bv_=6xJq{Y4QmD!q;64T*^xJ|w zn>z&t5w%KTnG+>e$z2;^maxTUN?WStq*XwqHa(;@W3%JAHOsF zlMHg7PK_-a_V+2U@j^ zu+IW5nAiw2^QC_`@^aw3dIS**G1+HU88rdtwe&5Oy8zL@Q3v-LlJ+N0eA;Y0(fiBL zunpYF#R zvDGNzC1||JiXrHxTgr1ZTn2UrQRdl{gg~M4?0?q9g%}*ghQIccuG01Dib&!NG@Fva z%Gutc9{ZTndECKvj&*8u88u{D7UN2ynnPDYM=F$=SY|6Zm2EHC(wPOVJdDj%6&m(z iaEg`x{YA*KMv!{Bbv-?a)EZX0SL}wuA?sW&)BX<_vpc8& literal 0 HcmV?d00001 diff --git a/res/wall_closed.png b/res/wall_closed.png index befe8fd287b8dc4c73ad61ca66620400e3fa59b8..99ee0e0c1cad24d0751faad9a2919f0196cdef60 100644 GIT binary patch literal 1949 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 literal 2004 zcmZ`)_gfN(8^=>KH5@rQg1n22#Cb~L&aVSU4$R7HkeUKkLQ0;4B+b4#!a*pZW@@O3 zCayC9XQ`y(EX_p6~=$&vhy6@!7Yq3~-WEbg9PsBTx zc$dyN7A)!QRl99g3Q#tOyEr0O`e$gZ`zHLLab`o2j)QT5cC`@Ec{KMz)669tlaZ9~ zGy&aw1KHYH3J^!&;`~iQzN)08(0Iwmt2_1K?A^i8g%35lG1<M>lhGl&)W#lr1YS_u@$9bb&UnM3vn%~Bxa|-dT9o%2C!yq5>(vvU>ejaNu zNf2qH{K@Ue4X)5H4q7b_0&==xsGEK~DDRi9HqI4F4@~EgU!4CHcnzmmleuvD0p_Zb zmyU~cl({tw$l(;Xz=$MsXV$Mu-%Z-?lNPg9-j}Cck{42GHnC_z}x!&j1buwM_ z`m*6ZZOzROnbiuut=Ub18WnMA4h_shI;7zSmBlP3F@0P?mc);`ouZ#Y90&~ftIRV^ z`isGCmNrf*{l zK@|6hG&1MiksloV$IU1(&s{S%0{Q9zewzZ_*5+-XzH&R+E#j=WC&X;H85iHN>{je zYTIA?yFIrs);fCffjMTjqMlLJ9TR0$x@~H14o?wA^cWjz%}-RUe;ZfD&c4q=oDA6r z>H4`3X2XxAj<&Q-#MxQ}qk>-{bHKn12^oOODygFPdh7_lsW<3c7R5qwY;qg01t^gLpMo=9_s}iJ88*n-v5+d zC#<(QL6GJ2T=Yqi!Y~MuY@mLlF4DU;Ln|Fh*YK>SqRBtH(FVB!L^0R!&q%1DBHaip;%rA0vBTaP*DN(On?i zZdeV-F;?cT=4n%IKURRdF;0bK@NW9qufp7IV~!0YM|x-1+B}Lk+?q>D;O18^deDS( zE%`a}X(J9EJ0NV-l#XoCq}uBct?TJUW(2RZI*DCZOe{4HKmSr#N( z-O7IdQlk1o@i>ir;8evd!6}TDXq98KeLws6t~-f>-g7R=ExAVLIFUP>Qz5oCK diff --git a/src/entity.lua b/src/entity.lua index 240eafd..b7710f9 100644 --- a/src/entity.lua +++ b/src/entity.lua @@ -1,77 +1,81 @@ -ENTITY_TYPE = { - ENTITY = 0, - MOB = 1, - TOWER = 2, - PROJECTILE = 3 -} +MOBS = {} +TOWERS = {} +PROJECTILES = {} -ENTITIES = {} --- entity structure: --- { --- TOB - number - time of birth, const --- hex - vec2 - current occupied hex, if any --- position - vec2 - current pixel position of it's translate (forced parent) node --- update - function - runs every frame with itself and its index as an argument --- node - node - scene graph node --- } --- --- mob(entity) structure: --- { --- path - 2d table - map of hexes to other hexes, forms a path --- speed - number - multiplier on distance travelled per frame, up to the update function to use correctly --- bounty - number - score bonus you get when this mob is killed --- hurtbox_radius - number - --- } --- --- tower(entity) structure: --- { --- -- @NOTE these should probably be wrapped in a 'weapon' struct or something, so towers can have multiple weapons --- range - number - distance it can shoot --- last_shot_time - number - timestamp (seconds) of last time it shot --- target_index - number - index of entity it is currently shooting --- } --- --- bullet/projectile structure --- { --- vector - vec2 - normalized vector of the current direction of this projectile --- velocity - number - multplier on distance travelled per frame --- damage - number - guess --- hitbox_radius - number - hitboxes are circles --- } --- -function make_and_register_entity(type_, hex, node, update) +--[[ +entity structure: +{ + TOB - number - time of birth, const + hex - vec2 - current occupied hex, if any + position - vec2 - current pixel position of it's translate (forced parent) node + update - function - runs every frame with itself and its index as an argument + node - node - scene graph node +} +--]] +function make_basic_entity(hex, node, update, position) local entity = {} - entity.type = type_ entity.TOB = TIME - entity.hex = hex - entity.position = hex_to_pixel(hex) - entity.update = update or function() log("unimplemented update function!") end + + -- usually you'll provide a hex and not a position, and the entity will spawn in the center + -- of the hex. if you want an entity to exist not at the center of a hex, you can provide a + -- pixel position instead + if position then + entity.position = position + entity.hex = pixel_to_hex(entity.position) + else + entity.hex = hex + entity.position = hex_to_pixel(hex) + end + + entity.update = update entity.node = am.translate(entity.position) ^ node - table.insert(ENTITIES, entity) - WORLD:append(entity.node) return entity end -function delete_all_entities() - for index,entity in pairs(ENTITIES) do - delete_entity(index) - end +function register_entity(t, entity) + table.insert(t, entity) + WORLD:append(entity.node) +end + +-- |t| is the source table, probably MOBS, TOWERS, or PROJECTILES +function delete_entity(t, index) + if not t then log("splat!") end - ENTITIES = {} + WORLD:remove(t[index].node) + t[index] = false -- leave empty indexes so other entities can learn that this entity was deleted end -function delete_entity(index) - WORLD:remove(ENTITIES[index].node) - ENTITIES[index] = nil -- leave empty indexes so other entities can learn that this entity was deleted +function delete_all_entities() + for mob_index,mob in pairs(MOBS) do + delete_entity(MOBS, mob_index) + end + for tower_index,tower in pairs(TOWERS) do + delete_entity(TOWERS, tower_index) + end + for projectile_index,projectile in pairs(PROJECTILES) do + delete_entity(PROJECTILES, projectile_index) + end end function do_entity_updates() - for index,entity in pairs(ENTITIES) do - entity.update(entity, index) + for mob_index,mob in pairs(MOBS) do + if mob and mob.update then + mob.update(mob, mob_index) + end + end + for tower_index,tower in pairs(TOWERS) do + if tower and tower.update then + tower.update(tower, tower_index) + end + end + for projectile_index,projectile in pairs(PROJECTILES) do + if projectile and projectile.update then + projectile.update(projectile, projectile_index) + end end end diff --git a/src/extra.lua b/src/extra.lua index bc83b9d..63043cf 100644 --- a/src/extra.lua +++ b/src/extra.lua @@ -1,4 +1,7 @@ +function booltostring(bool) + return bool and "true" or "false" +end function math.wrapf(float, range) return float - range * math.floor(float / range) diff --git a/src/geometry.lua b/src/geometry.lua index 414f763..1d6b7ba 100644 --- a/src/geometry.lua +++ b/src/geometry.lua @@ -1,7 +1,18 @@ function circles_intersect(center1, center2, radius1, radius2) - return (((center1.x - center2.x)^2 + (center1.y - center2.y)^2)^0.5) <= (radius1 + radius2) + local c1, c2, r1, r2 = center1, center2, radius1, radius2 + local d = math.distance(center1, center2) + local radii_sum = r1 + r2 + -- touching + if d == radii_sum then return 1 + + -- not touching or intersecting + elseif d > radii_sum then return false + + -- intersecting + else return 2 + end end function point_in_rect(point, rect) diff --git a/src/grid.lua b/src/grid.lua index c9484a1..818a3ab 100644 --- a/src/grid.lua +++ b/src/grid.lua @@ -1,6 +1,9 @@ +-- distance from hex centerpoint to any vertex HEX_SIZE = 20 +HEX_PIXEL_SIZE = vec2(hex_width(HEX_SIZE, ORIENTATION.FLAT) + , hex_height(HEX_SIZE, ORIENTATION.FLAT)) -- with 1920x1080, this is the minimal dimensions to cover the screen (65x33) -- @NOTE added 2 cell padding, because we terraform the very outer edge and it looks ugly @@ -10,26 +13,29 @@ HEX_GRID_HEIGHT = 35 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_DIMENSIONS.x/2), 0) +HEX_GRID_CENTER = vec2(math.floor(HEX_GRID_WIDTH/2) + , 0) + -- math.floor(HEX_GRID_HEIGHT/2)) -- index is hex coordinates [x][y] -- { { elevation, node, etc. } } HEX_MAP = {} -local function grid_pixel_dimensions() +do local hhs = hex_horizontal_spacing(HEX_SIZE) local hvs = hex_vertical_spacing(HEX_SIZE) -- number of 'spacings' on the grid == number of cells - 1 - return vec2((HEX_GRID_WIDTH - 1) * hhs - , (HEX_GRID_HEIGHT - 1) * hvs) + GRID_PIXEL_DIMENSIONS = vec2((HEX_GRID_WIDTH - 1) * hhs + , (HEX_GRID_HEIGHT - 1) * hvs) end -GRID_PIXEL_DIMENSIONS = grid_pixel_dimensions() +-- amulet puts 0,0 in the middle of the screen +-- transform coordinates by this to pretend 0,0 is elsewhere WORLDSPACE_COORDINATE_OFFSET = -GRID_PIXEL_DIMENSIONS/2 +-- the outer edges of the map are not interactable, most action occurs in the center HEX_GRID_INTERACTABLE_REGION_PADDING = 4 - function is_interactable(tile, evenq) return point_in_rect(evenq, { x1 = HEX_GRID_INTERACTABLE_REGION_PADDING, @@ -56,18 +62,17 @@ function color_at(elevation) elseif elevation < 1 then -- high elevation return COLORS.MOUNTAIN{ ra = elevation } - - else - log('bad elevation'); return vec4(0) end end --- hex_neighbours returns all coordinate positions that could be valid for a map extending infinite in all directions --- grid_neighbours only gets you the neighbours that are actually in the grid -function grid_neighbours(map, hex) - return table.filter(hex_neighbours(hex), function(_hex) - return map.get(_hex.x, _hex.y) - end) +function grid_heuristic(source, target) + return math.distance(source, target) +end + +function grid_cost(from, to) + local t1, t2 = HEX_MAP.get(from.x, from.y), HEX_MAP.get(to.x, to.y) + --local baseline = math.log(math.abs(1 - t1.elevation) + math.abs(1 - t2.elevation)) + return math.abs(t1.elevation - t2.elevation) --+ baseline end function random_map(seed) @@ -86,9 +91,9 @@ function random_map(seed) else -- scale noise to be closer to 0 the closer we are to the center - -- @NOTE i don't know if this 100% of the time makes the center tile passable, but it probably does 99.9+% of the time + -- @NOTE i don't know if this 100% of the time makes the center tile passable, but it seems to 99.9+% of the time local nx, ny = evenq.x/HEX_GRID_WIDTH - 0.5, -evenq.y/HEX_GRID_HEIGHT - 0.5 - local d = math.sqrt(nx^2 + ny^2) / math.sqrt(0.5) + local d = (nx^2 + ny^2)^0.5 / 0.5^0.5 noise = noise * d^0.125 -- arbitrary, seems to work good end diff --git a/src/hexyz.lua b/src/hexyz.lua index 59d3a32..a89d84c 100644 --- a/src/hexyz.lua +++ b/src/hexyz.lua @@ -311,7 +311,12 @@ function parallelogram_map(width, height, seed) get = function(x, y) return map_get(map, x, y) end, set = function(x, y, v) return map_set(map, x, y, v) end, partial = function(x, y, k, v) return map_partial_set(map, x, y, k, v) end, - traverse = function(callback) return map_traverse(map, callback) end + traverse = function(callback) return map_traverse(map, callback) end, + neighbours = function(hex) + return table.filter(hex_neighbours(hex), function(_hex) + return map.get(_hex.x, _hex.y) + end) + end }}) end @@ -344,7 +349,12 @@ function triangular_map(size, seed) get = function(x, y) return map_get(map, x, y) end, set = function(x, y, v) return map_set(map, x, y, v) end, partial = function(x, y, k, v) return map_partial_set(map, x, y, k, v) end, - traverse = function(callback) return map_traverse(map, callback) end + traverse = function(callback) return map_traverse(map, callback) end, + neighbours = function(hex) + return table.filter(hex_neighbours(hex), function(_hex) + return map.get(_hex.x, _hex.y) + end) + end }}) end @@ -382,7 +392,12 @@ function hexagonal_map(radius, seed) get = function(x, y) return map_get(map, x, y) end, set = function(x, y, v) return map_set(map, x, y, v) end, partial = function(x, y, k, v) return map_partial_set(map, x, y, k, v) end, - traverse = function(callback) return map_traverse(map, callback) end + traverse = function(callback) return map_traverse(map, callback) end, + neighbours = function(hex) + return table.filter(hex_neighbours(hex), function(_hex) + return map.get(_hex.x, _hex.y) + end) + end }}) end @@ -418,7 +433,12 @@ function rectangular_map(width, height, seed) get = function(x, y) return map_get(map, x, y) end, set = function(x, y, v) return map_set(map, x, y, v) end, partial = function(x, y, k, v) return map_partial_set(map, x, y, k, v) end, - traverse = function(callback) return map_traverse(map, callback) end + traverse = function(callback) return map_traverse(map, callback) end, + neighbours = function(hex) + return table.filter(hex_neighbours(hex), function(_hex) + return map.get(_hex.x, _hex.y) + end) + end }}) end @@ -426,43 +446,78 @@ end -- PATHFINDING ---[[ @TODO bad breadth first - local frontier = { _tower.hex } - local history = {} - history[_tower.hex.x] = {} - history[_tower.hex.x][_tower.hex.y] = true +function breadth_first(map, start) + local frontier = {} + frontier[1] = { start } + + local distance = {} + distance[start.x] = {} + distance[start.x][start.y] = 0 while not (#frontier == 0) do local current = table.remove(frontier, 1) - for _,neighbour in pairs(grid_neighbours(HEX_MAP, _tower.hex)) do - if not (history[neighbour.x] and history[neighbour.x][neighbour.y]) then - local mob = mob_on_hex(neighbour) - if mob then - _tower.target = mob - break - end + for _,neighbour in pairs(map.neighbours(current)) do + local d = map_get(distance, neighbour.x, neighbour.y) + if not d then table.insert(frontier, neighbour) + map_set(distance, neighbour.x, neighbour.y, d + 1) end end + end + + return distance +end + +function dijkstra(map, start, goal, cost_f) + local frontier = {} + frontier = { hex = start, priority = 0 } + + local came_from = {} + came_from[start.x] = {} + came_from[start.x][start.y] = false + + local cost_so_far = {} + cost_so_far[start.x] = {} + cost_so_far[start.x][start.y] = 0 - if _tower.target then - log(_tower.target) + while not (#frontier == 0) do + local current = table.remove(frontier, 1) + + if current.hex == goal then break end + + for _,neighbour in pairs(map.neighbours(current.hex)) do + local new_cost = map_get(cost_so_far, current.hex.x, current.hex.y) + cost_f(current.hex, neighbour) + local neighbour_cost = map_get(cost_so_far, neighbour.x, neighbour.y) + + if not neighbour_cost or new_cost < neighbour_cost then + map_set(cost_so_far, neighbour.x, neighbour.y, new_cost) + local priority = new_cost + table.insert(frontier, { hex = neighbour, priority = priority }) + map_set(came_from, neighbour.x, neighbour.y, current) + end + end end -]] + + return came_from +end -- generic A* pathfinding -- +-- |heuristic| has the form: +-- function(source, target) -- source and target are vec2's +-- return some numeric value +-- +-- |cost_f| has the form: +-- function (from, to) -- from and to are vec2's +-- return some numeric value +-- -- returns a map that has map[hex.x][hex.y] = { hex = vec2, priority = number }, -- where the hex is the spot it thinks you should go to from the indexed hex, and priority is the cost of that decision, -- as well as 'made_it' a bool that tells you if we were successful in reaching |goal| -function Astar(map, start, goal, neighbour_f, heuristic_f, cost_f) - local neighbour_f = neighbour_f or function(map, hex) return hex_neighbours(hex) end - local heuristic_f = heuristic_f or math.distance - local cost_f = cost_f or function(from, to) return 1 end - +function Astar(map, start, goal, heuristic, cost_f) local path = {} path[start.x] = {} path[start.x][start.y] = false @@ -483,13 +538,13 @@ function Astar(map, start, goal, neighbour_f, heuristic_f, cost_f) break end - for _,next_ in pairs(neighbour_f(map, current.hex)) do + for _,next_ in pairs(map.neighbours(current.hex)) do local new_cost = map_get(path_so_far, current.hex.x, current.hex.y) + cost_f(current.hex, next_) local next_cost = map_get(path_so_far, next_.x, next_.y) if not next_cost or new_cost < next_cost then map_set(path_so_far, next_.x, next_.y, new_cost) - local priority = new_cost + heuristic_f(goal, next_) + local priority = new_cost + heuristic(goal, next_) table.insert(frontier, { hex = next_, priority = priority }) map_set(path, next_.x, next_.y, current) end diff --git a/src/mob.lua b/src/mob.lua index 94e682b..2a7dc54 100644 --- a/src/mob.lua +++ b/src/mob.lua @@ -1,34 +1,81 @@ +--[[ +mob(entity) structure: +{ + path - 2d table - map of hexes to other hexes, forms a path + speed - number - multiplier on distance travelled per frame, up to the update function to use correctly + bounty - number - score bonus you get when this mob is killed + hurtbox_radius - number - +} +--]] + +-- distance from hex centerpoint to nearest edge +MOB_SIZE = hex_height(HEX_SIZE, ORIENTATION.FLAT) / 2 + +function mobs_on_hex(hex) + local t = {} + + for mob_index,mob in pairs(MOBS) do + if mob and mob.hex == hex then + table.insert(t, mob_index, mob) + end + end + return t +end + -- @NOTE returns i,v in the table function mob_on_hex(hex) - return table.find(ENTITIES, function(entity) - return entity.type == ENTITY_TYPE.MOB and entity.hex == hex + return table.find(MOBS, function(mob) + return mob and mob.hex == hex end) end - -function mob_die(mob, entity_index) +function mob_die(mob, mob_index) WORLD:action(vplay_sound(SOUNDS.EXPLOSION1)) - delete_entity(entity_index) + --WORLD:append(mob_death_explosion(mob)) + delete_entity(MOBS, mob_index) end -function do_hit_mob(mob, damage, index) +function mob_death_explosion(mob) + local t = 0.5 + return am.particles2d{ + source_pos = mob.position, + source_pos_var = vec2(mob.hurtbox_radius), + max_particles = 25, + start_size = mob.hurtbox_radius/10, + start_size_var = mob.hurtbox_radius/15, + end_size = 0, + angle = 0, + angle_var = math.pi, + speed = 105, + speed_var = 55, + life = t * 0.8, + life_var = t * 0.2, + start_color = COLORS.CLARET, + start_color_var = COLORS.DIRT, + end_color = COLORS.DIRT, + end_color_var = COLORS.CLARET, + damping = 0.3 + }:action(coroutine.create(function(self) + am.wait(am.delay(t)) + WORLD:remove(self) + end)) +end + +function do_hit_mob(mob, damage, mob_index) mob.health = mob.health - damage - if mob.health < 1 then + if mob.health <= 0 then update_score(mob.bounty) - mob_die(mob, index) + mob_die(mob, mob_index) end end function check_for_broken_mob_pathing(hex) - for _,entity in pairs(ENTITIES) do - if entity.type == ENTITY_TYPE.MOB and entity.path[hex.x] and entity.path[hex.x][hex.y] then - --local pathfinder = coroutine.create(function() - entity.path = get_mob_path(entity, HEX_MAP, entity.hex, HEX_GRID_CENTER) - --end) - --coroutine.resume(pathfinder) + for _,mob in pairs(MOBS) do + if mob and mob.path[hex.x] and mob.path[hex.x][hex.y] then + mob.path = get_mob_path(mob, HEX_MAP, mob.hex, HEX_GRID_CENTER) end end end @@ -43,22 +90,7 @@ end -- try reducing map size by identifying key nodes (inflection points) -- there are performance hits everytime we spawn a mob and it's Astar's fault function get_mob_path(mob, map, start, goal) - return Astar(map, goal, start, - -- neighbour function - function(map, hex) - return table.filter(grid_neighbours(map, hex), function(_hex) - return mob_can_pass_through(mob, _hex) - end) - end, - - -- heuristic function - math.distance, - - -- cost function - function(from, to) - return math.abs(map.get(from.x, from.y).elevation - map.get(to.x, to.y).elevation) - end - ) + return Astar(map, goal, start, grid_heuristic, grid_cost) end -- @FIXME there's a bug here where the position of the spawn hex is sometimes 1 closer to the center than we want @@ -91,58 +123,53 @@ local function get_spawn_hex() return spawn_hex end -local function make_and_register_mob() - local mob = make_and_register_entity( - -- type - ENTITY_TYPE.MOB, +local function mob_update(mob, mob_index) + mob.hex = pixel_to_hex(mob.position) - -- hex spawn position - get_spawn_hex(), + local frame_target = mob.path[mob.hex.x] and mob.path[mob.hex.x][mob.hex.y] - -- node - am.scale(2) - ^ am.rotate(TIME) - ^ pack_texture_into_sprite(TEX_MOB1_1, 20, 20), - - -- update - function(_mob, _mob_index) - _mob.hex = pixel_to_hex(_mob.position) - - local frame_target = _mob.path[_mob.hex.x] and _mob.path[_mob.hex.x][_mob.hex.y] - - if frame_target then - _mob.position = _mob.position + math.normalize(hex_to_pixel(frame_target.hex) - _mob.position) * _mob.speed - _mob.node.position2d = _mob.position - - else - if _mob.hex == HEX_GRID_CENTER then - update_score(-_mob.health) - mob_die(_mob, _mob_index) - else - log("stuck") - end - end - - -- passive animation - if math.random() < 0.01 then - _mob.node"rotate":action(am.tween(0.3, { angle = _mob.node"rotate".angle + math.pi*3 })) - else - _mob.node"rotate".angle = math.wrapf(_mob.node"rotate".angle + am.delta_time, math.pi*2) - end + if frame_target then + mob.position = mob.position + math.normalize(hex_to_pixel(frame_target.hex) - mob.position) * mob.speed + mob.node.position2d = mob.position + else + if mob.hex == HEX_GRID_CENTER then + update_score(-mob.health) + mob_die(mob, mob_index) + else + log("stuck") end + end + + --[[ passive animation + if math.random() < 0.01 then + mob.node"rotate":action(am.tween(0.3, { angle = mob.node"rotate".angle + math.pi*3 })) + else + mob.node"rotate".angle = math.wrapf(mob.node"rotate".angle + am.delta_time, math.pi*2) + end + --]] +end + +local function make_and_register_mob() + local mob = make_basic_entity( + get_spawn_hex(), + am.circle(vec2(0), MOB_SIZE, COLORS.SUNRAY), + mob_update ) mob.path = get_mob_path(mob, HEX_MAP, mob.hex, HEX_GRID_CENTER) mob.health = 10 mob.speed = 1 mob.bounty = 5 - mob.hurtbox_radius = 100 + mob.hurtbox_radius = MOB_SIZE + + register_entity(MOBS, mob) end local SPAWN_CHANCE = 100 function do_mob_spawning() --if WIN:key_pressed"space" then if math.random(SPAWN_CHANCE) == 1 then + --if #MOBS < 1 then make_and_register_mob() end end diff --git a/src/projectile.lua b/src/projectile.lua index 0d740c5..895b688 100644 --- a/src/projectile.lua +++ b/src/projectile.lua @@ -1,45 +1,81 @@ -function make_and_register_projectile(hex, vector, velocity, damage, hitbox_radius) - local projectile = make_and_register_entity( - -- type - ENTITY_TYPE.PROJECTILE, +--[[ +bullet/projectile(entity) structure +{ + vector - vec2 - normalized vector of the current direction of this projectile + velocity - number - multplier on distance travelled per frame + damage - number - guess + hitbox_radius - number - hitboxes are circles +} +--]] - hex, +function projectile_update(projectile, projectile_index) + projectile.position = projectile.position + projectile.vector * projectile.velocity + projectile.node.position2d = projectile.position + projectile.hex = pixel_to_hex(projectile.position) - -- node - am.circle(vec2(0), hitbox_radius - 1, COLORS.CLARET), + -- check if we're out of bounds + if not point_in_rect(projectile.position + WORLDSPACE_COORDINATE_OFFSET, { + x1 = WIN.left, + y1 = WIN.bottom, + x2 = WIN.right, + y2 = WIN.top + }) then + delete_entity(PROJECTILES, projectile_index) + return true + end - -- update function - function(_projectile, _projectile_index) - _projectile.position = _projectile.position + vector * velocity - _projectile.node.position2d = _projectile.position - _projectile.hex = pixel_to_hex(_projectile.position) + -- check if we hit something + -- get a list of hexes that could have something we could hit on them + local search_hexes = spiral_map(projectile.hex, 1) + local hit_mob_count = 0 + local hit_mobs = {} + for _,hex in pairs(search_hexes) do - local mob_index,mob = mob_on_hex(_projectile.hex) + -- check if there's a mob on the hex + for mob_index,mob in pairs(mobs_on_hex(hex)) do if mob and circles_intersect(mob.position - , _projectile.position + , projectile.position , mob.hurtbox_radius - , _projectile.hitbox_radius) then - - do_hit_mob(mob, _projectile.damage, mob_index) - delete_entity(_projectile_index) - WORLD:action(vplay_sound(SOUNDS.HIT1)) - - elseif not point_in_rect(_projectile.position + WORLDSPACE_COORDINATE_OFFSET, { - x1 = WIN.left, - y1 = WIN.bottom, - x2 = WIN.right, - y2 = WIN.top - }) then - delete_entity(_projectile_index) + , projectile.hitbox_radius) then + table.insert(hit_mobs, mob_index, mob) + hit_mob_count = hit_mob_count + 1 end end - ) + end + + -- we didn't hit anyone + if hit_mob_count == 0 then return end + + -- we could have hit multiple, (optionally) find the closest + local closest_mob_index, closest_mob = next(hit_mobs, nil) + local closest_d = math.distance(closest_mob.position, projectile.position) + for _mob_index,mob in pairs(hit_mobs) do + local d = math.distance(mob.position, projectile.position) + if d < closest_d then + closest_mob_index = _mob_index + closest_mob = mob + closest_d = d + end + end + + -- hit the mob, delete ourselves, affect the world + do_hit_mob(closest_mob, projectile.damage, closest_mob_index) + delete_entity(PROJECTILES, projectile_index) + WORLD:action(vplay_sound(SOUNDS.HIT1)) +end + +function make_and_register_projectile(hex, vector, velocity, damage, hitbox_radius) + local projectile = make_basic_entity(hex + , am.line(vector, vector*hitbox_radius, 3, COLORS.CLARET) + , projectile_update) projectile.vector = vector projectile.velocity = velocity projectile.damage = damage projectile.hitbox_radius = hitbox_radius + + register_entity(PROJECTILES, projectile) end diff --git a/src/tower.lua b/src/tower.lua index ecfdaa4..91a5580 100644 --- a/src/tower.lua +++ b/src/tower.lua @@ -1,53 +1,97 @@ +TOWER_TYPE = { + REDEYE = 0, + WALL = 1, + MOAT = 2, +} + +function tower_type_tostring(type_) + if type_ == TOWER_TYPE.REDEYE then + return "Redeye Tower" + elseif type_ == TOWER_TYPE.WALL then + return "Wall" + elseif type_ == TOWER_TYPE.MOAT then + return "Moat" + end +end + + +--[[ +tower(entity) structure: +{ + -- @NOTE these should probably be wrapped in a 'weapon' struct or something, so towers can have multiple weapons + range - number - distance it can shoot + last_shot_time - number - timestamp (seconds) of last time it shot + target_index - number - index of entity it is currently shooting +} +--]] function is_buildable(hex, tile, tower) local blocked = mob_on_hex(hex) return not blocked and is_passable(tile) end -function make_and_register_tower(hex) - local tower = make_and_register_entity( - -- type - ENTITY_TYPE.TOWER, - - -- spawning hex - hex, - - -- node - pack_texture_into_sprite(TEX_TOWER2, 45, 34), - - -- update function - function(_tower, _tower_index) - if not _tower.target_index then - for index,entity in pairs(ENTITIES) do - if entity and entity.type == ENTITY_TYPE.MOB then - local d = math.distance(entity.hex, _tower.hex) - if d <= _tower.range then - _tower.target_index = index - break - end - end - end - else - if ENTITIES[_tower.target_index] == nil then - _tower.target_index = false - - elseif (TIME - _tower.last_shot_time) > 1 then - local entity = ENTITIES[_tower.target_index] - - make_and_register_projectile( - _tower.hex, - math.normalize(hex_to_pixel(entity.hex) - _tower.position), - 15, - 5, - 4 - ) - - _tower.last_shot_time = TIME - _tower.node:action(vplay_sound(SOUNDS.LASER2)) +function update_tower_redeye(tower, tower_index) + if not tower.target_index then + for index,mob in pairs(MOBS) do + if mob then + local d = math.distance(mob.position, tower.position) / (HEX_SIZE * 2) + if d <= tower.range then + tower.target_index = index + break end end end + else + if MOBS[tower.target_index] == false then + tower.target_index = false + + elseif (TIME - tower.last_shot_time) > 1 then + local mob = MOBS[tower.target_index] + + make_and_register_projectile( + tower.hex, + math.normalize(hex_to_pixel(mob.hex) - tower.position), + 15, + 5, + 10 + ) + + tower.last_shot_time = TIME + tower.node:action(vplay_sound(SOUNDS.LASER2)) + end + end +end + +local function make_tower_sprite(tower_type) + if tower_type == TOWER_TYPE.REDEYE then + return pack_texture_into_sprite(TEX_TOWER2, HEX_PIXEL_SIZE.x, HEX_PIXEL_SIZE.y) + + elseif tower_type == TOWER_TYPE.WALL then + --return pack_texture_into_sprite(TEX_WALL_CLOSED, HEX_PIXEL_SIZE.x, HEX_PIXEL_SIZE.y) + return am.circle(vec2(0), HEX_SIZE, COLORS.VERY_DARK_GRAY, 6) + + elseif tower_type == TOWER_TYPE.MOAT then + --return pack_texture_into_sprite(TEX_MOAT1, HEX_PIXEL_SIZE.x, HEX_PIXEL_SIZE.y) + return am.circle(vec2(0), HEX_SIZE, COLORS.YALE_BLUE, 6) + end +end + +local function modify_terrain_by_tower_type(tower_type, hex) + +end + +local function get_tower_update_function(tower_type) + if tower_type == TOWER_TYPE.REDEYE then + return update_tower_redeye + end +end + +function make_and_register_tower(hex, tower_type) + local tower = make_basic_entity( + hex, + make_tower_sprite(tower_type), + get_tower_update_function(tower_type) ) tower.range = 10 @@ -57,5 +101,7 @@ function make_and_register_tower(hex) -- make this cell impassable HEX_MAP[hex.x][hex.y].elevation = 2 check_for_broken_mob_pathing(hex) + + register_entity(TOWERS, tower) end diff --git a/texture.lua b/texture.lua index 9ddec91..d43429b 100644 --- a/texture.lua +++ b/texture.lua @@ -6,6 +6,7 @@ function load_textures() TEX_ARROW = am.texture2d("res/arrow.png") TEX_WALL_CLOSED = am.texture2d("res/wall_closed.png") + TEX_MOAT1 = am.texture2d("res/moat1.png") TEX_TOWER1 = am.texture2d("res/tower1.png") TEX_TOWER2 = am.texture2d("res/tower2.png")