diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a1b1b0..d94350f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,7 @@ add_executable(game src/components/timer.h src/components/timer.cpp src/components/enemy.h -) + "src/components/ghost_frog.h" "src/components/ghost_frog.cpp" "src/components/orb.h" "src/components/orb.cpp") # Reference blah target_link_libraries(game blah) diff --git a/content/map/12x0.png b/content/map/12x0.png index 491b93b..d51bc1e 100644 Binary files a/content/map/12x0.png and b/content/map/12x0.png differ diff --git a/content/sprites/ghostfrog.ase b/content/sprites/ghostfrog.ase new file mode 100644 index 0000000..d014a78 Binary files /dev/null and b/content/sprites/ghostfrog.ase differ diff --git a/src/components/animator.cpp b/src/components/animator.cpp index e387f8a..8e1e700 100644 --- a/src/components/animator.cpp +++ b/src/components/animator.cpp @@ -72,7 +72,7 @@ void Animator::render(Batch& batch) if (in_valid_state()) { batch.push_matrix( - Mat3x2::create_transform(entity()->position, m_sprite->origin, scale, 0)); + Mat3x2::create_transform(entity()->position + offset, m_sprite->origin, scale, 0)); auto& anim = m_sprite->animations[m_animation_index]; auto& frame = anim.frames[m_frame_index]; diff --git a/src/components/animator.h b/src/components/animator.h index 3e9aab8..5875637 100644 --- a/src/components/animator.h +++ b/src/components/animator.h @@ -17,6 +17,7 @@ namespace TL public: Vec2 scale = Vec2::one; + Point offset = Point::zero; Animator() = default; Animator(const String& sprite); diff --git a/src/components/collider.cpp b/src/components/collider.cpp index 2d708b6..3fa490d 100644 --- a/src/components/collider.cpp +++ b/src/components/collider.cpp @@ -71,6 +71,11 @@ void Collider::set_cells(int x, int y, int w, int h, bool value) } bool Collider::check(uint32_t mask, Point offset) const +{ + return first(mask, offset) != nullptr; +} + +Collider* Collider::first(uint32_t mask, Point offset) { if (world()) { @@ -80,13 +85,32 @@ bool Collider::check(uint32_t mask, Point offset) const if (other != this && (other->mask & mask) == mask && overlaps(other, offset)) - return true; + return other; other = (Collider*)other->next(); } } - return false; + return nullptr; +} + +const Collider* Collider::first(uint32_t mask, Point offset) const +{ + if (world()) + { + auto other = world()->first(); + while (other) + { + if (other != this && + (other->mask & mask) == mask && + overlaps(other, offset)) + return other; + + other = (Collider*)other->next(); + } + } + + return nullptr; } bool Collider::overlaps(const Collider* other, Point offset) const diff --git a/src/components/collider.h b/src/components/collider.h index 0b46348..17abcf5 100644 --- a/src/components/collider.h +++ b/src/components/collider.h @@ -31,6 +31,8 @@ namespace TL void set_cell(int x, int y, bool value); void set_cells(int x, int y, int w, int h, bool value); + Collider* first(uint32_t mask, Point offset = Point::zero); + const Collider* first(uint32_t mask, Point offset = Point::zero) const; bool check(uint32_t mask, Point offset = Point::zero) const; bool overlaps(const Collider* other, Point offset = Point::zero) const; diff --git a/src/components/ghost_frog.cpp b/src/components/ghost_frog.cpp new file mode 100644 index 0000000..ea8f135 --- /dev/null +++ b/src/components/ghost_frog.cpp @@ -0,0 +1,268 @@ +#include "ghost_frog.h" +#include "mover.h" +#include "hurtable.h" +#include "player.h" +#include "animator.h" +#include "orb.h" +#include "../masks.h" +#include "../factory.h" + +using namespace TL; + +GhostFrog::GhostFrog() +{ + +} + +void GhostFrog::awake() +{ + m_home = entity()->position; +} + +void GhostFrog::update() +{ + m_timer += Time::delta; + + auto player = world()->first(); + auto mover = get(); + auto anim = get(); + auto hitbox = get(); + + // no player - turn off AI + if (!player) + return; + + auto x = entity()->position.x; + auto y = entity()->position.y; + auto player_x = player->entity()->position.x; + + // update sprite + if (mover->on_ground()) + { + + } + + // flip sprite + anim->scale = Vec2(m_facing, 1); + + // NORMAL STATE + if (m_state == st_readying_attack) + { + m_facing = Calc::sign(player_x - x); + if (m_facing == 0) + m_facing = 1; + + float target_x = player_x + 32 * -m_facing; + mover->speed.x = Calc::approach(mover->speed.x, Calc::sign(target_x - x) * 40, 400 * Time::delta); + mover->friction = 100; + anim->play("run"); + + if (m_timer > 3.0f || + (m_timer > 1.0f && hitbox->check(Mask::solid, Point(-m_facing * 8, 0)))) + { + mover->speed.x = 0; + set_state(st_perform_slash); + } + } + // SLASH STATE + else if (m_state == st_perform_slash) + { + // start attack anim + anim->play("attack"); + mover->friction = 500; + + // after 0.8s, do the lunge + if (Time::on_time(m_timer, 0.8f)) + { + mover->speed.x = m_facing * 250; + hitbox->set_rect(RectI(-4 + m_facing * 4, -12, 8, 12)); + + RectI rect(8, -8, 20, 8); + if (m_facing < 0) + rect.x = -(rect.x + rect.w); + + if (m_attack_collider) + m_attack_collider->destroy(); + m_attack_collider = entity()->add(Collider::make_rect(rect)); + m_attack_collider->mask = Mask::enemy; + } + // turn off attack collider + else if (Time::on_time(m_timer, anim->animation()->duration() - 1.0f)) + { + if (m_attack_collider) + m_attack_collider->destroy(); + m_attack_collider = nullptr; + } + // end attack state + else if (m_timer >= anim->animation()->duration()) + { + hitbox->set_rect(RectI(-4, -12, 8, 12)); + + if (health > 0) + { + set_state(st_readying_attack); + } + else + { + phase = 1; + health = max_health_2; + m_side = Calc::rand_int(0, 2) == 0 ? -1 : 1; + set_state(st_floating); + } + } + } + // FLOATING STATE + else if (m_state == st_floating) + { + anim->play("float"); + + mover->friction = 0; + mover->collider = nullptr; + + float target_y = m_home.y - 50; + float target_x = m_home.x + m_side * 50; + + if (Calc::sign(target_y - y) != Calc::sign(target_y - m_last_pos.y)) + { + mover->speed.y = 0; + entity()->position.y = target_y; + } + else + mover->speed.y = Calc::approach(mover->speed.y, Calc::sign(target_y - y) * 50, 800 * Time::delta); + + if (Calc::abs(y - target_y) < 8) + mover->speed.x = Calc::approach(mover->speed.x, Calc::sign(target_x - x) * 80, 800 * Time::delta); + else + mover->speed.x = 0; + + if (m_timer > 5.0f || (Calc::abs(target_x - x) < 8 && Calc::abs(target_y - y) < 8)) + set_state(st_shoot); + } + // SHOOTING STATE + else if (m_state == st_shoot) + { + mover->speed = Calc::approach(mover->speed, Vec2::zero, 300 * Time::delta); + + m_facing = Calc::sign(player_x - x); + if (m_facing == 0) + m_facing = 1; + + if (Time::on_time(m_timer, 1.0f)) + { + anim->play("reflect"); + } + else if (Time::on_time(m_timer, 1.2f)) + { + Factory::orb(world(), entity()->position + Point(m_facing * 12, -8)); + m_reflect_count = 0; + } + else if (Time::on_time(m_timer, 1.4f)) + { + anim->play("float"); + set_state(st_reflect); + } + } + // REFLECT STATE + else if (m_state == st_reflect) + { + if (Time::on_time(m_timer, 0.4f)) + anim->play("float"); + + auto orb = world()->first(); + if (!orb) + { + if (m_timer > 1.0f) + { + m_side = -m_side; + set_state(st_floating); + } + } + else if (!orb->towards_player) + { + if (m_reflect_count < 2) + { + if (Vec2(orb->entity()->position - orb->target()).length() < 16) + { + anim->play("reflect"); + orb->on_hit(); + m_reflect_count++; + m_timer = 0; + } + } + else + { + if (Vec2(orb->entity()->position - orb->target()).length() < 8) + { + Factory::pop(world(), entity()->position + Point(0, -8)); + orb->entity()->destroy(); + on_hurt(nullptr); + m_timer = 0; + } + } + } + } + // DEAD STATE + else if (m_state == st_dead_state) + { + anim->play("dead"); + + if (Time::on_interval(0.25f)) + { + auto offset = Point(Calc::rand_int(-16, 16), Calc::rand_int(-16, 16)); + Factory::pop(world(), entity()->position + Point(0, -8) + offset); + } + + if (Time::on_time(m_timer, 3.0f)) + { + for (int x = -1; x < 2; x ++) + for (int y = -1; y < 2; y ++) + Factory::pop(world(), entity()->position + Point(x * 12, -8 + y * 12)); + + Time::pause_for(0.3f); + entity()->destroy(); + } + } + + if (m_state == st_floating || m_state == st_shoot || m_state == st_reflect) + { + anim->offset.y = Calc::sin(Time::elapsed * 2) * 3; + } + + m_last_pos = entity()->position; +} + +void GhostFrog::on_hurt(Hurtable* hurtable) +{ + if (health > 0) + { + health--; + + if (health <= 0 && phase > 0) + { + set_state(st_dead_state); + } + + if (m_state == st_waiting) + { + Factory::pop(world(), entity()->position + Point(0, -8)); + Time::pause_for(0.25f); + set_state(st_readying_attack); + } + } +} + +void GhostFrog::on_hit_x(Mover* mover) +{ + +} + +void GhostFrog::on_hit_y(Mover* mover) +{ + +} + +void GhostFrog::set_state(int state) +{ + m_state = state; + m_timer = 0; +} diff --git a/src/components/ghost_frog.h b/src/components/ghost_frog.h new file mode 100644 index 0000000..ead3b84 --- /dev/null +++ b/src/components/ghost_frog.h @@ -0,0 +1,48 @@ +#pragma once +#include "../world.h" +#include + +using namespace Blah; + +namespace TL +{ + class Hurtable; + class Mover; + class Collider; + + class GhostFrog : public Component + { + public: + static constexpr int st_waiting = 0; + static constexpr int st_readying_attack = 1; + static constexpr int st_perform_slash = 2; + static constexpr int st_floating = 3; + static constexpr int st_shoot = 4; + static constexpr int st_reflect = 5; + static constexpr int st_dead_state = 6; + + static constexpr int max_health_1 = 10; + static constexpr int max_health_2 = 3; + int health = max_health_1; + int phase = 0; + + GhostFrog(); + + void awake() override; + void update() override; + void on_hurt(Hurtable* hurtable); + void on_hit_x(Mover* mover); + void on_hit_y(Mover* mover); + + private: + void set_state(int state); + float m_timer = 0; + int m_state = st_waiting; + int m_facing = 1; + int m_side = 1; + int m_reflect_count = 0; + Point m_home; + Point m_last_pos; + Collider* m_attack_collider = nullptr; + }; +} \ No newline at end of file diff --git a/src/components/orb.cpp b/src/components/orb.cpp new file mode 100644 index 0000000..0faf6dc --- /dev/null +++ b/src/components/orb.cpp @@ -0,0 +1,42 @@ +#include "orb.h" +#include "mover.h" +#include "hurtable.h" +#include "player.h" +#include "ghost_frog.h" +#include "../masks.h" +#include "../factory.h" + +using namespace TL; + +Point Orb::target() const +{ + auto player = world()->first(); + auto ghost = world()->first(); + + if (player && ghost) + return (towards_player ? player->entity()->position : ghost->entity()->position) + Point(0, -8); + + return Point(0, 0); +} + +void Orb::update() +{ + auto mover = get(); + auto diff = Vec2(target() - entity()->position).normal(); + mover->speed = diff * speed; +} + +void Orb::destroyed() +{ + Factory::pop(world(), entity()->position); +} + +void Orb::on_hit() +{ + towards_player = !towards_player; + speed += 40; + + auto hurt = get(); + if (towards_player) + hurt->stun_timer = 0; +} \ No newline at end of file diff --git a/src/components/orb.h b/src/components/orb.h new file mode 100644 index 0000000..943de8f --- /dev/null +++ b/src/components/orb.h @@ -0,0 +1,20 @@ +#pragma once +#include "../world.h" +#include + +using namespace Blah; + +namespace TL +{ + class Orb : public Component + { + public: + float speed = 40; + bool towards_player = true; + + Point target() const; + void update() override; + void destroyed() override; + void on_hit(); + }; +} \ No newline at end of file diff --git a/src/components/player.cpp b/src/components/player.cpp index 65ba487..c2b6872 100644 --- a/src/components/player.cpp +++ b/src/components/player.cpp @@ -2,6 +2,7 @@ #include "mover.h" #include "animator.h" #include "collider.h" +#include "orb.h" #include "../masks.h" using namespace TL; @@ -220,22 +221,31 @@ void Player::update() } // Hurt Check! - if (m_invincible_timer <= 0 && hitbox->check(Mask::enemy)) + if (m_invincible_timer <= 0) { - Time::pause_for(0.1f); - anim->play("hurt"); - - if (m_attack_collider) + auto hit = hitbox->first(Mask::enemy); + if (hit) { - m_attack_collider->destroy(); - m_attack_collider = nullptr; + Time::pause_for(0.1f); + anim->play("hurt"); + + if (m_attack_collider) + { + m_attack_collider->destroy(); + m_attack_collider = nullptr; + } + + mover->speed = Vec2(-m_facing * 100, -80); + + health--; + m_hurt_timer = hurt_duration; + m_invincible_timer = invincible_duration; + m_state = st_hurt; + + // hack: + // destroy orb + if (hit->get()) + hit->entity()->destroy(); } - - mover->speed = Vec2(-m_facing * 100, -80); - - health--; - m_hurt_timer = hurt_duration; - m_invincible_timer = invincible_duration; - m_state = st_hurt; } } diff --git a/src/components/player.h b/src/components/player.h index 03945af..5d4aea9 100644 --- a/src/components/player.h +++ b/src/components/player.h @@ -14,7 +14,7 @@ namespace TL static constexpr int st_attack = 1; static constexpr int st_hurt = 2; static constexpr int st_start = 3; - static constexpr int max_health = 3; + static constexpr int max_health = 30; int health = max_health; diff --git a/src/factory.cpp b/src/factory.cpp index 286de16..e8b4622 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -7,6 +7,8 @@ #include "components/hurtable.h" #include "components/timer.h" #include "components/enemy.h" +#include "components/ghost_frog.h" +#include "components/orb.h" using namespace TL; @@ -19,6 +21,7 @@ Entity* Factory::player(World* world, Point position) anim->depth = -10; auto hitbox = en->add(Collider::make_rect(RectI(-4, -12, 8, 12))); + hitbox->mask = Mask::player; auto mover = en->add(Mover()); mover->collider = hitbox; @@ -334,3 +337,55 @@ Entity* Factory::blob(World* world, Point position) return en; } +Entity* Factory::ghost_frog(World* world, Point position) +{ + auto en = world->add_entity(position); + en->add(GhostFrog()); + en->add(Enemy()); + + auto anim = en->add(Animator("ghostfrog")); + anim->play("sword"); + anim->depth = -5; + + auto hitbox = en->add(Collider::make_rect(RectI(-4, -12, 8, 12))); + hitbox->mask = Mask::enemy; + + auto mover = en->add(Mover()); + mover->collider = hitbox; + mover->gravity = 0; + mover->friction = 100; + mover->on_hit_x = [](Mover* self) { self->get()->on_hit_x(self); }; + mover->on_hit_y = [](Mover* self) { self->get()->on_hit_y(self); }; + + auto hurtable = en->add(Hurtable()); + hurtable->hurt_by = Mask::player_attack; + hurtable->collider = hitbox; + hurtable->on_hurt = [](Hurtable* self) { self->get()->on_hurt(self); }; + + return en; +} + +Entity* Factory::orb(World* world, Point position) +{ + auto en = world->add_entity(position); + en->add(Orb()); + + auto anim = en->add(Animator("bullet")); + anim->play("idle"); + anim->depth = -5; + + auto hitbox = en->add(Collider::make_rect(RectI(-4, -4, 8, 8))); + hitbox->mask = Mask::enemy; + + auto mover = en->add(Mover()); + mover->collider = hitbox; + mover->on_hit_x = [](Mover* self) { Factory::pop(self->world(), self->entity()->position); self->entity()->destroy(); }; + mover->on_hit_y = [](Mover* self) { Factory::pop(self->world(), self->entity()->position); self->entity()->destroy(); }; + + auto hurtable = en->add(Hurtable()); + hurtable->hurt_by = Mask::player_attack; + hurtable->collider = en->add(Collider::make_rect(RectI(-8, -8, 16, 16))); + hurtable->on_hurt = [](Hurtable* self) { self->get()->on_hit(); }; + + return en; +} diff --git a/src/factory.h b/src/factory.h index fcb49d7..2bf4807 100644 --- a/src/factory.h +++ b/src/factory.h @@ -16,5 +16,7 @@ namespace TL Entity* mosquito(World* world, Point position); Entity* door(World* world, Point position, bool wait_for_player = false); Entity* blob(World* world, Point position); + Entity* ghost_frog(World* world, Point position); + Entity* orb(World* world, Point position); } } \ No newline at end of file diff --git a/src/game.cpp b/src/game.cpp index 64a6d9d..e7b8891 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -29,7 +29,7 @@ void Game::startup() m_draw_colliders = false; // load first room - load_room(Point(11, 0)); + load_room(Point(12, 0)); camera = Vec2(room.x * width, room.y * height); } @@ -132,13 +132,17 @@ void Game::load_room(Point cell, bool is_reload) // closing door case 0x847e87: - Factory::door(&world, world_position, true); + Factory::door(&world, world_position, !is_reload); break; // blob case 0x3f3f74: Factory::blob(&world, world_position); break; + + case 0x76428a: + Factory::ghost_frog(&world, world_position + Point(-4, 0)); + break; } } } diff --git a/src/masks.h b/src/masks.h index 16e6b24..30d6220 100644 --- a/src/masks.h +++ b/src/masks.h @@ -9,5 +9,6 @@ namespace TL static constexpr uint32_t jumpthru = 1 << 1; static constexpr uint32_t player_attack = 1 << 2; static constexpr uint32_t enemy = 1 << 3; + static constexpr uint32_t player = 1 << 4; }; } \ No newline at end of file diff --git a/src/world.h b/src/world.h index f80fa6e..d7a124d 100644 --- a/src/world.h +++ b/src/world.h @@ -245,6 +245,7 @@ namespace TL entity->m_components.push_back(instance); // and we're done! + instance->awake(); return instance; }