diff --git a/app/models/item.rb b/app/models/item.rb new file mode 100644 index 00000000..3f41fce6 --- /dev/null +++ b/app/models/item.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Item < ApplicationRecord + MINIMUM_NAME_LENGTH = 3 + MAXIMUM_NAME_LENGTH = 24 + + belongs_to :owner, polymorphic: true, optional: true + + validates :name, presence: true, + length: { in: MINIMUM_NAME_LENGTH..MAXIMUM_NAME_LENGTH } +end diff --git a/app/models/room.rb b/app/models/room.rb index fa676e47..5ad16621 100644 --- a/app/models/room.rb +++ b/app/models/room.rb @@ -4,8 +4,9 @@ class Room < ApplicationRecord DEFAULT_COORDINATES = { x: 0, y: 0, z: 0 }.freeze has_many :characters, dependent: :restrict_with_exception - has_many :spawns, dependent: :destroy + has_many :items, foreign_key: :owner_id, inverse_of: :owner, dependent: :destroy has_many :monsters, dependent: :destroy + has_many :spawns, dependent: :destroy validates :description, presence: true validates :x, numericality: { only_integer: true }, diff --git a/app/views/game/_surroundings.html.erb b/app/views/game/_surroundings.html.erb index 67e4f805..bbd1d384 100644 --- a/app/views/game/_surroundings.html.erb +++ b/app/views/game/_surroundings.html.erb @@ -6,6 +6,12 @@ <% end %> +
    + <% room.items.sort_by(&:name).each do |item| %> + <%= render "game/surroundings/item", item: item %> + <% end %> +
+
    <% room.characters.active.sort_by(&:name).each do |character| %> <%= render "game/surroundings/character", character: character %> diff --git a/app/views/game/surroundings/_item.html.erb b/app/views/game/surroundings/_item.html.erb new file mode 100644 index 00000000..6b6628f3 --- /dev/null +++ b/app/views/game/surroundings/_item.html.erb @@ -0,0 +1,5 @@ +<%# locals: (item:) %> +
  1. + + <%= item.name %> +
  2. diff --git a/db/migrate/20240825015139_create_items.rb b/db/migrate/20240825015139_create_items.rb new file mode 100644 index 00000000..8db31a13 --- /dev/null +++ b/db/migrate/20240825015139_create_items.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class CreateItems < ActiveRecord::Migration[7.2] + def change + create_table :items do |t| + t.references :owner, null: false, polymorphic: true + + t.string :name, null: false, limit: 24 + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index db2e57c7..ff4dec3e 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[7.2].define(version: 2023_04_24_003938) do +ActiveRecord::Schema[7.2].define(version: 2024_08_25_015139) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -42,6 +42,15 @@ t.check_constraint "current_health <= maximum_health", name: "characters_current_health_check" end + create_table "items", force: :cascade do |t| + t.string "owner_type", null: false + t.bigint "owner_id", null: false + t.string "name", limit: 24, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["owner_type", "owner_id"], name: "index_items_on_owner" + end + create_table "monsters", force: :cascade do |t| t.string "name", limit: 24, null: false t.datetime "created_at", null: false diff --git a/db/seeds.rb b/db/seeds.rb index 9b15026f..4e7885e9 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -24,6 +24,10 @@ DESCRIPTION } ) + + Item.find_or_create_by(owner: room).tap do |item| + item.update(name: "Empty Jug") + end end Room.find_or_initialize_by(x: -1, y: 0, z: -1).tap do |room| diff --git a/spec/factories/item.rb b/spec/factories/item.rb new file mode 100644 index 00000000..8d03dcd0 --- /dev/null +++ b/spec/factories/item.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :item do + name + + trait :room do + owner { association(:room) } + end + end +end diff --git a/spec/features/characters/select_spec.rb b/spec/features/characters/select_spec.rb index a832f00c..67b881f5 100644 --- a/spec/features/characters/select_spec.rb +++ b/spec/features/characters/select_spec.rb @@ -38,6 +38,14 @@ expect(page).to have_surrounding_monster(monster) end + it "displays items in surroundings", js: false do + item = create(:item, owner: character.room) + + click_on character.name + + expect(page).to have_surrounding_item(item) + end + it "broadcasts an enter message to the room" do using_session(:nearby_character) do sign_in_as_character create(:character, room: character.room) diff --git a/spec/features/commands/move_spec.rb b/spec/features/commands/move_spec.rb index 8801559e..a639f024 100644 --- a/spec/features/commands/move_spec.rb +++ b/spec/features/commands/move_spec.rb @@ -49,6 +49,28 @@ expect(page).not_to have_surrounding_monster(north_monster) end + it "adds new surrounding items to the sender" do + north_room = create(:room, x: 0, y: 1, z: 0) + north_item = create(:item, owner: north_room) + + send_command(:move, :north) + + expect(page).to have_surrounding_item(north_item) + end + + it "removes old surrounding items from the sender" do + north_room = create(:room, x: 0, y: 1, z: 0) + north_item = create(:item, owner: north_room) + + send_command(:move, :north) + + wait_for(have_surrounding_item(north_item)) do + send_command(:move, :south) + end + + expect(page).not_to have_surrounding_item(north_item) + end + it "broadcasts the exit message to the source room" do create(:room, x: 0, y: 1, z: 0) diff --git a/spec/models/item_spec.rb b/spec/models/item_spec.rb new file mode 100644 index 00000000..907cbb10 --- /dev/null +++ b/spec/models/item_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Item do + describe "associations" do + subject(:item) { create(:item, :room) } + + it { is_expected.to belong_to(:owner).optional(true) } + end + + describe "validations" do + subject(:item) { create(:item, :room) } + + it { is_expected.to validate_presence_of(:name) } + + it do + expect(item).to validate_length_of(:name) + .is_at_least(described_class::MINIMUM_NAME_LENGTH) + .is_at_most(described_class::MAXIMUM_NAME_LENGTH) + end + end +end diff --git a/spec/models/room_spec.rb b/spec/models/room_spec.rb index 4a429ff0..f672fb41 100644 --- a/spec/models/room_spec.rb +++ b/spec/models/room_spec.rb @@ -6,9 +6,19 @@ describe "associations" do subject(:room) { build(:room) } + it { is_expected.to have_many(:monsters).dependent(:destroy) } + it { is_expected.to have_many(:spawns).dependent(:destroy) } + it "has many :characters dependent: :restrict_with_exception" do expect(room).to have_many(:characters).dependent(:restrict_with_exception) end + + it "has many :items inverse_of: owner, dependent: destroy" do + expect(room).to have_many(:items) + .with_foreign_key(:owner_id) + .inverse_of(:owner) + .dependent(:destroy) + end end describe "constants" do @@ -20,9 +30,6 @@ describe "validations" do subject(:room) { build(:room) } - it { is_expected.to have_many(:monsters).dependent(:destroy) } - it { is_expected.to have_many(:spawns).dependent(:destroy) } - it { is_expected.to validate_presence_of(:description) } it { is_expected.to validate_numericality_of(:x).only_integer } diff --git a/spec/support/helpers/matchers.rb b/spec/support/helpers/matchers.rb index 311130b8..5d54a2ab 100644 --- a/spec/support/helpers/matchers.rb +++ b/spec/support/helpers/matchers.rb @@ -12,6 +12,10 @@ def have_surrounding_character(character) have_css("#surrounding-characters li", text: character.name) end + def have_surrounding_item(item) + have_css("#surrounding-items li", text: item.name) + end + def have_surrounding_monster(monster) have_css("#surrounding-monsters li", text: monster.name) end diff --git a/spec/views/game/_surroundings.html.erb_spec.rb b/spec/views/game/_surroundings.html.erb_spec.rb index fd82c340..ed636a6b 100644 --- a/spec/views/game/_surroundings.html.erb_spec.rb +++ b/spec/views/game/_surroundings.html.erb_spec.rb @@ -48,6 +48,29 @@ end end + context "with items" do + let(:room) { create(:room) } + + it "renders the surrounding items ordered by name" do + item_first = create(:item, owner: room, name: "Dagger") + item_second = create(:item, owner: room, name: "Knife") + + expect(html).to have_css( + "#surrounding-items #surrounding_item_#{item_first.id}" + ).and( + have_css( + "#surrounding-items #surrounding_item_#{item_second.id}" + ) + ).and( + have_css( + "#surrounding-items " \ + "#surrounding_item_#{item_first.id} + " \ + "#surrounding_item_#{item_second.id}" + ) + ) + end + end + context "with monsters" do let(:monster_first) { create(:monster, room: room, name: "Blob") } let(:monster_second) { create(:monster, room: room, name: "Rat") } diff --git a/spec/views/game/surroundings/_item.html.erb_spec.rb b/spec/views/game/surroundings/_item.html.erb_spec.rb new file mode 100644 index 00000000..823aa910 --- /dev/null +++ b/spec/views/game/surroundings/_item.html.erb_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "game/surroundings/_item.html.erb" do + subject(:html) do + render template: "game/surroundings/_item", + locals: { item: item } + + rendered + end + + let(:item) { build_stubbed(:item) } + + it "renders the surrounding item" do + expect(html).to have_css("#surrounding_item_#{item.id}", text: item.name) + end +end