From 34e3bbd03bbcaa2c4b4dcb91aa0fa21b5a275cc9 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sun, 4 Jan 2026 14:47:16 -0800 Subject: [PATCH] add max_exorcism_level to weapons (#205) * add max_exorcism_level to weapons - migration to add column (nullable integer) - expose in blueprint - permit in controller - add spec for create/update * default exorcism_level=1 for befoulment weapons - set default on create for GridWeapon and CollectionWeapon - data migration to populate existing befoulment weapons - add specs for default behavior --- app/blueprints/api/v1/weapon_blueprint.rb | 4 +- app/controllers/api/v1/weapons_controller.rb | 2 +- app/models/collection_weapon.rb | 13 ++ app/models/grid_weapon.rb | 13 ++ ...60104000002_populate_max_exorcism_level.rb | 20 +++ ...00001_add_max_exorcism_level_to_weapons.rb | 5 + spec/factories/weapons.rb | 10 ++ .../collection_weapons_controller_spec.rb | 59 +++++++++ spec/requests/grid_weapons_controller_spec.rb | 65 ++++++++++ spec/requests/weapons_controller_spec.rb | 115 ++++++++++++++++++ 10 files changed, 303 insertions(+), 3 deletions(-) create mode 100644 db/data/20260104000002_populate_max_exorcism_level.rb create mode 100644 db/migrate/20260104000001_add_max_exorcism_level_to_weapons.rb create mode 100644 spec/requests/weapons_controller_spec.rb diff --git a/app/blueprints/api/v1/weapon_blueprint.rb b/app/blueprints/api/v1/weapon_blueprint.rb index 681ff13..24f4747 100644 --- a/app/blueprints/api/v1/weapon_blueprint.rb +++ b/app/blueprints/api/v1/weapon_blueprint.rb @@ -12,8 +12,8 @@ module Api # Primary information fields :granblue_id, :element, :proficiency, - :max_level, :max_skill_level, :max_awakening_level, :limit, :rarity, - :ax, :ax_type, :gacha, :promotions, :forge_order, :extra + :max_level, :max_skill_level, :max_awakening_level, :max_exorcism_level, + :limit, :rarity, :ax, :ax_type, :gacha, :promotions, :forge_order, :extra # Series - returns full object with flags if weapon_series is present, fallback to legacy integer field :series do |w| diff --git a/app/controllers/api/v1/weapons_controller.rb b/app/controllers/api/v1/weapons_controller.rb index cc4b168..2c8e9dc 100644 --- a/app/controllers/api/v1/weapons_controller.rb +++ b/app/controllers/api/v1/weapons_controller.rb @@ -219,7 +219,7 @@ module Api :flb, :ulb, :transcendence, :extra, :extra_prerequisite, :limit, :ax, :gacha, :min_hp, :max_hp, :max_hp_flb, :max_hp_ulb, :min_atk, :max_atk, :max_atk_flb, :max_atk_ulb, - :max_level, :max_skill_level, :max_awakening_level, + :max_level, :max_skill_level, :max_awakening_level, :max_exorcism_level, :release_date, :flb_date, :ulb_date, :transcendence_date, :wiki_en, :wiki_ja, :wiki_raw, :gamewith, :kamigame, :recruits, :forged_from, :forge_chain_id, :forge_order, diff --git a/app/models/collection_weapon.rb b/app/models/collection_weapon.rb index 7399ed0..a9288cb 100644 --- a/app/models/collection_weapon.rb +++ b/app/models/collection_weapon.rb @@ -15,6 +15,7 @@ class CollectionWeapon < ApplicationRecord has_many :grid_weapons, dependent: :nullify before_destroy :orphan_grid_items + before_validation :set_default_exorcism_level, on: :create # Set defaults before validation so database defaults don't cause validation failures attribute :awakening_level, :integer, default: 1 @@ -174,4 +175,16 @@ class CollectionWeapon < ApplicationRecord def orphan_grid_items grid_weapons.update_all(orphaned: true, collection_weapon_id: nil) end + + ## + # Sets default exorcism_level to 1 for befoulment weapons if not provided. + # + # @return [void] + def set_default_exorcism_level + return unless weapon.present? + return unless exorcism_level.nil? + return unless weapon.weapon_series&.augment_type == 'befoulment' + + self.exorcism_level = 1 + end end \ No newline at end of file diff --git a/app/models/grid_weapon.rb b/app/models/grid_weapon.rb index b01b824..0dc9bac 100644 --- a/app/models/grid_weapon.rb +++ b/app/models/grid_weapon.rb @@ -56,6 +56,7 @@ class GridWeapon < ApplicationRecord validate :no_conflicts, on: :create before_save :assign_mainhand + before_validation :set_default_exorcism_level, on: :create ##### Amoeba configuration amoeba do @@ -245,4 +246,16 @@ class GridWeapon < ApplicationRecord def assign_mainhand self.mainhand = (position == -1) end + + ## + # Sets default exorcism_level to 1 for befoulment weapons if not provided. + # + # @return [void] + def set_default_exorcism_level + return unless weapon.present? + return unless exorcism_level.nil? + return unless weapon.weapon_series&.augment_type == 'befoulment' + + self.exorcism_level = 1 + end end diff --git a/db/data/20260104000002_populate_max_exorcism_level.rb b/db/data/20260104000002_populate_max_exorcism_level.rb new file mode 100644 index 0000000..dc3e0b4 --- /dev/null +++ b/db/data/20260104000002_populate_max_exorcism_level.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class PopulateMaxExorcismLevel < ActiveRecord::Migration[8.0] + def up + # Set max_exorcism_level = 5 for all weapons that belong to a series with befoulment augment type + updated = Weapon + .joins(:weapon_series) + .where(weapon_series: { augment_type: :befoulment }) + .update_all(max_exorcism_level: 5) + + puts " Updated #{updated} weapons with max_exorcism_level = 5" + end + + def down + Weapon + .joins(:weapon_series) + .where(weapon_series: { augment_type: :befoulment }) + .update_all(max_exorcism_level: nil) + end +end diff --git a/db/migrate/20260104000001_add_max_exorcism_level_to_weapons.rb b/db/migrate/20260104000001_add_max_exorcism_level_to_weapons.rb new file mode 100644 index 0000000..f01db44 --- /dev/null +++ b/db/migrate/20260104000001_add_max_exorcism_level_to_weapons.rb @@ -0,0 +1,5 @@ +class AddMaxExorcismLevelToWeapons < ActiveRecord::Migration[8.0] + def change + add_column :weapons, :max_exorcism_level, :integer, default: nil + end +end diff --git a/spec/factories/weapons.rb b/spec/factories/weapons.rb index 5285ac4..8b10220 100644 --- a/spec/factories/weapons.rb +++ b/spec/factories/weapons.rb @@ -85,5 +85,15 @@ FactoryBot.define do trait :ax_weapon do ax { true } end + + trait :odiant do + weapon_series { WeaponSeries.find_by(slug: 'odiant') || create(:weapon_series, :odiant) } + max_exorcism_level { 5 } + end + + trait :with_befoulment do + weapon_series { WeaponSeries.find_by(slug: 'odiant') || create(:weapon_series, :odiant) } + max_exorcism_level { 5 } + end end end \ No newline at end of file diff --git a/spec/requests/collection_weapons_controller_spec.rb b/spec/requests/collection_weapons_controller_spec.rb index 310cb80..ef93f93 100644 --- a/spec/requests/collection_weapons_controller_spec.rb +++ b/spec/requests/collection_weapons_controller_spec.rb @@ -279,4 +279,63 @@ RSpec.describe 'Collection Weapons API', type: :request do expect(response).to have_http_status(:not_found) end end + + describe 'default exorcism_level for befoulment weapons' do + let(:odiant_series) { create(:weapon_series, :odiant) } + let(:befoulment_weapon) { create(:weapon, weapon_series: odiant_series, max_exorcism_level: 5) } + let(:regular_weapon) { create(:weapon) } + + it 'sets exorcism_level to 1 when creating with befoulment weapon and no exorcism_level provided' do + attributes = { + collection_weapon: { + weapon_id: befoulment_weapon.id, + uncap_level: 3, + transcendence_step: 0 + } + } + + post '/api/v1/collection/weapons', params: attributes.to_json, headers: headers + + expect(response).to have_http_status(:created) + json = JSON.parse(response.body) + expect(json['exorcismLevel']).to eq(1) + end + + it 'respects provided exorcism_level for befoulment weapon' do + befoulment_modifier = create(:weapon_stat_modifier, :befoulment) + + attributes = { + collection_weapon: { + weapon_id: befoulment_weapon.id, + uncap_level: 3, + transcendence_step: 0, + exorcism_level: 3, + befoulment_modifier_id: befoulment_modifier.id, + befoulment_strength: 5.0 + } + } + + post '/api/v1/collection/weapons', params: attributes.to_json, headers: headers + + expect(response).to have_http_status(:created) + json = JSON.parse(response.body) + expect(json['exorcismLevel']).to eq(3) + end + + it 'does not set exorcism_level for non-befoulment weapons' do + attributes = { + collection_weapon: { + weapon_id: regular_weapon.id, + uncap_level: 3, + transcendence_step: 0 + } + } + + post '/api/v1/collection/weapons', params: attributes.to_json, headers: headers + + expect(response).to have_http_status(:created) + json = JSON.parse(response.body) + expect(json['exorcismLevel']).to be_nil + end + end end \ No newline at end of file diff --git a/spec/requests/grid_weapons_controller_spec.rb b/spec/requests/grid_weapons_controller_spec.rb index f1963f8..c5a4d95 100644 --- a/spec/requests/grid_weapons_controller_spec.rb +++ b/spec/requests/grid_weapons_controller_spec.rb @@ -345,6 +345,71 @@ RSpec.describe 'GridWeapons API', type: :request do end end + describe 'default exorcism_level for befoulment weapons' do + let(:odiant_series) { create(:weapon_series, :odiant) } + let(:befoulment_weapon) { create(:weapon, weapon_series: odiant_series, max_exorcism_level: 5) } + let(:regular_weapon) { create(:weapon) } + + it 'sets exorcism_level to 1 when creating with befoulment weapon and no exorcism_level provided' do + params = { + weapon: { + party_id: party.id, + weapon_id: befoulment_weapon.id, + position: 1, + uncap_level: 3, + transcendence_step: 0 + } + } + + post '/api/v1/grid_weapons', params: params.to_json, headers: headers + + expect(response).to have_http_status(:created) + json = JSON.parse(response.body) + expect(json['grid_weapon']['exorcismLevel']).to eq(1) + end + + it 'respects provided exorcism_level for befoulment weapon' do + befoulment_modifier = create(:weapon_stat_modifier, :befoulment) + + params = { + weapon: { + party_id: party.id, + weapon_id: befoulment_weapon.id, + position: 1, + uncap_level: 3, + transcendence_step: 0, + exorcism_level: 4, + befoulment_modifier_id: befoulment_modifier.id, + befoulment_strength: 5.0 + } + } + + post '/api/v1/grid_weapons', params: params.to_json, headers: headers + + expect(response).to have_http_status(:created) + json = JSON.parse(response.body) + expect(json['grid_weapon']['exorcismLevel']).to eq(4) + end + + it 'does not set exorcism_level for non-befoulment weapons' do + params = { + weapon: { + party_id: party.id, + weapon_id: regular_weapon.id, + position: 1, + uncap_level: 3, + transcendence_step: 0 + } + } + + post '/api/v1/grid_weapons', params: params.to_json, headers: headers + + expect(response).to have_http_status(:created) + json = JSON.parse(response.body) + expect(json['grid_weapon']['exorcismLevel']).to be_nil + end + end + # Debug hook: if any example fails and a response exists, print the error message. after(:each) do |example| if example.exception && defined?(response) && response.present? diff --git a/spec/requests/weapons_controller_spec.rb b/spec/requests/weapons_controller_spec.rb new file mode 100644 index 0000000..6614ba8 --- /dev/null +++ b/spec/requests/weapons_controller_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Weapons API', type: :request do + let(:editor_user) { create(:user, role: 7) } + let(:regular_user) { create(:user, role: 3) } + + let(:editor_token) do + Doorkeeper::AccessToken.create!(resource_owner_id: editor_user.id, expires_in: 30.days, scopes: 'public') + end + let(:regular_token) do + Doorkeeper::AccessToken.create!(resource_owner_id: regular_user.id, expires_in: 30.days, scopes: 'public') + end + let(:editor_headers) do + { 'Authorization' => "Bearer #{editor_token.token}", 'Content-Type' => 'application/json' } + end + let(:regular_headers) do + { 'Authorization' => "Bearer #{regular_token.token}", 'Content-Type' => 'application/json' } + end + + let!(:weapon_series) { create(:weapon_series, :odiant) } + let!(:weapon) { create(:weapon, weapon_series: weapon_series, max_exorcism_level: 5) } + + describe 'GET /api/v1/weapons/:id' do + it 'returns the weapon with max_exorcism_level' do + get "/api/v1/weapons/#{weapon.id}" + + expect(response).to have_http_status(:ok) + json = JSON.parse(response.body) + expect(json['maxExorcismLevel']).to eq(5) + end + + it 'returns null for max_exorcism_level when not set' do + weapon_without_exorcism = create(:weapon, max_exorcism_level: nil) + + get "/api/v1/weapons/#{weapon_without_exorcism.id}" + + expect(response).to have_http_status(:ok) + json = JSON.parse(response.body) + expect(json['maxExorcismLevel']).to be_nil + end + end + + describe 'POST /api/v1/weapons' do + let(:valid_params) do + { + weapon: { + granblue_id: '1040000001', + name_en: 'Test Weapon', + rarity: 4, + element: 1, + proficiency: 1, + max_exorcism_level: 5 + } + } + end + + it 'creates a weapon with max_exorcism_level when editor' do + post '/api/v1/weapons', params: valid_params.to_json, headers: editor_headers + + expect(response).to have_http_status(:created) + json = JSON.parse(response.body) + expect(json['maxExorcismLevel']).to eq(5) + end + + it 'creates a weapon with null max_exorcism_level' do + params = valid_params.deep_dup + params[:weapon][:max_exorcism_level] = nil + params[:weapon][:granblue_id] = '1040000002' + + post '/api/v1/weapons', params: params.to_json, headers: editor_headers + + expect(response).to have_http_status(:created) + json = JSON.parse(response.body) + expect(json['maxExorcismLevel']).to be_nil + end + + it 'rejects creation from non-editor' do + post '/api/v1/weapons', params: valid_params.to_json, headers: regular_headers + + expect(response).to have_http_status(:unauthorized) + end + end + + describe 'PATCH /api/v1/weapons/:id' do + it 'updates max_exorcism_level' do + patch "/api/v1/weapons/#{weapon.id}", + params: { weapon: { max_exorcism_level: 3 } }.to_json, + headers: editor_headers + + expect(response).to have_http_status(:ok) + json = JSON.parse(response.body) + expect(json['maxExorcismLevel']).to eq(3) + end + + it 'clears max_exorcism_level when set to null' do + patch "/api/v1/weapons/#{weapon.id}", + params: { weapon: { max_exorcism_level: nil } }.to_json, + headers: editor_headers + + expect(response).to have_http_status(:ok) + json = JSON.parse(response.body) + expect(json['maxExorcismLevel']).to be_nil + end + + it 'rejects update from non-editor' do + patch "/api/v1/weapons/#{weapon.id}", + params: { weapon: { max_exorcism_level: 3 } }.to_json, + headers: regular_headers + + expect(response).to have_http_status(:unauthorized) + end + end +end