* Update gitignore There is a mystery postgres folder and we are going to ignore it * Add migrations * Update preview state default to pending * Adds indexes * Adds PgHero and PgStatements * Update .gitignore * Update Gemfile Production: - `newrelic_rpm` Development: - `pg_query` - `prosopite` * Configure Sidekiq Create job for cleaning up party previews * Configure Prosopite and remove CacheFreeLogger * Enable query logging * Update api_controller.rb Add N+1 detectioin via Prosopite in development/test environments * Refactor canonical object blueprints * Refactor grid object blueprints * Remove N+1 from grid object models Reimplementing `character` `summon` and `weapon` was making N+1s which made queries really slow * Add counter caches to party * Add preview generation helpers The Party model can respond to queries about its preview state with the following models: - `schedule_preview_generation` - `preview_content_changed?` - `preview_expired?` - `should_generate_preview?` - `ready_for_preview?` - `needs_preview_generation?` - `preview_relevant_attributes` Removes the following methods: - `schedule_preview_regeneration` - `preview_relevant_changes?` * Add cache to is_favorited(user) * Refactored PartyBlueprint to minimize N+1s * Remove preview expiry constants These are defined in the Coordinator instead * Add method comments * Create parties_controller.rbs * Update logic and logs * Updates excluded methods and calculate_count * Use `includes` instead of `joins` * Use a less-insane way of counting * Adds a helper method for party privacy * Update filter condition helpers Just minor refactoring * Fix old view name in PartyBlueprint * Refactor parties#create * Remove redundant return * Update parties_controller.rbs * Update parties#index * Update parties_controller.rb Updates apply_includes and apply_excludes, along with modifying id_to_table and build_query * Update parties_controller.rb Adds the rest of the changes, too tired to write them all out. Some preview generation, some filtering * Refactor parties#index and parties#favorites These are mostly the same methods, so we remove common code into build_parties_query and render_paginated_parties * Alias table name to object to maintain API consistency * Maintain API consistency with raid blueprint * Optimize party loading by adding eager loading to `set_from_slug` - Refactored `set_from_slug` to use `includes` for eager loading associated models: - `user`, `job`, `raid` (with `group`) - `characters` (with `character` and `awakening`) - `weapons` (with `weapon`, `awakenings`, `weapon_key1`, `weapon_key2`, `weapon_key3`) - `summons` (with `summon`) - `guidebooks` (`guidebook1`, `guidebook2`, `guidebook3`) - `source_party`, `remixes`, `skills`, and `accessory` - This change improves query efficiency by reducing N+1 queries and ensures all relevant associations are preloaded. - Removed redundant favorite check as it was not necessary in this context. * Refactor grid blueprints - **GridCharacterBlueprint:** - Removed `:minimal` view restriction on `party` association. - Improved nil checks for `ring1`, `ring2`, and `earring` to prevent errors. - Converted string values in `awakening_level`, `over_mastery`, and `aetherial_mastery` fields to integers for consistency. - Ensured `over_mastery` and `aetherial_mastery` only include valid entries, filtering out blank or zero-modifier values. - **GridWeaponBlueprint:** - Removed `:minimal` view restriction on `party` association. - Ensured `weapon` association exists before accessing `ax`, `series`, or `awakening`. - Improved conditional checks for `weapon_keys` to prevent errors when `weapon` or `series` is nil. - Converted `awakening_level` field to integer for consistency. - **GridCharacterBlueprint:** - Removed `:minimal` view restriction on `party` association. * Update raid blueprints - Show flat representation of raid group in RaidBlueprint's nested view - Show nested representation of raid in RaidGroupBlueprint's full view * Move n+1 detection to around_action hook * Improve handling mastery bonuses - Improved handling of nested attributes: - Replaced old mastery structure with new `rings` and `awakening` assignments. - Added `new_rings` and `new_awakening` virtual attributes for easier updates. - Updated `assign_attributes` to exclude `rings` and `awakening` to prevent conflicts. - Enhanced parameter transformation: - Introduced `transform_character_params` to process `rings`, `awakening`, and `earring` more reliably. - Ensured proper type conversion (`to_i`) for numeric values in `uncap_level`, `transcendence_step`, and `awakening_level`. - Improved error handling for missing values by setting defaults where needed. - Optimized database queries: - Added `.includes(:awakening)` to `set` to prevent N+1 query issues. - Updated strong parameters: - Changed `rings` from individual keys (`ring1`, `ring2`, etc.) to a structured array format. - Refactored permitted attributes to align with the new nested structure. * Eager-load jobs when querying job skills * Eager load raids/groups when querying * Update users_controller.rb More efficient way of denoting favorited parties. * Update awakening.rb - Removes explicitly defined associations and adds ActiveRecord associations instead * Update party.rb - Removes favorited accessor - Renames derivative_parties to remixes and adds in-built sort * Update weapon_awakening.rb - Removes redefined explicit associations * Update grid_character.rb - Adds code transforming incoming ring and awakening values into something the db understands * Update character.rb Add explicit Awakenings enum * Update coordinator.rb Adds 'queued' as a state for generation
245 lines
6.6 KiB
Ruby
245 lines
6.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class GridCharacter < ApplicationRecord
|
|
belongs_to :character, foreign_key: :character_id, primary_key: :id
|
|
|
|
belongs_to :awakening, optional: true
|
|
belongs_to :party,
|
|
counter_cache: :characters_count,
|
|
inverse_of: :characters
|
|
validates_presence_of :party
|
|
|
|
validate :validate_awakening_level, on: :update
|
|
validate :transcendence, on: :update
|
|
validate :validate_over_mastery_values, on: :update
|
|
validate :validate_aetherial_mastery_value, on: :update
|
|
validate :over_mastery_attack_matches_hp, on: :update
|
|
|
|
# Virtual attribute for the new rings structure
|
|
attr_accessor :new_rings
|
|
|
|
# Virtual attribute for the new awakening structure
|
|
attr_accessor :new_awakening
|
|
|
|
##### Amoeba configuration
|
|
amoeba do
|
|
set ring1: { modifier: nil, strength: nil }
|
|
set ring2: { modifier: nil, strength: nil }
|
|
set ring3: { modifier: nil, strength: nil }
|
|
set ring4: { modifier: nil, strength: nil }
|
|
set earring: { modifier: nil, strength: nil }
|
|
set perpetuity: false
|
|
end
|
|
|
|
before_validation :apply_new_rings, if: -> { new_rings.present? }
|
|
before_validation :apply_new_awakening, if: -> { new_awakening.present? }
|
|
|
|
# Add awakening before the model saves
|
|
before_save :add_awakening
|
|
|
|
def validate_awakening_level
|
|
errors.add(:awakening, 'awakening level too low') if awakening_level < 1
|
|
errors.add(:awakening, 'awakening level too high') if awakening_level > 9
|
|
end
|
|
|
|
def transcendence
|
|
errors.add(:transcendence_step, 'character has no transcendence') if transcendence_step.positive? && !character.ulb
|
|
errors.add(:transcendence_step, 'transcendence step too high') if transcendence_step > 5 && character.ulb
|
|
errors.add(:transcendence_step, 'transcendence step too low') if transcendence_step.negative? && character.ulb
|
|
end
|
|
|
|
def over_mastery_attack
|
|
errors.add(:ring1, 'invalid value') unless ring1['modifier'].nil? || atk_values.include?(ring1['strength'])
|
|
end
|
|
|
|
def over_mastery_hp
|
|
return if ring2['modifier'].nil?
|
|
|
|
errors.add(:ring2, 'invalid value') unless hp_values.include?(ring2['strength'])
|
|
end
|
|
|
|
def over_mastery_attack_matches_hp
|
|
return if ring1[:modifier].nil? && ring2[:modifier].nil?
|
|
|
|
return if ring2[:strength] == (ring1[:strength] / 2)
|
|
|
|
errors.add(:over_mastery,
|
|
'over mastery attack and hp values do not match')
|
|
end
|
|
|
|
def validate_over_mastery_values
|
|
[ring1, ring2, ring3, ring4].each_with_index do |ring, index|
|
|
next if ring['modifier'].nil?
|
|
|
|
modifier = over_mastery_modifiers[ring['modifier']]
|
|
check_value({ "ring#{index}": { ring[modifier] => ring['strength'] } },
|
|
'over_mastery')
|
|
end
|
|
end
|
|
|
|
def validate_aetherial_mastery_value
|
|
return if earring['modifier'].nil?
|
|
|
|
return unless earring['modifier'].positive?
|
|
|
|
modifier = aetherial_mastery_modifiers[earring['modifier']].to_sym
|
|
check_value({ "earring": { modifier => earring['strength'] } },
|
|
'aetherial_mastery')
|
|
end
|
|
|
|
def blueprint
|
|
GridCharacterBlueprint
|
|
end
|
|
|
|
private
|
|
|
|
def add_awakening
|
|
return unless awakening.nil?
|
|
|
|
self.awakening = Awakening.where(slug: 'character-balanced').sole
|
|
end
|
|
|
|
def apply_new_rings
|
|
# Expect new_rings to be an array of hashes, e.g.,
|
|
# [{"modifier" => "1", "strength" => "1500"}, {"modifier" => "2", "strength" => "750"}]
|
|
default_ring = { "modifier" => nil, "strength" => nil }
|
|
rings_array = Array(new_rings).map(&:to_h)
|
|
# Pad with defaults so there are exactly four rings
|
|
rings_array.fill(default_ring, rings_array.size...4)
|
|
self.ring1 = rings_array[0]
|
|
self.ring2 = rings_array[1]
|
|
self.ring3 = rings_array[2]
|
|
self.ring4 = rings_array[3]
|
|
end
|
|
|
|
def apply_new_awakening
|
|
self.awakening_id = new_awakening[:id]
|
|
self.awakening_level = new_awakening[:level].present? ? new_awakening[:level].to_i : 1
|
|
end
|
|
|
|
def check_value(property, type)
|
|
# Input format
|
|
# { ring1: { atk: 300 } }
|
|
|
|
key = property.keys.first
|
|
modifier = property[key].keys.first
|
|
|
|
return if modifier.nil?
|
|
|
|
case type
|
|
when 'over_mastery'
|
|
errors.add(key, 'invalid value') unless over_mastery_values.include?(key['strength'])
|
|
when 'aetherial_mastery'
|
|
errors.add(key, 'value too low') if aetherial_mastery_values[modifier][:min] > self[key]['strength']
|
|
errors.add(key, 'value too high') if aetherial_mastery_values[modifier][:max] < self[key]['strength']
|
|
end
|
|
end
|
|
|
|
def over_mastery_modifiers
|
|
{
|
|
1 => 'atk',
|
|
2 => 'hp',
|
|
3 => 'debuff_success',
|
|
4 => 'skill_cap',
|
|
5 => 'ca_dmg',
|
|
6 => 'ca_cap',
|
|
7 => 'stamina',
|
|
8 => 'enmity',
|
|
9 => 'crit',
|
|
10 => 'da',
|
|
11 => 'ta',
|
|
12 => 'def',
|
|
13 => 'heal',
|
|
14 => 'debuff_resist',
|
|
15 => 'dodge'
|
|
}
|
|
end
|
|
|
|
def over_mastery_values
|
|
{
|
|
atk: [300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000],
|
|
hp: [150, 300, 450, 600, 750, 900, 1050, 1200, 1350, 1500],
|
|
debuff_success: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
skill_cap: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
ca_dmg: [10, 12, 14, 16, 18, 20, 22, 24, 27, 30],
|
|
ca_cap: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
crit: [10, 12, 14, 16, 18, 20, 22, 24, 27, 30],
|
|
enmity: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
stamina: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
def: [6, 7, 8, 9, 10, 12, 14, 16, 18, 20],
|
|
heal: [3, 6, 9, 12, 15, 18, 21, 24, 27, 30],
|
|
debuff_resist: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
dodge: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
da: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
ta: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
}
|
|
end
|
|
|
|
def aetherial_mastery_modifiers
|
|
{
|
|
1 => 'da',
|
|
2 => 'ta',
|
|
3 => 'ele_atk',
|
|
4 => 'ele_resist',
|
|
5 => 'stamina',
|
|
6 => 'enmity',
|
|
7 => 'supplemental',
|
|
8 => 'crit',
|
|
9 => 'counter_dodge',
|
|
10 => 'counter_dmg'
|
|
}
|
|
end
|
|
|
|
def aetherial_mastery_values
|
|
{
|
|
da: {
|
|
min: 10,
|
|
max: 17
|
|
},
|
|
ta: {
|
|
min: 5,
|
|
max: 12
|
|
},
|
|
ele_atk: {
|
|
min: 15,
|
|
max: 22
|
|
},
|
|
ele_resist: {
|
|
min: 5,
|
|
max: 12
|
|
},
|
|
stamina: {
|
|
min: 5,
|
|
max: 12
|
|
},
|
|
enmity: {
|
|
min: 5,
|
|
max: 12
|
|
},
|
|
supplemental: {
|
|
min: 5,
|
|
max: 12
|
|
},
|
|
crit: {
|
|
min: 18,
|
|
max: 35
|
|
},
|
|
counter_dodge: {
|
|
min: 5,
|
|
max: 12
|
|
},
|
|
counter_dmg: {
|
|
min: 10,
|
|
max: 17
|
|
}
|
|
}
|
|
end
|
|
|
|
def atk_values
|
|
[300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000]
|
|
end
|
|
|
|
def hp_values
|
|
[150, 300, 450, 600, 750, 900, 1050, 1200, 1350, 1500]
|
|
end
|
|
end
|