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:) %>
+-
+
+ <%= item.name %>
+
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