From 5c578ee527ea0d769a45ebf963bf2affe5015b26 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 22 Dec 2025 19:52:59 -0800 Subject: [PATCH] auto-compute forge chain fields from forged_from - add before_save callback to calculate forge_order and forge_chain_id - add validation to prevent circular forge chains --- app/models/weapon.rb | 47 ++++++++++++++++++++++++++++++++++++++++++++ db/schema.rb | 8 +++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/models/weapon.rb b/app/models/weapon.rb index c74b26f..5d04fe8 100644 --- a/app/models/weapon.rb +++ b/app/models/weapon.rb @@ -139,6 +139,9 @@ class Weapon < ApplicationRecord # Forge chain scopes scope :in_forge_chain, ->(chain_id) { where(forge_chain_id: chain_id).order(:forge_order) } + # Forge chain callbacks + before_save :compute_forge_chain_fields, if: :forged_from_changed? + # Forge chain methods def forged_from_weapon return nil unless forged_from.present? @@ -188,4 +191,48 @@ class Weapon < ApplicationRecord found = WeaponSeries.find_by(id: value) || WeaponSeries.find_by(slug: value) self.weapon_series = found end + + # Validation to prevent circular forge chains + validate :no_circular_forge_chain + + def no_circular_forge_chain + return unless forged_from.present? + + visited = Set.new([granblue_id]) + current = forged_from + + while current.present? + if visited.include?(current) + errors.add(:forged_from, 'creates a circular forge chain') + return + end + visited << current + current = Weapon.find_by(granblue_id: current)&.forged_from + end + end + + private + + # Auto-compute forge_order and forge_chain_id based on forged_from + def compute_forge_chain_fields + if forged_from.present? + base_weapon = Weapon.find_by(granblue_id: forged_from) + if base_weapon + # Inherit or create forge_chain_id from base weapon + self.forge_chain_id = base_weapon.forge_chain_id || base_weapon.id + + # Compute forge_order as base weapon's order + 1 + self.forge_order = base_weapon.forge_order.to_i + 1 + + # Ensure base weapon has forge_chain_id if it didn't + if base_weapon.forge_chain_id.nil? + base_weapon.update_column(:forge_chain_id, base_weapon.id) + base_weapon.update_column(:forge_order, 0) if base_weapon.forge_order.nil? + end + end + else + # Clearing forged_from - reset forge_order if part of a chain + self.forge_order = 0 if forge_chain_id.present? + end + end end diff --git a/db/schema.rb b/db/schema.rb index c63b95c..b2c3b2d 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[8.0].define(version: 2025_12_20_100000) do +ActiveRecord::Schema[8.0].define(version: 2025_12_21_210000) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" enable_extension "pg_catalog.plpgsql" @@ -989,6 +989,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_20_100000) do t.integer "promotions", default: [], null: false, array: true t.uuid "weapon_series_id" t.boolean "gacha", default: false, null: false + t.integer "extra_prerequisite" + t.string "forged_from" + t.uuid "forge_chain_id" + t.integer "forge_order" + t.index ["forge_chain_id"], name: "index_weapons_on_forge_chain_id" + t.index ["forged_from"], name: "index_weapons_on_forged_from" t.index ["gacha"], name: "index_weapons_on_gacha" t.index ["granblue_id"], name: "index_weapons_on_granblue_id" t.index ["name_en"], name: "index_weapons_on_name_en", opclass: :gin_trgm_ops, using: :gin