player.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. Player = class()
  2. Player.width = 45
  3. Player.height = 90
  4. Player.depth = -3.5
  5. Player.walkSpeed = 65
  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. -- Health decay
  114. if ctx.tutorial:shouldDecayHealth() then
  115. self:hurt(self.maxHealth * .033 * ls.tickrate)
  116. end
  117. -- Lerp healthbar
  118. self.healthDisplay = lume.lerp(self.healthDisplay, self.health, math.min(10 * ls.tickrate, 1))
  119. -- Juju trickle
  120. self.jujuTimer = timer.rot(self.jujuTimer, function()
  121. self:addJuju(1)
  122. return self.jujuRate
  123. end)
  124. end
  125. function Player:draw()
  126. -- Flash when invincible
  127. if math.floor(self.invincible * 4) % 2 == 0 then
  128. local x, y = lume.lerp(self.prevx, self.x, ls.accum / ls.tickrate), lume.lerp(self.prevy, self.y, ls.accum / ls.tickrate)
  129. love.graphics.setColor(255, 255, 255)
  130. self.animation:draw(x, y)
  131. end
  132. end
  133. function Player:keypressed(key)
  134. -- Select minions with digits
  135. for i = 1, #self.deck do
  136. if tonumber(key) == i then
  137. self.summonSelect = i
  138. return
  139. end
  140. end
  141. -- Summon with space
  142. if key == ' ' and not self.dead and ctx.tutorial:shouldSummon() then
  143. self:summon()
  144. end
  145. if key == 'q' then
  146. local picked = false
  147. ctx.shrujus:each(function(shruju)
  148. if shruju:playerNearby() then
  149. if self.shruju then self.shruju:drop() end
  150. shruju:pickup()
  151. picked = true
  152. return true
  153. end
  154. end)
  155. if not picked and self.shruju then
  156. self.shruju:drop()
  157. self.shruju = nil
  158. end
  159. end
  160. end
  161. function Player:gamepadpressed(gamepad, button)
  162. end
  163. function Player:gamepadaxis(joystick, axis, value)
  164. if axis == 'triggerright' and value > .75 and not self.dead then
  165. self:summon()
  166. end
  167. end
  168. function Player:paused()
  169. -- Reset prev variables when paused to fix lerp jitter.
  170. self.prevx = self.x
  171. self.prevy = self.y
  172. self.animation:set('idle')
  173. if self.ghost then
  174. self.ghost.prevx = self.ghost.x
  175. self.ghost.prevy = self.ghost.y
  176. end
  177. end
  178. ----------------
  179. -- Behavior
  180. ----------------
  181. function Player:move()
  182. -- If we can't move then don't move
  183. local animation = self.animation.state.name
  184. if not ctx.tutorial:shouldPlayerMove() or self.dead or animation == 'summon' or animation == 'death' or animation == 'resurrect' then
  185. self.speed = 0
  186. return
  187. end
  188. -- Adjust speed to target speed based on keystate
  189. local maxSpeed = self.walkSpeed
  190. if love.keyboard.isDown('left', 'a') or (self.joystick and self.joystick:getGamepadAxis('leftx') < -.5) then
  191. self.speed = lume.lerp(self.speed, -maxSpeed, math.min(10 * ls.tickrate, 1))
  192. elseif love.keyboard.isDown('right', 'd') or (self.joystick and self.joystick:getGamepadAxis('leftx') > .5) then
  193. self.speed = lume.lerp(self.speed, maxSpeed, math.min(10 * ls.tickrate, 1))
  194. else
  195. self.speed = lume.lerp(self.speed, 0, math.min(10 * ls.tickrate, 1))
  196. self.footstepIndex = 0
  197. end
  198. -- Actually move
  199. self.x = self.x + self.speed * ls.tickrate
  200. self.direction = self.speed == 0 and self.direction or lume.sign(self.speed)
  201. -- Don't go outside map
  202. self.x = math.clamp(self.x, 0, ctx.map.width)
  203. end
  204. function Player:summon(options)
  205. options = options or {}
  206. local minion = self.deck[self.summonSelect].code
  207. local cooldown = self.deck[self.summonSelect].cooldown
  208. local cost = data.unit[minion].cost
  209. local animation = self.animation.state.name
  210. -- Check if we can summon
  211. 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
  212. return ctx.sound:play('misclick', function(sound) sound:setVolume(.3) end)
  213. end
  214. -- Create minion
  215. local unit = ctx.units:add(minion, {player = self, x = self.x + love.math.random(-20, 20)})
  216. -- Set cooldowns (global cooldown)
  217. local cooldown = config.player.baseCooldown
  218. if self:hasShruju('refresh') and love.math.random() < .25 then cooldown = 0 end
  219. for i = 1, #self.deck do
  220. if cooldown > self.deck[i].cooldown then
  221. self.deck[i].cooldown = cooldown
  222. self.deck[i].maxCooldown = cooldown
  223. end
  224. end
  225. -- Achievement: Mini Arsenal
  226. ctx.event:emit('achievement', {name = 'miniarsenal'})
  227. -- Aftermath, juice, animations, etc.
  228. self.totalSummoned = self.totalSummoned + 1
  229. self.invincible = 0
  230. self.animation:set('summon')
  231. if not options.nosound then
  232. local summonSound = love.math.random(1, 3)
  233. ctx.sound:play('summon' .. summonSound)
  234. end
  235. ctx.hud.units.animations[self.summonSelect]:set('spawn')
  236. for i = 1, 20 do
  237. ctx.particles:emit('jujudrop', self.x + love.math.randomNormal(20), self.y + love.math.randomNormal(20) + self.height / 2, 1)
  238. end
  239. end
  240. function Player:animate()
  241. if self.dead then return end
  242. -- Flip animation, set animation speed
  243. self.animation:set(math.abs(self.speed) > self.walkSpeed / 2 and 'walk' or 'idle')
  244. self.animation.speed = self.animation.state.name == 'walk' and math.max(math.abs(self.speed / Player.walkSpeed), .4) or 1
  245. if self.speed ~= 0 then self.animation.flipped = lume.sign(self.speed) > 0 end
  246. end
  247. function Player:spend(amount)
  248. if self.juju >= amount then
  249. self.juju = self.juju - amount
  250. return true
  251. end
  252. return false
  253. end
  254. function Player:addJuju(amount)
  255. if ctx.tutorial and ctx.tutorial.active then return end
  256. self.juju = self.juju + amount
  257. self.totalJuju = self.totalJuju + amount
  258. end
  259. function Player:hurt(amount, source, kind)
  260. if not self.dead and self.invincible == 0 then
  261. amount = self.buffs:prehurt(amount, source, kind) or amount
  262. self.health = math.max(self.health - amount, 0)
  263. self.buffs:posthurt(amount, source, kind)
  264. ctx.event:emit('player.hurt', {amount = amount, source = source, kind = kind})
  265. if amount > 5 then
  266. local sound = data.media.sounds['hit' .. love.math.random(1, 3)]
  267. ctx.sound:play(sound, function(sound) sound:setVolume(0) end)
  268. end
  269. -- Die if we are dead
  270. if self.health <= 0 and self.deathTimer == 0 then self:die() end
  271. end
  272. return amount
  273. end
  274. function Player:die()
  275. self.deathTimer = self.deathDuration
  276. self.dead = true
  277. self.ghost = GhostPlayer(self)
  278. self.buffs:die()
  279. self.animation:set('death')
  280. ctx.sound:play('death', function(sound) sound:setVolume(.3) end)
  281. if self:hasShruju('reincarnation') then
  282. self:summon({force = true, nosound = true})
  283. end
  284. end
  285. function Player:spawn()
  286. self.deathTimer = 0
  287. self.invincible = 4.5
  288. self.health = self.maxHealth
  289. self.dead = false
  290. self.ghost:despawn()
  291. self.ghost = nil
  292. self.animation:set('resurrect')
  293. end
  294. function Player:heal(amount, source)
  295. if self.dead then return end
  296. self.health = math.min(self.health + amount, self.maxHealth)
  297. end
  298. ----------------
  299. -- Helper
  300. ----------------
  301. function Player:getHealthbar()
  302. local x = lume.lerp(self.prevx, self.x, ls.accum / ls.tickrate)
  303. local y = lume.lerp(self.prevy, self.y, ls.accum / ls.tickrate)
  304. local healthDisplay = lume.lerp(self.prevHealthDisplay, self.healthDisplay, ls.accum / ls.tickrate)
  305. local health = lume.lerp(self.prevHealth, self.health, ls.accum / ls.tickrate)
  306. return x, y, health / self.maxHealth, healthDisplay / self.maxHealth
  307. end
  308. function Player:atShrine()
  309. local shrine = table.values(ctx.shrines:filter(function(shrine) return shrine.team == self.team end))[1]
  310. if not shrine then return false end
  311. return math.abs(self.x - shrine.x) < self.width
  312. end
  313. function Player:initDeck()
  314. self.deck = {}
  315. local minions = {}
  316. if ctx.mode == 'campaign' then
  317. minions = {config.biomes[ctx.biome].minion}
  318. elseif ctx.mode == 'survival' then
  319. minions = ctx.user.survival.minions
  320. end
  321. for i = 1, 2 do
  322. local code = minions[i]
  323. if code then
  324. self.deck[code] = {
  325. runes = ctx.user.runes[code],
  326. cooldown = 0,
  327. maxCooldown = 3,
  328. code = code
  329. }
  330. self.deck[i] = self.deck[code]
  331. -- Attribute and ability runes
  332. table.each(self.deck[i].runes, function(rune)
  333. if rune.attributes then
  334. table.each(rune.attributes, function(amount, attribute)
  335. local class = data.unit[code]
  336. class.attributes[attribute] = class.attributes[attribute] + amount
  337. end)
  338. elseif rune.abilities then
  339. table.each(rune.abilities, function(stats, ability)
  340. table.each(stats, function(amount, stat)
  341. local target = data.ability[code][ability]
  342. local proxy = config.runes.abilityProxies[code] and config.runes.abilityProxies[code][ability]
  343. if proxy then
  344. if type(proxy) == 'string' then
  345. if proxy == 'buff' then
  346. target = data.buff[ability]
  347. elseif proxy == 'ability' then
  348. target = data.ability[code][ability]
  349. end
  350. elseif type(proxy) == 'table' then
  351. local kind, key = unpack(proxy)
  352. if kind == 'ability' then
  353. target = data.ability[code][key]
  354. elseif kind == 'buff' then
  355. target = data.buff[key]
  356. end
  357. end
  358. end
  359. local key = 'rune' .. stat:capitalize()
  360. target[key] = target[key] + amount
  361. end)
  362. end)
  363. end
  364. end)
  365. end
  366. end
  367. end
  368. function Player:contains(x, y)
  369. math.inside(x, y, self.x - self.width / 2, self.y, self.width, self.height)
  370. end
  371. function Player:hasShruju(code)
  372. return self.shruju and self.shruju.code == code
  373. end