From a875aaaedaeff0cfe1a8730b105efe6f38557f7e Mon Sep 17 00:00:00 2001 From: Hubol Date: Mon, 12 Aug 2024 22:52:21 -0500 Subject: [PATCH] Reworking attacks --- src/igua/mixins/mxn-enemy.ts | 10 ++- src/igua/mixins/mxn-projectile.ts | 27 ++++++ src/igua/mixins/mxn-rpg-status.ts | 13 +-- .../objects/enemies/obj-angel-bouncing.ts | 10 ++- src/igua/objects/obj-water-drip-source.ts | 24 +++--- src/igua/rpg/rpg-attack.ts | 20 +++++ src/igua/rpg/rpg-enemy.ts | 18 ++-- src/igua/rpg/rpg-faction.ts | 5 ++ src/igua/rpg/rpg-player.ts | 16 +++- src/igua/rpg/rpg-progress.ts | 1 + src/igua/rpg/rpg-status.ts | 86 ++++++++++++++----- src/igua/scenes/player-test.ts | 27 ++---- 12 files changed, 177 insertions(+), 80 deletions(-) create mode 100644 src/igua/mixins/mxn-projectile.ts create mode 100644 src/igua/rpg/rpg-attack.ts create mode 100644 src/igua/rpg/rpg-faction.ts diff --git a/src/igua/mixins/mxn-enemy.ts b/src/igua/mixins/mxn-enemy.ts index 9ba7869..0c03740 100644 --- a/src/igua/mixins/mxn-enemy.ts +++ b/src/igua/mixins/mxn-enemy.ts @@ -5,6 +5,8 @@ import { RpgLoot } from "../rpg/rpg-loot"; import { RpgEnemy } from "../rpg/rpg-enemy"; import { objLootDrop } from "../objects/obj-loot-drop"; import { layers } from "../globals"; +import { RpgFaction } from "../rpg/rpg-faction"; +import { RpgAttack } from "../rpg/rpg-attack"; interface MxnEnemyArgs { hurtboxes: DisplayObject[]; @@ -21,6 +23,10 @@ export function mxnEnemy(obj: DisplayObject, args: MxnEnemyArgs) { level: 0, max: 100, value: 0, + }, + faction: RpgFaction.Enemy, + quirks: { + emotionalDamageIsFatal: false, } }; @@ -57,8 +63,8 @@ export function mxnEnemy(obj: DisplayObject, args: MxnEnemyArgs) { .merge({ // TODO needs other damage types!! // Maybe it's time for a RpgAttack.Model ?! - strikePlayer(damage: number) { - RpgEnemy.Methods.strikePlayer(enemy, 0, damage, 0); + strikePlayer(attack: RpgAttack.Model) { + RpgEnemy.Methods.strikePlayer(enemy, attack); } }); diff --git a/src/igua/mixins/mxn-projectile.ts b/src/igua/mixins/mxn-projectile.ts new file mode 100644 index 0000000..b8f661b --- /dev/null +++ b/src/igua/mixins/mxn-projectile.ts @@ -0,0 +1,27 @@ +import { DisplayObject } from "pixi.js"; +import { Instances } from "../../lib/game-engine/instances"; +import { mxnRpgStatus } from "./mxn-rpg-status"; +import { RpgAttack } from "../rpg/rpg-attack"; +import { RpgEnemy } from "../rpg/rpg-enemy"; + +interface MxnProjectileArgs { + attack: RpgAttack.Model; + enemy?: RpgEnemy.Model; +} + +export function mxnProjectile(obj: DisplayObject, args: MxnProjectileArgs) { + return obj + .dispatches<'hit'>() + .step(self => { + for (const instance of Instances(mxnRpgStatus)) { + // TODO filter by faction here pls + if (obj.collidesOne(instance.hurtboxes)) { + const result = instance.damage(args.attack); + if (!result.rejected) + self.dispatch('hit'); + if (self.destroyed) + return; + } + } + }) +} \ No newline at end of file diff --git a/src/igua/mixins/mxn-rpg-status.ts b/src/igua/mixins/mxn-rpg-status.ts index 75de896..7a9689d 100644 --- a/src/igua/mixins/mxn-rpg-status.ts +++ b/src/igua/mixins/mxn-rpg-status.ts @@ -1,5 +1,6 @@ import { DisplayObject } from "pixi.js"; import { RpgStatus } from "../rpg/rpg-status"; +import { RpgAttack } from "../rpg/rpg-attack"; interface MxnRpgStatusArgs { status: RpgStatus.Model; @@ -14,18 +15,18 @@ export function mxnRpgStatus(obj: DisplayObject, args: MxnRpgStatusArgs) { .track(mxnRpgStatus) .merge({ hurtboxes: args.hurtboxes }) .merge({ - damage(amount: number) { - RpgStatus.Methods.damage(args.status, args.effects, amount); + damage(attack: RpgAttack.Model) { + const result = RpgStatus.Methods.damage(args.status, args.effects, attack); // TODO feels weird, should maybe be part of return value of damage? - if (args.status.health === 0) + // Or should be part of effects, I think! + if (!result.rejected && result.died) rpgStatusObj.dispatch('rpgStatus.died'); + + return result; }, heal(amount: number) { RpgStatus.Methods.heal(args.status, args.effects, amount); }, - poison(amount: number) { - RpgStatus.Methods.poison(args.status, args.effects, amount); - } }) .dispatches<'rpgStatus.died'>() .step(() => { diff --git a/src/igua/objects/enemies/obj-angel-bouncing.ts b/src/igua/objects/enemies/obj-angel-bouncing.ts index ec37522..3327ecf 100644 --- a/src/igua/objects/enemies/obj-angel-bouncing.ts +++ b/src/igua/objects/enemies/obj-angel-bouncing.ts @@ -8,9 +8,15 @@ import { mxnPhysics } from "../../mixins/mxn-physics"; import { vnew } from "../../../lib/math/vector-type"; import { playerObj } from "../obj-player"; import { mxnEnemy } from "../../mixins/mxn-enemy"; +import { RpgPlayer } from "../../rpg/rpg-player"; +import { RpgAttack } from "../../rpg/rpg-attack"; const clownTxs = Tx.Enemy.CommonClown.split({ count: 2 }); +const atkSpikeBall = RpgAttack.create({ + physical: 10, +}) + export function objAngelBouncing() { // const obj = merge(new Container(), { hspeed, vspeed: 0, portal, dangerous, bounceAgainstWall, limitedRangeEnabled }); // container.ext.isHatParent = true; @@ -138,9 +144,9 @@ export function objAngelBouncing() { }) .step(self => { if (playerObj.collides(mask)) - self.damage(10); + self.damage(RpgPlayer.MeleeAttack); else if (playerObj.collides(spikeBall)) - self.strikePlayer(10); + self.strikePlayer(atkSpikeBall); }, 1001) diff --git a/src/igua/objects/obj-water-drip-source.ts b/src/igua/objects/obj-water-drip-source.ts index 3e0617e..f41dca6 100644 --- a/src/igua/objects/obj-water-drip-source.ts +++ b/src/igua/objects/obj-water-drip-source.ts @@ -4,8 +4,9 @@ import { Tx } from "../../assets/textures"; import { mxnPhysics } from "../mixins/mxn-physics"; import { sleep } from "../../lib/game-engine/promise/sleep"; import { Rng } from "../../lib/math/rng"; -import { Instances } from "../../lib/game-engine/instances"; -import { mxnRpgStatus } from "../mixins/mxn-rpg-status"; +import { RpgAttack } from "../rpg/rpg-attack"; +import { RpgFaction } from "../rpg/rpg-faction"; +import { mxnProjectile } from "../mixins/mxn-projectile"; interface ObjWaterDripSourceArgs { delayMin: number; @@ -24,24 +25,21 @@ export function objWaterDripSource({ delayMin, delayMax }: ObjWaterDripSourceArg }) } +const atkPoisonDrip = RpgAttack.create({ + poison: 5, + versus: RpgFaction.Anyone, +}); + function objWaterDrip(poison: boolean) { const obj = Sprite.from(poison ? Tx.Effects.PoisonDripSmall : Tx.Effects.WaterDripSmall) .mixin(mxnPhysics, { gravity: 0.05, physicsRadius: 4, physicsOffset: [0, -2], onMove: (ev) => { - if (poison) { - // TODO should be a more terse way to accomplish this! - for (const instance of Instances(mxnRpgStatus)) { - if (obj.collidesOne(instance.hurtboxes)) { - instance.poison(5); - // TODO sfx - return obj.destroy(); - } - } - } if (ev.hitGround) { obj.destroy(); // TODO drip sfx } - } }); + } }) + .mixin(mxnProjectile, { attack: atkPoisonDrip }) + .handles('hit', self => self.destroy()); return obj; } \ No newline at end of file diff --git a/src/igua/rpg/rpg-attack.ts b/src/igua/rpg/rpg-attack.ts new file mode 100644 index 0000000..a3e6f05 --- /dev/null +++ b/src/igua/rpg/rpg-attack.ts @@ -0,0 +1,20 @@ +import { Integer } from "../../lib/math/number-alias-types"; +import { RpgFaction } from "./rpg-faction"; + +export namespace RpgAttack { + export interface Model { + physical: Integer; + emotional: Integer; + poison: Integer; + versus: RpgFaction; + } + + export function create(model: Partial): Model { + return { + physical: model.physical ?? 0, + emotional: model.emotional ?? 0, + poison: model.poison ?? 0, + versus: model.versus ?? RpgFaction.Player, + } + } +} \ No newline at end of file diff --git a/src/igua/rpg/rpg-enemy.ts b/src/igua/rpg/rpg-enemy.ts index 7f2e5f0..a6fa08f 100644 --- a/src/igua/rpg/rpg-enemy.ts +++ b/src/igua/rpg/rpg-enemy.ts @@ -1,5 +1,6 @@ import { Integer } from "../../lib/math/number-alias-types"; import { playerObj } from "../objects/obj-player"; +import { RpgAttack } from "./rpg-attack"; import { RpgPlayer } from "./rpg-player"; export namespace RpgEnemy { @@ -10,23 +11,14 @@ export namespace RpgEnemy { } export const Methods = { - strikePlayer(model: Model, poison: Integer, physical: Integer, emotional: Integer) { + strikePlayer(model: Model, attack: RpgAttack.Model) { if (!playerObj) return; - // TODO not sure if shameCount should increase when player is invulnerable - // Or if a "slow poison" should increase the shameCount - // Maybe shameCount should only increase when the player was previously vulnerable and becomes invulnerable - // But capture that in a cooler way + // TODO not sure if a "slow poison" should increase the shameCount - // TODO feels bad!!! Fix!!! - const wasInvulnerable = RpgPlayer.Model.invulnerable > 0; - - // TODO use poison, physical, emotional - playerObj.damage(physical); - - // TODO feels bad!!!! Should come from result of damage, I think!!! - if (!wasInvulnerable && RpgPlayer.Model.invulnerable > 0) + const result = playerObj.damage(attack); + if (!result.rejected && result.damaged) model.shameCount++; } } diff --git a/src/igua/rpg/rpg-faction.ts b/src/igua/rpg/rpg-faction.ts new file mode 100644 index 0000000..0e06be9 --- /dev/null +++ b/src/igua/rpg/rpg-faction.ts @@ -0,0 +1,5 @@ +export enum RpgFaction { + Player, + Enemy, + Anyone, +} \ No newline at end of file diff --git a/src/igua/rpg/rpg-player.ts b/src/igua/rpg/rpg-player.ts index 48d283a..5b3b59c 100644 --- a/src/igua/rpg/rpg-player.ts +++ b/src/igua/rpg/rpg-player.ts @@ -1,3 +1,5 @@ +import { RpgAttack } from "./rpg-attack"; +import { RpgFaction } from "./rpg-faction"; import { RpgProgress } from "./rpg-progress"; import { RpgStatus } from "./rpg-status"; @@ -34,7 +36,11 @@ export const RpgPlayer = { set value(value) { RpgProgress.character.status.poison.value = value; } - } + }, + faction: RpgFaction.Player, + quirks: { + emotionalDamageIsFatal: true, + }, } satisfies RpgStatus.Model, get WalkingTopSpeed() { let speed = 2.5; @@ -42,4 +48,12 @@ export const RpgPlayer = { speed += 0.5 * Math.max(0, RpgProgress.character.status.poison.level - 1); return speed }, + MeleeAttack: { + emotional: 0, + get physical() { + return 5 + RpgProgress.character.attributes.strength * 5; + }, + poison: 0, + versus: RpgFaction.Enemy, + } satisfies RpgAttack.Model, } \ No newline at end of file diff --git a/src/igua/rpg/rpg-progress.ts b/src/igua/rpg/rpg-progress.ts index bf5e22c..87833ed 100644 --- a/src/igua/rpg/rpg-progress.ts +++ b/src/igua/rpg/rpg-progress.ts @@ -16,6 +16,7 @@ function getInitialRpgProgress() { attributes: { health: 1, intelligence: 0, + strength: 1, }, looks: getDefaultLooks(), position: { diff --git a/src/igua/rpg/rpg-status.ts b/src/igua/rpg/rpg-status.ts index 511aa75..cd6b1e4 100644 --- a/src/igua/rpg/rpg-status.ts +++ b/src/igua/rpg/rpg-status.ts @@ -1,9 +1,13 @@ +import { RpgAttack } from "./rpg-attack"; +import { RpgFaction } from "./rpg-faction"; + export namespace RpgStatus { const Consts = { FullyPoisonedHealth: 5, } export interface Model { + faction: RpgFaction; health: number; healthMax: number; invulnerable: number; @@ -12,7 +16,10 @@ export namespace RpgStatus { level: number; value: number; max: number; - } + }; + quirks: { + emotionalDamageIsFatal: boolean; + }; } export enum DamageKind { @@ -24,7 +31,23 @@ export namespace RpgStatus { export interface Effects { healed(value: number, delta: number): void; tookDamage(value: number, delta: number, kind: DamageKind): void; + // TODO IDK!!! I think died() needs to be here!! + } + + interface DamageAccepted { + rejected: false; + ailments?: boolean; + damaged?: boolean; + died?: boolean; + } + + interface DamageRejected { + rejected: true; + wrongFaction?: boolean; + invulnerable?: boolean; } + + type DamageResult = DamageAccepted | DamageRejected; export const Methods = { tick(model: Model, effects: Effects, count: number) { @@ -42,26 +65,53 @@ export namespace RpgStatus { }, // TODO I think API of damage methods should pass ALL damage types, build ups - damage(model: Model, effects: Effects, amount: number, kind = DamageKind.Physical) { + damage(model: Model, effects: Effects, attack: RpgAttack.Model): DamageResult { + if (attack.versus !== RpgFaction.Anyone && attack.versus !== model.faction) + return { rejected: true, wrongFaction: true }; + + const ailments = attack.poison > 0; + + model.poison.value += attack.poison; + if (model.poison.value >= model.poison.max) { + model.poison.value = 0; + model.poison.level += 1; + } + // TODO should resistances to damage be factored here? // Or should that be computed in a previous step? // TODO warn when amount is not an integer + if (attack.physical === 0 && attack.emotional === 0) + return { rejected: false, ailments }; + if (model.invulnerable > 0) - return; - - // TODO - // Emotional damage should not kill enemies - // But can kill player - - const previous = model.health; - model.health = Math.max(0, model.health - amount); - const diff = previous - model.health; - - effects.tookDamage(model.health, diff, kind); + return { rejected: true, invulnerable: true } + + let damaged = false; + + { + const previous = model.health; + const min = model.quirks.emotionalDamageIsFatal ? 0 : 1; + model.health = Math.max(min, model.health - attack.emotional); + const diff = previous - model.health; + damaged ||= diff > 0; + + effects.tookDamage(model.health, diff, DamageKind.Emotional); + } + + { + const previous = model.health; + model.health = Math.max(0, model.health - attack.physical); + const diff = previous - model.health; + damaged ||= diff > 0; + + effects.tookDamage(model.health, diff, DamageKind.Physical); + } model.invulnerable = model.invulnerableMax; + + return { rejected: false, ailments, damaged, died: damaged && model.health <= 0 }; }, heal(model: Model, effects: Effects, amount: number) { @@ -73,15 +123,5 @@ export namespace RpgStatus { effects.healed(model.health, diff); }, - - poison(model: Model, effects: Effects, amount: number) { - // TODO warn when amount is not an integer - - model.poison.value += amount; - if (model.poison.value >= model.poison.max) { - model.poison.value = 0; - model.poison.level += 1; - } - } } } diff --git a/src/igua/scenes/player-test.ts b/src/igua/scenes/player-test.ts index ce5f871..9d51304 100644 --- a/src/igua/scenes/player-test.ts +++ b/src/igua/scenes/player-test.ts @@ -1,20 +1,19 @@ import { Sprite } from "pixi.js"; -import { createPlayerObj, playerObj } from "../objects/obj-player"; +import { playerObj } from "../objects/obj-player"; import { Tx } from "../../assets/textures"; import { mxnCutscene } from "../mixins/mxn-cutscene"; import { show } from "../cutscene/show"; -import { objPipe, objPipeSlope, objSolidBlock, objSolidSlope } from "../objects/obj-terrain"; -import { Input, layers, scene } from "../globals"; +import { Input } from "../globals"; import { Rng } from "../../lib/math/rng"; -import { container } from "../../lib/pixi/container"; -import { NoAtlasTx } from "../../assets/no-atlas-textures"; import { Lvl } from "../../assets/generated/levels/generated-level-data"; import { sleep } from "../../lib/game-engine/promise/sleep"; -import { objStatusBar } from "../objects/obj-status-bar"; import { RpgProgress } from "../rpg/rpg-progress"; import { objAngelBouncing } from "../objects/enemies/obj-angel-bouncing"; import { Instances } from "../../lib/game-engine/instances"; import { objWaterDripSource } from "../objects/obj-water-drip-source"; +import { mxnProjectile } from "../mixins/mxn-projectile"; +import { RpgAttack } from "../rpg/rpg-attack"; +import { RpgFaction } from "../rpg/rpg-faction"; export function PlayerTest() { Sprite.from(Tx.Placeholder).at(128, 128 - 14).mixin(mxnCutscene, async () => { @@ -34,22 +33,10 @@ export function PlayerTest() { LockedDoor.add(Rng.vunit().scale(4)); await sleep(60); } - }); + }) + .mixin(mxnProjectile, { attack: RpgAttack.create({ poison: 1, versus: RpgFaction.Anyone }) }); playerObj.step(() => { - if (Input.justWentDown('CastSpell')) { - playerObj.heal(20); - playerObj.poison(20); - } - // if (playerObj.collides(LockedDoor) && Rng.float() > 0.9) { - // playerObj.poison(10); - // } - if (playerObj.collides(LockedDoor)) { - playerObj.poison(1); - } - if (Input.justWentDown('InventoryMenuToggle')) { - playerObj.damage(20); - } if (Input.justWentDown('Jump')) console.log(JSON.parse(JSON.stringify(RpgProgress))) })