From 44facc2e567eb3c045ce082428f42276e45b0202 Mon Sep 17 00:00:00 2001 From: David Gay Date: Sat, 22 May 2021 18:10:19 -0400 Subject: Monsters and basic combat --- app/controllers/application_controller.rb | 4 ++ app/controllers/game_controller.rb | 80 ++++++++++++++++++++++ app/models/character.rb | 45 ++++++++++++ app/models/monster.rb | 31 +++++++++ app/views/activities/_results.html.erb | 2 + app/views/activities/show.html.erb | 2 +- data/activities.yml | 21 ++++++ data/monsters.yml | 48 +++++++++++++ .../20210522194937_add_wounds_to_characters.rb | 5 ++ db/migrate/20210522201259_create_monsters.rb | 12 ++++ db/schema.rb | 12 +++- db/seeds.rb | 5 ++ test/fixtures/monsters.yml | 11 +++ test/models/monster_test.rb | 7 ++ 14 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 app/models/monster.rb create mode 100644 data/monsters.yml create mode 100644 db/migrate/20210522194937_add_wounds_to_characters.rb create mode 100644 db/migrate/20210522201259_create_monsters.rb create mode 100644 test/fixtures/monsters.yml create mode 100644 test/models/monster_test.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index efd8427..81e07da 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,4 +10,8 @@ class ApplicationController < ActionController::Base def redirect_if_no_active_character redirect_to new_character_path unless current_char end + + def roll(sides) + rand(sides) + 1 + end end diff --git a/app/controllers/game_controller.rb b/app/controllers/game_controller.rb index 58644a7..c63fbc5 100644 --- a/app/controllers/game_controller.rb +++ b/app/controllers/game_controller.rb @@ -13,6 +13,23 @@ class GameController < ApplicationController activity.whatnot[:results].each do |result| type = result[:type] case type + when "monster" + next if rand > (result[:chance] || 1) + table_roll = rand + result[:table].sort_by { |t| -t[:score] }.each do |table_entry| + score = table_entry[:score] + result[:table_scaling]&.each do |scale_entry| + case scale_entry[:type] + when "skill" + score = score**(1 + (scale_entry[:scale_value] * current_char.skill_level(scale_entry[:gid]))) + end + end + if table_roll >= score + activity = Activity.find_by_gid(table_entry[:gid]) + monster = Monster.find_by_gid(table_entry[:gid]) + resolve_combat_with(monster) + end + end when "item" next if rand > (result[:chance] || 1) @@ -83,4 +100,67 @@ class GameController < ApplicationController current_char.shift_item(item, quantity) @results.push({ type: "item", item: item, quantity: quantity, xp: xp_awards }) end + + def resolve_combat_with(mon) + char = current_char + char_hp = current_char.max_hp + mon_hp = mon.max_hp + combat_message = ->(msg) { @results.push({ type: "message", body: "[#{char_hp}/#{char.max_hp}] #{msg}" }) } + combat_message.call("You encountered a #{mon.name}.") + char_initiative = roll(10) + char.speed + mon_initative = roll(10) + mon.speed + if char_initiative > mon_initative + turn_order = [char, mon] + elsif mon_initative > char_initiative + turn_order = [mon, char] + else + turn_order = [char, mon].shuffle + end + turn_order.cycle do |actor| + case actor + when char + accuracy_roll = roll(20) + char.accuracy + evasion_roll = roll(20) + mon.evasion + if accuracy_roll >= evasion_roll + dealt_damage = roll(4) + char.power # TODO: Replace d4 with weapon damage + if accuracy_roll >= (evasion_roll + 10) + combat_message.call("You landed a critical hit!") + dealt_damage = dealt_damage * 2 + end + blocked_damage = (accuracy_roll >= (roll(20) + mon.block)) ? 0 : mon.block_value + resolved_damage = dealt_damage - blocked_damage + mon_hp -= resolved_damage + combat_message.call("You hit for #{resolved_damage} (#{dealt_damage} - #{blocked_damage} blocked)") + elsif evasion_roll > accuracy_roll + combat_message.call("The #{mon.name} evaded your attack.") + end + when mon + combat_message.call("Monsters don't get turns yet.") + else + raise "Invalid combatant (class is #{actor.class})" + end + if char_hp < 1 || mon_hp < 1 + if char_hp < 1 + combat_message.call("You were defeated! You retreat, wounded.") + char.increment(:wounds) + else + combat_message.call("You defeated the #{mon.name}.") + mon.whatnot[:awards]&.each do |award_data| + case award_data[:type] + when "xp" + skill = Skill.find_by_gid(award_data[:skill]) + amount = award_data[:base] + char.add_skill_xp(skill, amount) + combat_message.call("You gained #{amount} #{skill.name} XP.") + else + raise "Invalid award type string (#{award_data[:type]})" + end + end + end + break + else + combat_message.call("-" * 20) + end + end + end end diff --git a/app/models/character.rb b/app/models/character.rb index 59ed5ae..bad2bb2 100644 --- a/app/models/character.rb +++ b/app/models/character.rb @@ -19,6 +19,22 @@ class Character < ApplicationRecord after_create :create_skills after_create { Hearth.create(character: self) } + def beastslay_level; skill_level("beastslay"); end + def fluxseethe_level; skill_level("fluxseethe"); end + def havencast_level; skill_level("havencast"); end + def hexcarve_level; skill_level("hexcarve"); end + def magiculture_level; skill_level("magiculture"); end + def manatrawl_level; skill_level("manatrawl"); end + def omenbind_level; skill_level("omenbind"); end + def otherforge_level; skill_level("otherforge"); end + def planequarry_level; skill_level("planequarry"); end + def spicework_level; skill_level("spicework"); end + def synthsever_level; skill_level("synthsever"); end + def veilreach_level; skill_level("veilreach"); end + def wealdreap_level; skill_level("wealdreap"); end + def wildscour_level; skill_level("wildscour"); end + def worldcall_level; skill_level("worldcall"); end + def shift_item(item, amount) item = Item.find_by_gid(item) if item.is_a? String CharacterItem.transaction do @@ -70,6 +86,7 @@ class Character < ApplicationRecord end def add_skill_xp(skill, amount) + skill = Skill.find_by_gid(skill) if skill.is_a? String CharacterSkill.find_by(skill: skill).increment!(:xp, amount) end @@ -115,6 +132,34 @@ class Character < ApplicationRecord self.update(activity: activity, activity_started_at: Time.now) if self.can_do_activity?(activity) end + def can_fight? + self.wounds < 1 + end + + def max_hp + 10 + self.beastslay_level + end + + def speed + self.beastslay_level + end + + def accuracy + self.beastslay_level + end + + def power + self.beastslay_level + end + + def evasion + self.beastslay_level + end + + def block + self.beastslay_level + end + private def create_skills Skill.all.each { |skill| self.character_skills.create(skill: skill, xp: 0) } diff --git a/app/models/monster.rb b/app/models/monster.rb new file mode 100644 index 0000000..1b521aa --- /dev/null +++ b/app/models/monster.rb @@ -0,0 +1,31 @@ +class Monster < ApplicationRecord + include HasWhatnot + + def max_hp + self.whatnot[:max_hp][:base] + end + + def speed + self.whatnot[:speed][:base] + end + + def accuracy + self.whatnot[:accuracy][:base] + end + + def power + self.whatnot[:power][:base] + end + + def evasion + self.whatnot[:evasion][:base] + end + + def block + self.whatnot[:block][:base] + end + + def block_value + self.whatnot[:block_value][:base] + end +end diff --git a/app/views/activities/_results.html.erb b/app/views/activities/_results.html.erb index 0fef8fb..5fde590 100644 --- a/app/views/activities/_results.html.erb +++ b/app/views/activities/_results.html.erb @@ -11,6 +11,8 @@

You constructed <%= result[:hearth_amenity].name %>.

<% when "activity" %>

You realized how to <%= result[:activity].name %>!

+ <% when "message" %> +

<%= result[:body] %>

<% when "error" %>

<%= result[:message] %>

<% end %> diff --git a/app/views/activities/show.html.erb b/app/views/activities/show.html.erb index 84c19a4..c9c6d25 100644 --- a/app/views/activities/show.html.erb +++ b/app/views/activities/show.html.erb @@ -2,7 +2,7 @@

<%= @activity.description %>

+ style="height: 30rem;" id="result_output">
diff --git a/data/activities.yml b/data/activities.yml index 26f4535..4975cf9 100644 --- a/data/activities.yml +++ b/data/activities.yml @@ -151,3 +151,24 @@ quarry_floret_mines: xp: - gid: "planequarry" value: 50 +hunt_killing_fields: + name: "Hunt The Killing Fields" + description: "Hunt monsters in The Killing Fields." + location: "floret_region" + innate: true + whatnot: + duration: + base: 60 + minimum: 35 + scaling: + - type: "skill" + gid: "beastslay" + scale_value: 2 + results: + - type: "monster" + chance: 1 + table: + - gid: "pit_leech" + score: 0 + - gid: "stalk_beast" + score: 0.70 diff --git a/data/monsters.yml b/data/monsters.yml new file mode 100644 index 0000000..631c83c --- /dev/null +++ b/data/monsters.yml @@ -0,0 +1,48 @@ +pit_leech: + name: "pit leech" + description: >- + A brown-black glistening thing the size of your arm and four times as wide. Featureless, except + for a ring of razor-sharp teeth at one end, encircling an inquisitive maw. + whatnot: + max_hp: + base: 5 + speed: + base: 1 + accuracy: + base: 1 + power: + base: 1 + evasion: + base: 1 + block: + base: 1 + block_value: + base: 1 + awards: + - type: "xp" + skill: "beastslay" + base: 5 +stalk_beast: + name: "stalk beast" + description: >- + A walking tangle of long, sinewy eye stalks, each punctuated with a fist-sized eyeball. They were and + are the heralds of things to come. + whatnot: + max_hp: + base: 9 + speed: + base: 3 + accuracy: + base: 2 + power: + base: 1 + evasion: + base: 2 + block: + base: 1 + block_value: + base: 1 + awards: + - type: "xp" + skill: "beastslay" + base: 9 diff --git a/db/migrate/20210522194937_add_wounds_to_characters.rb b/db/migrate/20210522194937_add_wounds_to_characters.rb new file mode 100644 index 0000000..be56e29 --- /dev/null +++ b/db/migrate/20210522194937_add_wounds_to_characters.rb @@ -0,0 +1,5 @@ +class AddWoundsToCharacters < ActiveRecord::Migration[6.1] + def change + add_column :characters, :wounds, :integer + end +end diff --git a/db/migrate/20210522201259_create_monsters.rb b/db/migrate/20210522201259_create_monsters.rb new file mode 100644 index 0000000..39f0a1a --- /dev/null +++ b/db/migrate/20210522201259_create_monsters.rb @@ -0,0 +1,12 @@ +class CreateMonsters < ActiveRecord::Migration[6.1] + def change + create_table :monsters do |t| + t.string :gid + t.string :name + t.text :description + t.jsonb :whatnot + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 5649a54..6dfbe87 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_05_22_184444) do +ActiveRecord::Schema.define(version: 2021_05_22_201259) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -67,6 +67,7 @@ ActiveRecord::Schema.define(version: 2021_05_22_184444) do t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "active_title_id" + t.integer "wounds" t.index ["active_title_id"], name: "index_characters_on_active_title_id" t.index ["activity_id"], name: "index_characters_on_activity_id" t.index ["user_id"], name: "index_characters_on_user_id" @@ -149,6 +150,15 @@ ActiveRecord::Schema.define(version: 2021_05_22_184444) do t.index ["gid"], name: "index_locations_on_gid" end + create_table "monsters", force: :cascade do |t| + t.string "gid" + t.string "name" + t.text "description" + t.jsonb "whatnot" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + create_table "skills", force: :cascade do |t| t.string "gid" t.string "name" diff --git a/db/seeds.rb b/db/seeds.rb index ca72f1e..5a6c696 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -49,3 +49,8 @@ load_data_file("data/hearth_amenities.yml").map do |gid, hash| hearth_amenity = HearthAmenity.find_or_create_by(gid: gid) hearth_amenity.update(hash) end + +load_data_file("data/monsters.yml").map do |gid, hash| + monster = Monster.find_or_create_by(gid: gid) + monster.update(hash) +end diff --git a/test/fixtures/monsters.yml b/test/fixtures/monsters.yml new file mode 100644 index 0000000..ec30753 --- /dev/null +++ b/test/fixtures/monsters.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: MyString + description: MyText + whatnot: + +two: + name: MyString + description: MyText + whatnot: diff --git a/test/models/monster_test.rb b/test/models/monster_test.rb new file mode 100644 index 0000000..f57dbbd --- /dev/null +++ b/test/models/monster_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class MonsterTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end -- cgit v1.2.3