From e37402ff309311a14d7dd666d0d8b29517504017 Mon Sep 17 00:00:00 2001 From: David Gay Date: Sun, 6 Jun 2021 19:06:46 -0400 Subject: Leviathans --- app/errors/monster_spawn_error.rb | 2 + app/lib/activity_processor.rb | 57 +++++++++++++++++----- app/models/location.rb | 1 + app/models/monster_spawn.rb | 35 +++++++++++++ app/models/monster_spawn_combat.rb | 6 +++ app/views/application/_results.html.erb | 3 ++ app/views/locations/show.html.erb | 10 ++++ data/activities.yml | 18 +++++++ data/items.yml | 6 +++ data/monsters.yml | 43 ++++++++++++++++ db/migrate/20210606215806_create_monster_spawns.rb | 10 ++++ .../20210606215818_create_monster_spawn_combats.rb | 11 +++++ db/schema.rb | 25 +++++++++- test/fixtures/monster_spawn_combats.yml | 11 +++++ test/fixtures/monster_spawns.yml | 9 ++++ test/models/monster_spawn_combat_test.rb | 7 +++ test/models/monster_spawn_test.rb | 7 +++ 17 files changed, 249 insertions(+), 12 deletions(-) create mode 100644 app/errors/monster_spawn_error.rb create mode 100644 app/models/monster_spawn.rb create mode 100644 app/models/monster_spawn_combat.rb create mode 100644 db/migrate/20210606215806_create_monster_spawns.rb create mode 100644 db/migrate/20210606215818_create_monster_spawn_combats.rb create mode 100644 test/fixtures/monster_spawn_combats.yml create mode 100644 test/fixtures/monster_spawns.yml create mode 100644 test/models/monster_spawn_combat_test.rb create mode 100644 test/models/monster_spawn_test.rb diff --git a/app/errors/monster_spawn_error.rb b/app/errors/monster_spawn_error.rb new file mode 100644 index 0000000..5a6a667 --- /dev/null +++ b/app/errors/monster_spawn_error.rb @@ -0,0 +1,2 @@ +class MonsterSpawnError < StandardError +end diff --git a/app/lib/activity_processor.rb b/app/lib/activity_processor.rb index c0eaab1..11f0db0 100644 --- a/app/lib/activity_processor.rb +++ b/app/lib/activity_processor.rb @@ -39,6 +39,18 @@ class ActivityProcessor when "xp" puts "Result: #{result}" handle_xp_result(result) + when "monster_spawn" + raise TooManyWoundsError unless @character.can_fight? + next if rand > (result[:chance] || 1) + + @results.push({ type: "br" }) + + monster_spawn = MonsterSpawn.where(location: Location.find_by_gid(result[:location])).select(&:alive?).first + raise MonsterSpawnError unless monster_spawn + + @results.push({ type: type, monster_spawn: monster_spawn }) + resolve_combat_with(monster_spawn) + break when "monster" raise TooManyWoundsError unless @character.can_fight? next if rand > (result[:chance] || 1) @@ -129,6 +141,10 @@ class ActivityProcessor @character.stop_activity @results.replace([{ type: "error", message: "You can't fight in your condition. You'll have to heal a wound." }]) + rescue MonsterSpawnError + @character.stop_activity + @results.replace([{ type: "error", + message: "There are no living leviathans here." }]) end private @@ -193,9 +209,16 @@ class ActivityProcessor end def resolve_combat_with(mon) + + monster_spawn = nil + if mon.is_a? MonsterSpawn + monster_spawn = mon + mon = monster_spawn.monster + end + char = @character char_hp = @character.max_hp - mon_hp = mon.max_hp + mon_hp = monster_spawn.present? ? monster_spawn.remaining_hp : mon.max_hp combat_message = ->(msg) { @results.push({ type: "message", body: "[#{char_hp}/#{char.max_hp}] #{msg}" }) } char_initiative = roll(20) + char.speed mon_initative = roll(20) + mon.speed @@ -244,6 +267,13 @@ class ActivityProcessor combat_message.call("#{target.name} evaded #{actor.name}'s attack.") end if char_hp < 1 || mon_hp < 1 + if monster_spawn + hp_lost = monster_spawn.remaining_hp - mon_hp + if hp_lost > 0 + monster_spawn.monster_spawn_combats.create(monster_spawn: monster_spawn, character: char, + hp_lost: monster_spawn.remaining_hp - mon_hp) + end + end if char_hp < 1 @results.push({ type: "message", body: "You were defeated! You retreat, wounded." }) char.wounds += 1 @@ -256,16 +286,21 @@ class ActivityProcessor end else @results.push({ type: "message", body: "You slew the #{mon.name}." }) - mon.whatnot[:awards]&.each do |award_data| - case award_data[:type] - when "title" - handle_title_result(award_data) - when "xp" - handle_xp_result(award_data) - when "item" - handle_item_result(award_data) - else - raise "Invalid award type string (#{award_data[:type]})" + if monster_spawn + char.stop_activity + return + else + mon.whatnot[:awards]&.each do |award_data| + case award_data[:type] + when "title" + handle_title_result(award_data) + when "xp" + handle_xp_result(award_data) + when "item" + handle_item_result(award_data) + else + raise "Invalid award type string (#{award_data[:type]})" + end end end end diff --git a/app/models/location.rb b/app/models/location.rb index b7c5bf5..e008270 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -2,5 +2,6 @@ class Location < ApplicationRecord include HasWhatnot has_many :activities + has_many :monster_spawns validates :gid, :name, presence: true end diff --git a/app/models/monster_spawn.rb b/app/models/monster_spawn.rb new file mode 100644 index 0000000..d34e390 --- /dev/null +++ b/app/models/monster_spawn.rb @@ -0,0 +1,35 @@ +class MonsterSpawn < ApplicationRecord + belongs_to :monster + belongs_to :location + has_many :monster_spawn_combats + + after_update :check_hp + after_create :send_chat_message + + def alive? + self.remaining_hp > 0 + end + + def remaining_hp + self.monster.max_hp - MonsterSpawnCombat.where(monster_spawn: self).sum(:hp_lost) + end + + private + def send_chat_message + chat_message = ChatMessage.new(body: "A leviathan has appeared in #{location.name}!", + chat_room: ChatRoom.find_by_gid("news")) + if chat_message.save + ChatRoomChannel.broadcast_chat_message(chat_message) + end + end + + def check_hp + if alive? + chat_message = ChatMessage.new(body: "The #{monster.name} in #{location.name} has been slain!", + chat_room: ChatRoom.find_by_gid("news")) + if chat_message.save + ChatRoomChannel.broadcast_chat_message(chat_message) + end + end + end +end diff --git a/app/models/monster_spawn_combat.rb b/app/models/monster_spawn_combat.rb new file mode 100644 index 0000000..50a565b --- /dev/null +++ b/app/models/monster_spawn_combat.rb @@ -0,0 +1,6 @@ +class MonsterSpawnCombat < ApplicationRecord + belongs_to :monster_spawn + belongs_to :character + + validates :hp_lost, numericality: { greater_than_or_equal_to: 0, only_integer: true } +end diff --git a/app/views/application/_results.html.erb b/app/views/application/_results.html.erb index 31fb93e..ec62991 100644 --- a/app/views/application/_results.html.erb +++ b/app/views/application/_results.html.erb @@ -17,6 +17,9 @@ <% when "monster" %>

You encountered a <%= result[:monster].name %>.

<%= result[:monster].description %>

+ <% when "monster_spawn" %> +

You found the <%= result[:monster_spawn].monster.name %>!

+

<%= result[:monster_spawn].monster.description %>

<% when "xp" %>

You gained <%= result[:xp] %> <%= result[:skill].name %> XP.

<% when "title" %> diff --git a/app/views/locations/show.html.erb b/app/views/locations/show.html.erb index ff7b2a8..fcc668a 100644 --- a/app/views/locations/show.html.erb +++ b/app/views/locations/show.html.erb @@ -3,6 +3,16 @@

<%= @location.description %>

+<% @location.monster_spawns.select(&:alive?).each do |ms| %> +

A <%= ms.monster.name %> is ravaging this location! (<%= ms.remaining_hp %> HP)

+ <%# TODO: HACK %> + <% activity = Activity.find_by_gid("beastslay_leviathan_floret_region") %> + <%= form_with url: start_activity_path(activity) do |f| %> + <%= f.hidden_field :id, value: activity.id %> + <%= f.submit "Hunt" %> + <% end %> +<% end %> + <% @location.activities.order(:name).each do |activity| %>

<%= link_to activity.name, activity_path(activity) %>

diff --git a/data/activities.yml b/data/activities.yml index 9e1fb8c..1ab47e3 100644 --- a/data/activities.yml +++ b/data/activities.yml @@ -2453,3 +2453,21 @@ craft_midoras_mudtub_mash: xp: - gid: "spicework" value: 10 +beastslay_leviathan_floret_region: + name: "Hunt a Leviathan in the Floret Region" + description: "You are hunting down a leviathan ravaging the Floret Region." + whatnot: + duration: + base: 80 + minimum: 40 + scaling: + - type: "skill" + gid: "beastslay" + scale_value: 1 + - type: "stat" + gid: "beastslay_speed" + scale_value: 1 + results: + - type: "monster_spawn" + location: "floret_region" + chance: 1 diff --git a/data/items.yml b/data/items.yml index dcfe80a..255bd1c 100644 --- a/data/items.yml +++ b/data/items.yml @@ -1010,3 +1010,9 @@ onus_of_vision: - type: "stat_change" gid: "speed" modifier: -2 +balgoloth_skull: + name: "balgoloth skull" + description: "The enormous skull of a balgoloth. About as big as your entire upper body." + whatnot: + tags: + - "material" diff --git a/data/monsters.yml b/data/monsters.yml index 8185c59..59024d6 100644 --- a/data/monsters.yml +++ b/data/monsters.yml @@ -267,3 +267,46 @@ crypt_writhe: - type: "item" chance: 0.001 gid: "crypt_writhe_trophy" +balgoloth: + name: "balgoloth" + description: >- + A fur-covered, bipedal creature standing at least twenty feet tall. It strides across the lands with great + sloth, brandishing thick, foot-long claws. It can smell thoughts from miles away, and the inner machinations of + anything smarter than a grinpad enrages it. It only returns to calm once the thoughts have been silenced. + whatnot: + tags: + - "leviathan" + max_hp: + base: 250 + speed: + base: 20 + accuracy: + base: 25 + power: + base: 40 + evasion: + base: 12 + resistances: + - gid: "physical" + base: 40 + - gid: "energy" + base: 20 + - gid: "fire" + base: -20 + hit_effects: + - type: "damage" + gid: "bash" + min: 20 + max: 40 + awards: + - type: "xp" + gid: "beastslay" + base: 100 + - type: "item" + chance: 1 + gid: "balgoloth_skull" + - type: "item" + chance: 1 + gid: "vestige" + min_quantity: 250 + max_quantity: 500 diff --git a/db/migrate/20210606215806_create_monster_spawns.rb b/db/migrate/20210606215806_create_monster_spawns.rb new file mode 100644 index 0000000..96ccefe --- /dev/null +++ b/db/migrate/20210606215806_create_monster_spawns.rb @@ -0,0 +1,10 @@ +class CreateMonsterSpawns < ActiveRecord::Migration[6.1] + def change + create_table :monster_spawns do |t| + t.references :monster, null: false, foreign_key: true + t.references :location, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20210606215818_create_monster_spawn_combats.rb b/db/migrate/20210606215818_create_monster_spawn_combats.rb new file mode 100644 index 0000000..a2dd894 --- /dev/null +++ b/db/migrate/20210606215818_create_monster_spawn_combats.rb @@ -0,0 +1,11 @@ +class CreateMonsterSpawnCombats < ActiveRecord::Migration[6.1] + def change + create_table :monster_spawn_combats do |t| + t.references :monster_spawn, null: false, foreign_key: true + t.references :character, null: false, foreign_key: true + t.integer :hp_lost + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 55a66d9..6e34c95 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_06_06_214340) do +ActiveRecord::Schema.define(version: 2021_06_06_215818) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -196,6 +196,25 @@ ActiveRecord::Schema.define(version: 2021_06_06_214340) do t.index ["sender_id"], name: "index_messages_on_sender_id" end + create_table "monster_spawn_combats", force: :cascade do |t| + t.bigint "monster_spawn_id", null: false + t.bigint "character_id", null: false + t.integer "hp_lost" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["character_id"], name: "index_monster_spawn_combats_on_character_id" + t.index ["monster_spawn_id"], name: "index_monster_spawn_combats_on_monster_spawn_id" + end + + create_table "monster_spawns", force: :cascade do |t| + t.bigint "monster_id", null: false + t.bigint "location_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["location_id"], name: "index_monster_spawns_on_location_id" + t.index ["monster_id"], name: "index_monster_spawns_on_monster_id" + end + create_table "monsters", force: :cascade do |t| t.string "gid" t.string "name" @@ -292,6 +311,10 @@ ActiveRecord::Schema.define(version: 2021_06_06_214340) do add_foreign_key "learned_activities", "characters" add_foreign_key "messages", "characters", column: "recipient_id" add_foreign_key "messages", "characters", column: "sender_id" + add_foreign_key "monster_spawn_combats", "characters" + add_foreign_key "monster_spawn_combats", "monster_spawns" + add_foreign_key "monster_spawns", "locations" + add_foreign_key "monster_spawns", "monsters" add_foreign_key "states", "characters" add_foreign_key "states", "conditions" add_foreign_key "title_awards", "characters" diff --git a/test/fixtures/monster_spawn_combats.yml b/test/fixtures/monster_spawn_combats.yml new file mode 100644 index 0000000..66d1fff --- /dev/null +++ b/test/fixtures/monster_spawn_combats.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + monster_spawn: one + character: one + hp_lost: 1 + +two: + monster_spawn: two + character: two + hp_lost: 1 diff --git a/test/fixtures/monster_spawns.yml b/test/fixtures/monster_spawns.yml new file mode 100644 index 0000000..4106695 --- /dev/null +++ b/test/fixtures/monster_spawns.yml @@ -0,0 +1,9 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + monster: one + location: one + +two: + monster: two + location: two diff --git a/test/models/monster_spawn_combat_test.rb b/test/models/monster_spawn_combat_test.rb new file mode 100644 index 0000000..7717a34 --- /dev/null +++ b/test/models/monster_spawn_combat_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class MonsterSpawnCombatTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/monster_spawn_test.rb b/test/models/monster_spawn_test.rb new file mode 100644 index 0000000..07d8c85 --- /dev/null +++ b/test/models/monster_spawn_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class MonsterSpawnTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end -- cgit v1.2.3