player.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. Player = class()
  2. Player.width = 45
  3. Player.height = 90
  4. Player.depth = -3.5
  5. Player.walkSpeed = 75
  6. ----------------
  7. -- Core
  8. ----------------
  9. function Player:init()
  10. -- Physics
  11. self.x = ctx.map.width / 2
  12. self.y = ctx.map.height - ctx.map.groundHeight - self.height
  13. self.prevx = self.x
  14. self.prevy = self.y
  15. self.direction = 1
  16. self.speed = 0
  17. self.walkSpeed = Player.walkSpeed
  18. -- Health
  19. self.maxHealth = 100
  20. self.health = self.maxHealth
  21. self.healthDisplay = self.health
  22. self.prevHealthDisplay = self.healthDisplay
  23. self.prevHealth = self.health
  24. self.maxHealthIncreaseTime = 60
  25. -- Dead
  26. self.dead = false
  27. self.deathTimer = 0
  28. self.deathDuration = 7
  29. -- Juju
  30. self.juju = 0
  31. self.totalJuju = 0
  32. self.jujuRate = config.player.jujuRate / (ctx.mode == 'survival' and 2 or 1)
  33. self.jujuTimer = self.jujuRate
  34. self:addJuju(config.player.baseJuju)
  35. -- The current magic shruju
  36. self.shruju = nil
  37. -- Summoning and selection
  38. self.summonSelect = 1
  39. self.totalSummoned = 0
  40. -- Buffs
  41. self.invincible = 0
  42. self.ghostSpeedMultiplier = 1
  43. self.cooldownSpeed = 1
  44. self.buffs = PlayerBuffs(self)
  45. self.footstepIndex = 0
  46. -- joystick
  47. self.joystick = #love.joystick.getJoysticks() > 0 and love.joystick.getJoysticks()[1]
  48. end
  49. function Player:activate()
  50. -- Create animation
  51. self.animation = data.animation.muju()
  52. self.animation:on('complete', function(data)
  53. if data.state.name ~= 'death' and not data.state.loop then
  54. self.animation:set('idle', {force = true})
  55. end
  56. end)
  57. self.animation:on('event', function(event)
  58. if event.data.name == 'stepone' or event.data.name == 'steptwo' then
  59. if self.footstepIndex < 2 then
  60. self.footstepIndex = self.footstepIndex + 1
  61. else
  62. ctx.sound:play('footstep' .. love.math.random(1, 2), function(sound)
  63. sound:setPitch(.9 + love.math.random() * .3)
  64. sound:setVolume(.4)
  65. end)
  66. end
  67. end
  68. end)
  69. self.animation.spine.skeleton:setSkin(ctx.user.hat or 'nohat')
  70. self.animation.spine.skeleton:findSlot('hat').a = ctx.user.hat and 1 or 0
  71. self.animation.spine.skeleton:findBone('hat').scaleX = self.animation.scale
  72. self.animation.spine.skeleton:findBone('hat').scaleY = self.animation.scale
  73. -- Color animation
  74. for _, slot in pairs({'robebottom', 'torso', 'front_upper_arm', 'rear_upper_arm', 'front_bracer', 'rear_bracer'}) do
  75. local slot = self.animation.spine.skeleton:findSlot(slot)
  76. slot.r, slot.g, slot.b = unpack(config.player.colors[ctx.user.color])
  77. end
  78. -- Initialize deck data structure from ctx.user
  79. self:initDeck()
  80. ctx.event:emit('view.register', {object = self})
  81. end
  82. function Player:update()
  83. -- Lerp vars
  84. self.prevx = self.x
  85. self.prevy = self.y
  86. self.prevHealthDisplay = self.healthDisplay
  87. self.prevHealth = self.health
  88. -- Core updates
  89. self:move()
  90. self:animate()
  91. self.buffs:update()
  92. if self.ghost then self.ghost:update() end
  93. -- Rots
  94. if ctx.tutorial:shouldDecayGhost() then self.deathTimer = timer.rot(self.deathTimer, function() self:spawn() end) end
  95. self.invincible = timer.rot(self.invincible)
  96. for i = 1, #self.deck do
  97. if self.deck[i].cooldown > 0 then
  98. self.deck[i].cooldown = self.deck[i].cooldown - ls.tickrate * self.cooldownSpeed
  99. if self.deck[i].cooldown <= 0 then
  100. ctx.hud.units.cooldownPop[i] = 1
  101. self.deck[i].cooldown = 0
  102. end
  103. end
  104. end
  105. -- Max Health Increase
  106. if ctx.timer * ls.tickrate > self.maxHealthIncreaseTime then
  107. local ratio = self.health / self.maxHealth
  108. self.maxHealth = self.maxHealth + config.player.maxHealthPerMinute
  109. self.health = self.maxHealth * ratio
  110. self.prevHealth = self.health
  111. self.maxHealthIncreaseTime = self.maxHealthIncreaseTime + 60
  112. end
  113. -- Lerp healthbar
  114. self.healthDisplay = math.lerp(self.healthDisplay, self.health, math.min(10 * ls.tickrate, 1))
  115. -- Juju trickle
  116. self.jujuTimer = timer.rot(self.jujuTimer, function()
  117. self:addJuju(1)
  118. return self.jujuRate
  119. end)
  120. end
  121. function Player:draw()
  122. -- Flash when invincible
  123. if math.floor(self.invincible * 4) % 2 == 0 then
  124. local x, y = math.lerp(self.prevx, self.x, ls.accum / ls.tickrate), math.lerp(self.prevy, self.y, ls.accum / ls.tickrate)
  125. love.graphics.setColor(255, 255, 255)
  126. self.animation:draw(x, y)
  127. end
  128. end
  129. function Player:keypressed(key)
  130. -- Select minions with digits
  131. for i = 1, #self.deck do
  132. if tonumber(key) == i then
  133. self.summonSelect = i
  134. return
  135. end
  136. end
  137. -- Summon with space
  138. if key == ' ' and not self.dead and ctx.tutorial:shouldSummon() then
  139. self:summon()
  140. end
  141. if key == 'q' then
  142. local picked = false
  143. ctx.shrujus:each(function(shruju)
  144. if shruju:playerNearby() then
  145. if self.shruju then self.shruju:drop() end
  146. shruju:pickup()
  147. picked = true
  148. return true
  149. end
  150. end)
  151. if not picked and self.shruju then
  152. self.shruju:drop()
  153. self.shruju = nil
  154. end
  155. end
  156. end
  157. function Player:gamepadpressed(gamepad, button)
  158. end
  159. function Player:gamepadaxis(joystick, axis, value)
  160. if axis == 'triggerright' and value > .75 and not self.dead then
  161. self:summon()
  162. end
  163. end
  164. function Player:paused()
  165. -- Reset prev variables when paused to fix lerp jitter.
  166. self.prevx = self.x
  167. self.prevy = self.y
  168. self.animation:set('idle')
  169. if self.ghost then
  170. self.ghost.prevx = self.ghost.x
  171. self.ghost.prevy = self.ghost.y
  172. end
  173. end
  174. ----------------
  175. -- Behavior
  176. ----------------
  177. function Player:move()
  178. -- If we can't move then don't move
  179. local animation = self.animation.state.name
  180. if not ctx.tutorial:shouldPlayerMove() or self.dead or animation == 'summon' or animation == 'death' or animation == 'resurrect' then
  181. self.speed = 0
  182. return
  183. end
  184. -- Adjust speed to target speed based on keystate
  185. local maxSpeed = self.walkSpeed
  186. if love.keyboard.isDown('left', 'a') or (self.joystick and self.joystick:getGamepadAxis('leftx') < -.5) then
  187. self.speed = math.lerp(self.speed, -maxSpeed, math.min(10 * ls.tickrate, 1))
  188. elseif love.keyboard.isDown('right', 'd') or (self.joystick and self.joystick:getGamepadAxis('leftx') > .5) then
  189. self.speed = math.lerp(self.speed, maxSpeed, math.min(10 * ls.tickrate, 1))
  190. else
  191. self.speed = math.lerp(self.speed, 0, math.min(10 * ls.tickrate, 1))
  192. self.footstepIndex = 0
  193. end
  194. -- Actually move
  195. self.x = self.x + self.speed * ls.tickrate
  196. self.direction = self.speed == 0 and self.direction or math.sign(self.speed)
  197. -- Don't go outside map
  198. self.x = math.clamp(self.x, 0, ctx.map.width)
  199. end
  200. function Player:summon(options)
  201. options = options or {}
  202. local minion = self.deck[self.summonSelect].code
  203. local cooldown = self.deck[self.summonSelect].cooldown
  204. local unitCount = #ctx.units:filter(function(u) return u.player end)
  205. local cost = data.unit[minion].cost * (unitCount == 0 and 0 or 1)
  206. local animation = self.animation.state.name
  207. -- Check if we can summon
  208. if not options.force and not (not ctx.hud.upgrades.active and not ctx.paused and cooldown == 0 and animation ~= 'dead' and animation ~= 'resurrect' and self:spend(cost)) then
  209. return ctx.sound:play('misclick', function(sound) sound:setVolume(.3) end)
  210. end
  211. -- Ow
  212. self:hurt(self.maxHealth * .1)
  213. -- Create minion
  214. local unit = ctx.units:add(minion, {player = self, x = self.x + love.math.random(-20, 20)})
  215. -- Set cooldowns (global cooldown)
  216. local cooldown = config.player.baseCooldown
  217. if self:hasShruju('refresh') and love.math.random() < .25 then cooldown = 0 end
  218. for i = 1, #self.deck do
  219. if cooldown > self.deck[i].cooldown then
  220. self.deck[i].cooldown = cooldown
  221. self.deck[i].maxCooldown = cooldown
  222. end
  223. end
  224. -- Achievement: Mini Arsenal
  225. ctx.event:emit('achievement', {name = 'miniarsenal'})
  226. -- Aftermath, juice, animations, etc.
  227. self.totalSummoned = self.totalSummoned + 1
  228. self.invincible = 0
  229. self.animation:set('summon')
  230. if not options.nosound then
  231. local summonSound = love.math.random(1, 3)
  232. ctx.sound:play('summon' .. summonSound)
  233. end
  234. ctx.hud.units.animations[self.summonSelect]:set('spawn')
  235. for i = 1, 20 do
  236. ctx.particles:emit('jujudrop', self.x + love.math.randomNormal(20), self.y + love.math.randomNormal(20) + self.height / 2, 1)
  237. end
  238. end
  239. function Player:animate()
  240. if self.dead then return end
  241. -- Flip animation, set animation speed
  242. self.animation:set(math.abs(self.speed) > self.walkSpeed / 2 and 'walk' or 'idle')
  243. self.animation.speed = self.animation.state.name == 'walk' and math.max(math.abs(self.speed / Player.walkSpeed), .4) or 1
  244. if self.speed ~= 0 then self.animation.flipped = math.sign(self.speed) > 0 end
  245. end
  246. function Player:spend(amount)
  247. if self.juju >= amount then
  248. self.juju = self.juju - amount
  249. return true
  250. end
  251. return false
  252. end
  253. function Player:addJuju(amount)
  254. if ctx.tutorial and ctx.tutorial.active then return end
  255. self.juju = self.juju + amount
  256. self.totalJuju = self.totalJuju + amount
  257. end
  258. function Player:hurt(amount, source, kind)
  259. if not self.dead and self.invincible == 0 then
  260. amount = self.buffs:prehurt(amount, source, kind) or amount
  261. self.health = math.max(self.health - amount, 0)
  262. self.buffs:posthurt(amount, source, kind)
  263. ctx.event:emit('player.hurt', {amount = amount, source = source, kind = kind})
  264. if amount > 5 then
  265. local sound = data.media.sounds['hit' .. love.math.random(1, 3)]
  266. ctx.sound:play(sound, function(sound) sound:setVolume(0) end)
  267. end
  268. -- Die if we are dead
  269. if self.health <= 0 and self.deathTimer == 0 then self:die() end
  270. end
  271. return amount
  272. end
  273. function Player:die()
  274. self.deathTimer = self.deathDuration
  275. self.dead = true
  276. self.ghost = GhostPlayer(self)
  277. self.buffs:die()
  278. self.animation:set('death')
  279. ctx.sound:play('death', function(sound) sound:setVolume(.3) end)
  280. if self:hasShruju('reincarnation') then
  281. self:summon({force = true, nosound = true})
  282. end
  283. end
  284. function Player:spawn()
  285. self.deathTimer = 0
  286. self.invincible = 4.5
  287. self.health = self.maxHealth
  288. self.dead = false
  289. self.ghost:despawn()
  290. self.ghost = nil
  291. self.animation:set('resurrect')
  292. end
  293. function Player:heal(amount, source)
  294. if self.dead then return end
  295. self.health = math.min(self.health + amount, self.maxHealth)
  296. end
  297. ----------------
  298. -- Helper
  299. ----------------
  300. function Player:getHealthbar()
  301. local x = math.lerp(self.prevx, self.x, ls.accum / ls.tickrate)
  302. local y = math.lerp(self.prevy, self.y, ls.accum / ls.tickrate)
  303. local healthDisplay = math.lerp(self.prevHealthDisplay, self.healthDisplay, ls.accum / ls.tickrate)
  304. local health = math.lerp(self.prevHealth, self.health, ls.accum / ls.tickrate)
  305. return x, y, health / self.maxHealth, healthDisplay / self.maxHealth
  306. end
  307. function Player:atShrine()
  308. local shrine = table.values(ctx.shrines:filter(function(shrine) return shrine.team == self.team end))[1]
  309. if not shrine then return false end
  310. return math.abs(self.x - shrine.x) < self.width
  311. end
  312. function Player:initDeck()
  313. self.deck = {}
  314. local minions = {}
  315. if ctx.mode == 'campaign' then
  316. minions = {config.biomes[ctx.biome].minion}
  317. elseif ctx.mode == 'survival' then
  318. minions = ctx.user.survival.minions
  319. end
  320. for i = 1, 2 do
  321. local code = minions[i]
  322. if code then
  323. self.deck[code] = {
  324. runes = ctx.user.runes[code],
  325. cooldown = 0,
  326. maxCooldown = 2,
  327. code = code
  328. }
  329. self.deck[i] = self.deck[code]
  330. -- Attribute and ability runes
  331. table.each(self.deck[i].runes, function(rune)
  332. if rune.attributes then
  333. table.each(rune.attributes, function(amount, attribute)
  334. local class = data.unit[code]
  335. class.attributes[attribute] = class.attributes[attribute] + amount
  336. end)
  337. elseif rune.abilities then
  338. table.each(rune.abilities, function(stats, ability)
  339. table.each(stats, function(amount, stat)
  340. local target = data.ability[code][ability]
  341. local proxy = config.runes.abilityProxies[code] and config.runes.abilityProxies[code][ability]
  342. if proxy then
  343. if type(proxy) == 'string' then
  344. if proxy == 'buff' then
  345. target = data.buff[ability]
  346. elseif proxy == 'ability' then
  347. target = data.ability[code][ability]
  348. end
  349. elseif type(proxy) == 'table' then
  350. local kind, key = unpack(proxy)
  351. if kind == 'ability' then
  352. target = data.ability[code][key]
  353. elseif kind == 'buff' then
  354. target = data.buff[key]
  355. end
  356. end
  357. end
  358. local key = 'rune' .. stat:capitalize()
  359. target[key] = target[key] + amount
  360. end)
  361. end)
  362. end
  363. end)
  364. end
  365. end
  366. end
  367. function Player:contains(x, y)
  368. math.inside(x, y, self.x - self.width / 2, self.y, self.width, self.height)
  369. end
  370. function Player:hasShruju(code)
  371. return self.shruju and self.shruju.code == code
  372. end