diff options
Diffstat (limited to 'app')
32 files changed, 507 insertions, 128 deletions
diff --git a/app/controllers/characters/bestiary_controller.rb b/app/controllers/characters/bestiary_controller.rb new file mode 100644 index 0000000..7e37d9c --- /dev/null +++ b/app/controllers/characters/bestiary_controller.rb @@ -0,0 +1,12 @@ +class Characters::BestiaryController < ApplicationController + before_action :set_character + + def index + @monster_kills = @character.monster_kills.all + end + + private + def set_character + @character = Character.find(params[:character_id]) + end +end diff --git a/app/controllers/characters/hearth_controller.rb b/app/controllers/characters/hearth_controller.rb index 82f72d6..c525add 100644 --- a/app/controllers/characters/hearth_controller.rb +++ b/app/controllers/characters/hearth_controller.rb @@ -7,6 +7,7 @@ class Characters::HearthController < ApplicationController forge: [], laboratory: [], spicebench: [], + binding_array: [], } Activity.where("gid like ?", "craft_%").each do |activity| @@ -20,6 +21,8 @@ class Characters::HearthController < ApplicationController @amenity_activities[:laboratory].push(activity) && next when "spicebench" @amenity_activities[:spicebench].push(activity) && next + when "binding_array" + @amenity_activities[:binding_array].push(activity) && next else raise "Invalid amenity gid (#{requirement_data[:gid]}" end diff --git a/app/controllers/characters/item_infixes_controller.rb b/app/controllers/characters/item_infixes_controller.rb new file mode 100644 index 0000000..0b5f1c5 --- /dev/null +++ b/app/controllers/characters/item_infixes_controller.rb @@ -0,0 +1,27 @@ +class Characters::ItemInfixesController < ApplicationController + def create + # TODO: Can this find-by-id happen automagically? + @item_infix = current_char.infix(Item.find(params[:item_id]), Skill.find(params[:skill_id])) + if @item_infix + flash[:notice] = "Infixed #{@item_infix.item.name}." + else + flash[:alert] = "Failed to infix item." + end + redirect_to character_skills_path(current_char) + end + + def destroy + @item_infix = ItemInfix.find(params[:id]) + if current_char.remove_infix(@item_infix) + flash[:notice] = "Removed #{@item_infix.item.name}." + else + flash[:alert] = "Failed to remove #{@item_infix.item.name}." + end + redirect_to character_skills_path(current_char) + end + + private + def item_infix_params + params.require(:item_infix).permit(:item_id, :skill_id) + end +end diff --git a/app/controllers/characters/items_controller.rb b/app/controllers/characters/items_controller.rb index 470e21c..e38b69a 100644 --- a/app/controllers/characters/items_controller.rb +++ b/app/controllers/characters/items_controller.rb @@ -1,6 +1,7 @@ class Characters::ItemsController < ApplicationController + before_action :set_character, only: :index + def index - @character = Character.find(params[:character_id]) end def equip @@ -66,4 +67,13 @@ class Characters::ItemsController < ApplicationController redirect_to character_items_path(current_char) end end + + private + def set_character + @character = Character.find(params[:character_id]) + unless current_char == @character + flash[:alert] = "You can only look at your own items." + redirect_to character_path(@character) + end + end end diff --git a/app/controllers/characters/rankings_controller.rb b/app/controllers/characters/rankings_controller.rb index bbae9fc..429b487 100644 --- a/app/controllers/characters/rankings_controller.rb +++ b/app/controllers/characters/rankings_controller.rb @@ -6,4 +6,4 @@ class Characters::RankingsController < ApplicationController def index @character = Character.find(params[:character_id]) end -end
\ No newline at end of file +end diff --git a/app/controllers/characters/skills_controller.rb b/app/controllers/characters/skills_controller.rb new file mode 100644 index 0000000..6fcf417 --- /dev/null +++ b/app/controllers/characters/skills_controller.rb @@ -0,0 +1,15 @@ +class Characters::SkillsController < ApplicationController + before_action :set_character, only: :index + + def index + end + + private + def set_character + @character = Character.find(params[:character_id]) + unless current_char == @character + flash[:alert] = "You can only look at your own skills." + redirect_to character_path(@character) + end + end +end diff --git a/app/controllers/characters/spells_controller.rb b/app/controllers/characters/spells_controller.rb new file mode 100644 index 0000000..a0e6913 --- /dev/null +++ b/app/controllers/characters/spells_controller.rb @@ -0,0 +1,9 @@ +class Characters::SpellsController < ApplicationController + def index + @spell_activities = Activity.where("gid like ?", "havencast_%").where(innate: true).order(:name) + # TODO: Don't load into memory + @spell_activities = @spell_activities.to_a + current_char.learned_activities + .map { |la| la.activity } + .select { |a| a.gid.start_with?("havencast_") } + end +end diff --git a/app/controllers/characters_controller.rb b/app/controllers/characters_controller.rb index 77e1a94..2eb906b 100644 --- a/app/controllers/characters_controller.rb +++ b/app/controllers/characters_controller.rb @@ -1,8 +1,8 @@ class CharactersController < ApplicationController skip_before_action :redirect_if_no_active_character, only: [:new, :create] + before_action :set_character, only: [:show, :set_combat_styles] def show - @character = Character.find(params[:id]) end def new @@ -22,11 +22,6 @@ class CharactersController < ApplicationController end def set_combat_styles - @character = Character.find(params[:character_id]) - unless @character == current_char - flash[:alert] = "You can't set the combat styles of another character." - redirect_to character_path(@character) and return - end if @character.update(offensive_style: params[:offensive_style], defensive_style: params[:defensive_style]) flash[:notice] = "Changed combat styles to #{@character.offensive_style} and #{@character.defensive_style}." @@ -40,4 +35,12 @@ class CharactersController < ApplicationController def character_params params.require(:character).permit(:name) end + + def set_character + @character = Character.find(params[:id]) + unless current_char == @character + flash[:alert] = "You can only manage your own character." + redirect_to character_path(@character) + end + end end diff --git a/app/controllers/leaderboard_controller.rb b/app/controllers/leaderboard_controller.rb index e6e8543..14b8acb 100644 --- a/app/controllers/leaderboard_controller.rb +++ b/app/controllers/leaderboard_controller.rb @@ -2,7 +2,7 @@ class LeaderboardController < ApplicationController def index - @top_per_skill = Hash[Skill.all.map { |s| [s.name.to_sym, CharacterSkill.top_xp_for(s)] }] + @top_per_skill = Hash[Skill.all.order(:name).map { |s| [s.name.to_sym, CharacterSkill.top_xp_for(s)] }] @top_total_xp = Character.top_total_xp @top_total_level = Character.top_total_level end diff --git a/app/errors/item_infix_error.rb b/app/errors/item_infix_error.rb new file mode 100644 index 0000000..bc6b251 --- /dev/null +++ b/app/errors/item_infix_error.rb @@ -0,0 +1,2 @@ +class ItemInfixError < StandardError +end diff --git a/app/javascript/channels/chat_room_channel.js b/app/javascript/channels/chat_room_channel.js index af29953..514742f 100644 --- a/app/javascript/channels/chat_room_channel.js +++ b/app/javascript/channels/chat_room_channel.js @@ -10,14 +10,19 @@ consumer.subscriptions.create("ChatRoomChannel", { }, received(data) { + // If scrolled to the bottom or near the bottom, then smooth scroll to the bottom. + var shouldScroll = false; + var chatOutputElement = document.getElementById("chat_output"); + if ((chatOutputElement.scrollTop + 100) >= (chatOutputElement.scrollHeight - chatOutputElement.offsetHeight)) { + shouldScroll = true; + } + // Called when there's incoming data on the websocket for this channel var node = document.createElement("P"); node.innerHTML = data.html; - var chatOutputElement = document.getElementById("chat_output"); chatOutputElement.appendChild(node); - // If scrolled to the bottom or near the bottom, then smooth scroll to the bottom. - if ((chatOutputElement.scrollTop + 100) >= (chatOutputElement.scrollHeight - chatOutputElement.offsetHeight)) { + if (shouldScroll) { chatOutputElement.scrollTo({ top: chatOutputElement.scrollHeight, left: 0, behavior: 'smooth' }); diff --git a/app/javascript/controllers/results_controller.js b/app/javascript/controllers/results_controller.js new file mode 100644 index 0000000..266b80c --- /dev/null +++ b/app/javascript/controllers/results_controller.js @@ -0,0 +1,13 @@ +import { Controller } from "stimulus"; + +export default class extends Controller { + static targets = [ "output" ]; + + connect() { + this.scrollToBottom(); + } + + scrollToBottom() { + this.outputTarget.scrollTop = this.outputTarget.scrollHeight; + } +} diff --git a/app/lib/activity_processor.rb b/app/lib/activity_processor.rb index b5dd11c..cca2185 100644 --- a/app/lib/activity_processor.rb +++ b/app/lib/activity_processor.rb @@ -90,6 +90,13 @@ class ActivityProcessor item = Item.find_by_gid(result[:gid]) hp = @character.hearth.hearth_plantings.create(item: item) @results.push({ type: type, hearth_planting: hp }) + when "condition" + Character.transaction do + condition = Condition.find_by_gid(result[:gid]) + @character.states.create!(condition: condition, expires_at: Time.now + result[:duration]) + @results.push({ type: "message", body: result[:message] }) + @results.push({ type: type, condition: condition }) + end when "activity" next if rand > (result[:chance] || 1) table_roll = rand @@ -97,6 +104,7 @@ class ActivityProcessor score = table_entry[:score] if table_roll >= score new_activity = Activity.find_by_gid(table_entry[:gid]) + raise "Invalid activity gid (#{table_entry[:gid]})" unless new_activity unless @character.learned_activities.exists?(activity: new_activity) @character.learned_activities.create(activity: new_activity) @results.push({ type: type, activity: new_activity }) @@ -108,6 +116,26 @@ class ActivityProcessor end end + # Note: This will result in equipment being checked for breakage twice (after combat, and now) if it provides the + # `beastslay_speed` stat. At the time of this writing, that stat doesn't exist. + # But just something to keep in mind. + # Note: This will result in equipment being checked twice if it provides two speed stats. + # Fine for now since no equipment gives two skill speed stats, but may want to refine in the future. + if @activity.whatnot[:requirements]&.any? + required_skill_gids = @activity.whatnot[:requirements].select { |r| r[:type] == "skill" }.map { |r| r[:gid] }.uniq + required_skill_gids.each do |required_skill_gid| + skill = Skill.find_by_gid(required_skill_gid) + broken_item = @character.do_equipment_break_check(skill: skill) + if broken_item + @results.push({ type: "warning", message: "Your #{broken_item.name} was damaged beyond repair!" }) + end + broken_infix_item = @character.do_item_infix_break_check(skill: skill) + if broken_infix_item + @results.push({ type: "warning", message: "Your #{broken_infix_item.name} omen faded away." }) + end + end + end + if @character.activity && @character.queued_actions if @character.queued_actions > 0 @character.queued_actions -= 1 @@ -166,7 +194,8 @@ class ActivityProcessor end def handle_title_result(data) - if @character.award_title(data[:gid]) + title = Title.find_by_gid(data[:gid]) + if @character.award_title(title) @results.push({ type: "title", title: title }) end end @@ -230,8 +259,13 @@ class ActivityProcessor turn_order = [[char, mon], [mon, char]].shuffle end turn_order.cycle do |actor, target| + base_accuracy_roll = roll(20) accuracy_roll = base_accuracy_roll + actor.accuracy(with_combat_style: true) + if actor == mon + accuracy_roll += char.total_enemy_stat_change("accuracy") + end + evasion_roll = roll(20) + target.evasion(with_combat_style: true) if accuracy_roll >= evasion_roll || base_accuracy_roll == 20 @@ -273,8 +307,22 @@ class ActivityProcessor combat_message.call("#{target.name} evaded #{actor.name}'s attack.") end + # HACK: DoT is char-only, and one-damage-type-only for now. + if actor == char + actor.dots.each do |data| + damage_roll = rand(data[:min]..data[:max]) + effective_resistance = [target.resistance(data[:gid]), damage_roll].min + resolved_damage = damage_roll - (effective_resistance * rand(0.5..1)).round + mon_hp -= resolved_damage + combat_message.call("#{data[:message]} (#{resolved_damage} #{data[:gid]})") + end + end + if char_hp < 1 || mon_hp < 1 - break_check + broken_item = @character.do_equipment_break_check + if broken_item + @results.push({ type: "warning", message: "Your #{broken_item.name} was damaged beyond repair!" }) + end if monster_spawn hp_lost = monster_spawn.remaining_hp - mon_hp @@ -295,6 +343,11 @@ class ActivityProcessor end else @results.push({ type: "message", body: "You slew the #{mon.name}." }) + + monster_kills = character.monster_kills.find_or_initialize_by(monster: mon) + monster_kills.quantity ? monster_kills.quantity += 1 : monster_kills.quantity = 1 + monster_kills.save + if monster_spawn char.stop_activity return @@ -317,10 +370,4 @@ class ActivityProcessor end end end - - def break_check - @character.do_equipment_break_checks.each do |broken_item| - @results.push({ type: "warning", message: "Your #{broken_item.name} was damaged beyond repair!" }) - end - end end diff --git a/app/models/character.rb b/app/models/character.rb index d386002..2669626 100644 --- a/app/models/character.rb +++ b/app/models/character.rb @@ -9,10 +9,12 @@ class Character < ApplicationRecord has_many :character_items has_many :learned_activities has_many :items, through: :character_items + has_many :item_infixes has_many :character_skills has_many :conditions, through: :states has_many :states has_many :chat_messages + has_many :monster_kills has_many :bazaar_orders validates :name, presence: true # TODO: Make defaults better. This has to allow nil so the `attribute` default works, and I don't like that. @@ -115,14 +117,30 @@ class Character < ApplicationRecord end end - def do_equipment_break_checks(exclude_slots: []) - broken_items = [] - equipment.where.not(slot: exclude_slots).each do |equipment| - if equipment.break_check - broken_items.push(equipment.item) - end + def do_equipment_break_check(skill: nil) + skill = Skill.find_by_gid(skill) if skill&.is_a? String + # TODO: HACK: Should check other stats besides speed stat in the future. + # TODO: HACK: May not want a chance to break if speed is _reduced_. Though no equipment does this yet. + if skill + threatened_equipment = equipment.all.select { |eq| eq.effects&.select { |ef| ef[:gid] == "#{skill.gid}_speed" }&.any? } + else + threatened_equipment = equipment.all end - broken_items + break_slot = Equipment.random_break_slot + return nil unless break_slot + broken_equipment = threatened_equipment.find { |eq| eq.slot == break_slot } + return nil unless broken_equipment + broken_equipment.destroy + broken_equipment.item + end + + def do_item_infix_break_check(skill:) + skill = Skill.find_by_gid(skill) if skill.is_a? String + return nil unless ItemInfix.break_check + broken_ii = item_infixes.where(skill: skill).sample + return nil unless broken_ii + broken_ii.destroy + broken_ii.item end def has_item?(item, quantity = 1) @@ -136,6 +154,10 @@ class Character < ApplicationRecord self.equipment.find_by(item: item) end + def equipment_with_tag(tag) + self.equipment.all.find { |e| e.item.has_tag?(tag) } + end + def open_slots_for(item) full_slots = self.equipment.map { |e| e.slot } item.equip_slots.reject { |slot| full_slots.include?(slot) } @@ -182,6 +204,30 @@ class Character < ApplicationRecord end end + def max_infixes(skill) + skill = Skill.find_by_gid(skill) if skill.is_a? String + 1 + (skill_level(skill) / 20).floor + end + + def available_infixes(skill) + skill = Skill.find_by_gid(skill) if skill.is_a? String + max_infixes(skill) - item_infixes.where(skill: skill).count + end + + def infix(item, skill) + Character.transaction do + shift_item(item, -1) + item_infixes.create(item: item, skill: skill) + end + end + + def remove_infix(item_infix) + Character.transaction do + shift_item(item_infix.item, 1) + item_infix.destroy + end + end + def add_skill_xp(skill, amount) skill = Skill.find_by_gid(skill) if skill.is_a? String Character.transaction do @@ -247,7 +293,14 @@ class Character < ApplicationRecord activity.whatnot[:requirements]&.each do |requirement| case requirement[:type] when "equipment" - return false unless self.equipment_with_gid(requirement[:gid]) + if requirement[:tag] + return false unless self.equipment_with_tag(requirement[:tag]) + else + return false unless self.equipment_with_gid(requirement[:gid]) + end + when "stat" + # TODO: HACK: This won't work with built-in stats! Need to change this to work with power and whatnot. + return false unless self.total_stat_change(requirement[:gid]) >= requirement[:value] when "skill" return false unless self.skill_level(requirement[:gid]) >= requirement[:level] when "hearth_amenity" @@ -278,6 +331,7 @@ class Character < ApplicationRecord def start_resting return false if self.started_resting_at + stop_activity self.update(started_resting_at: Time.now) end @@ -306,17 +360,26 @@ class Character < ApplicationRecord hearth_amenity_effects = self.hearth.built_hearth_amenities.filter_map { |a| a.effects if a.effects } equipment_effects = self.equipment.filter_map { |a| a.effects if a.effects } state_effects = self.states.filter_map { |a| a.effects if a.effects && !a.expired? } - (hearth_amenity_effects + equipment_effects + state_effects).flatten + item_infix_effects = self.item_infixes.filter_map { |a| a.effects if a.effects } + (hearth_amenity_effects + equipment_effects + state_effects + item_infix_effects).flatten end def total_stat_change(gid) effects.filter_map { |e| e[:modifier] if e[:type] == "stat_change" && e[:gid] == gid }.sum end + def total_enemy_stat_change(gid) + effects.filter_map { |e| e[:modifier] if e[:type] == "enemy_stat_change" && e[:gid] == gid }.sum + end + def damage_ranges effects.filter_map { |e| { gid: e[:gid], min: e[:min], max: e[:max] } if e[:type] == "damage" } end + def dots + effects.filter_map { |e| { gid: e[:gid], min: e[:min], max: e[:max], message: e[:message] } if e[:type] == "dot" } + end + def planting_spots [total_stat_change("planting_spots"), 0].max end diff --git a/app/models/character_skill.rb b/app/models/character_skill.rb index 368cc28..406cade 100644 --- a/app/models/character_skill.rb +++ b/app/models/character_skill.rb @@ -39,6 +39,10 @@ class CharacterSkill < ApplicationRecord end end + def self.xp_required_for_level(level) + level <= 120 ? XP_TOTALS_PER_LEVEL[level - 1] : nil + end + def level XP_TOTALS_PER_LEVEL.each_with_index do |total, index| return index if total > self.xp @@ -46,7 +50,7 @@ class CharacterSkill < ApplicationRecord end def total_xp_for_next_level - xp_required_for_level(level + 1) + CharacterSkill.xp_required_for_level(level + 1) end def xp_to_next_level @@ -57,11 +61,13 @@ class CharacterSkill < ApplicationRecord CharacterSkill.top_xp_for(self.skill, limit: nil).map(&:character).map(&:id).index(self.character.id) + 1 end - private - def xp_required_for_level(level) - level <= 120 ? XP_TOTALS_PER_LEVEL[level - 1] : nil - end + def percentage_of_skill_level_completed + xp_gained_this_level = xp - CharacterSkill.xp_required_for_level(level) + total_xp_gain_neeeded_for_entire_level = total_xp_for_next_level - CharacterSkill.xp_required_for_level(level) + (xp_gained_this_level.to_f / total_xp_gain_neeeded_for_entire_level) * 100 + end + private def send_chat_message_if_leveled_up if CharacterSkill.level_for_xp(self.xp_was) < CharacterSkill.level_for_xp(self.xp) chat_message = ChatMessage.new(body: "reached #{self.skill.name} level #{self.level}!", diff --git a/app/models/concerns/has_costs_and_requirements.rb b/app/models/concerns/has_costs_and_requirements.rb index 34ff0f3..9c4abf8 100644 --- a/app/models/concerns/has_costs_and_requirements.rb +++ b/app/models/concerns/has_costs_and_requirements.rb @@ -19,8 +19,14 @@ module HasCostsAndRequirements case req[:type] when "skill" requirements.push "level #{req[:level]} #{Skill.find_by_gid(req[:gid]).name}" + when "stat" + requirements.push "#{req[:value]} #{req[:gid]}" when "equipment" - requirements.push "equipped #{Item.find_by_gid(req[:gid]).name}" + if req[:tag] + requirements.push "equipped #{req[:tag]}" + else + requirements.push "equipped #{Item.find_by_gid(req[:gid]).name}" + end when "hearth_amenity" requirements.push "level #{req[:level]} #{HearthAmenity.find_by_gid(req[:gid]).name}" else diff --git a/app/models/equipment.rb b/app/models/equipment.rb index e1dc7a3..12e8ae7 100644 --- a/app/models/equipment.rb +++ b/app/models/equipment.rb @@ -5,6 +5,17 @@ class Equipment < ApplicationRecord :left_ring, :right_ring, :waist, :legs, :feet, :curio] validates :slot, presence: true, uniqueness: { scope: :character } + def self.random_break_slot + roll = rand + if roll >= 0.99933333 + [:neck, :left_ring, :right_ring].sample + elsif roll >= 0.999 + [:back, :waist, :curio].sample + elsif roll >= 0.998 + [:mainhand, :offhand, :head, :torso, :grip, :legs, :feet].sample + end + end + def slot self[:slot].to_sym end @@ -14,14 +25,6 @@ class Equipment < ApplicationRecord end def break_check - roll = rand - if [:neck, :left_ring, :right_ring].include?(slot) - destroy and return true if roll > 0.9998 - elsif [:back, :waist, :curio].include?(slot) - destroy and return true if roll > 0.9996 - else - destroy and return true if roll > 0.999 - end false end end diff --git a/app/models/item.rb b/app/models/item.rb index 8b80788..0e25b2f 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -13,6 +13,13 @@ class Item < ApplicationRecord self.whatnot && self.whatnot[:use_effects]&.any? end + def infixable?(skill = nil) + skill = Skill.find_by_gid(skill) if skill.is_a? String + return false unless self.whatnot && self.whatnot[:infix_skills]&.any? + return true unless skill + self.whatnot[:infix_skills].select { |data| data[:gid] == skill.gid }.any? + end + def equip_slots return [] unless self.equipment? self.whatnot[:equip_slots].map { |data| data.to_sym } diff --git a/app/models/item_infix.rb b/app/models/item_infix.rb new file mode 100644 index 0000000..c484242 --- /dev/null +++ b/app/models/item_infix.rb @@ -0,0 +1,21 @@ +class ItemInfix < ApplicationRecord + belongs_to :character + belongs_to :item + belongs_to :skill + + before_create :check_max_infixes + + def self.break_check + rand >= 0.99 + end + + def effects + self.item.whatnot[:infix_effects] + end + + private + def check_max_infixes + current_infixes = character.item_infixes.where(skill: skill) + raise :abort if current_infixes.count >= character.max_infixes(skill) + end +end diff --git a/app/models/monster_kill.rb b/app/models/monster_kill.rb new file mode 100644 index 0000000..24519b1 --- /dev/null +++ b/app/models/monster_kill.rb @@ -0,0 +1,22 @@ +class MonsterKill < ApplicationRecord + belongs_to :monster + belongs_to :character + + validates :quantity, numericality: { greater_than_or_equal_to: 0, only_integer: true } + scope :ordered_by_monster_name, -> { includes(:monster).order("monsters.name") } + + after_save :award_titles + + private + def award_titles + character.award_title("spiteful") if quantity >= 1000 + character.award_title("hateful") if quantity >= 10_000 + character.award_title("vicious") if quantity >= 100_000 + + all_kills_quantity = character.monster_kills.sum(:quantity) + character.award_title("slayer") if all_kills_quantity >= 1_000 + character.award_title("butcher") if all_kills_quantity >= 10_000 + character.award_title("slaughterer") if all_kills_quantity >= 100_000 + character.award_title("massacrer") if all_kills_quantity >= 1_000_000 + end +end diff --git a/app/views/application/_navbar.html.erb b/app/views/application/_navbar.html.erb index be2d9f2..e315b42 100644 --- a/app/views/application/_navbar.html.erb +++ b/app/views/application/_navbar.html.erb @@ -1,21 +1,27 @@ -<ul class="py-2 px-2 col-span-12 text-display"> +<ul class="py-2 px-2 col-span-12 text-display space-x-2.5"> <% if current_char %> - <li class="mr-6 inline"> + <li class="inline"> <%= link_to "Locations", locations_path %> </li> - <li class="mr-6 inline"> + <li class="inline"> <%= link_to "Character", character_path(current_char) %> </li> - <li class="mr-6 inline"> + <li class="inline"> + <%= link_to "Skills", character_skills_path(current_char) %> + </li> + <li class="inline"> <%= link_to "Inventory", character_items_path(current_char) %> </li> - <li class="mr-6 inline"> + <li class="inline"> + <%= link_to "Spells", character_spells_path(current_char) %> + </li> + <li class="inline"> <%= link_to "Hearth", character_hearth_path(current_char) %> </li> - <li class="mr-6 inline"> + <li class="inline"> <%= link_to "Bazaar", bazaar_path %> </li> - <li class="mr-6 inline"> + <li class="inline"> <%= link_to "Messages", messages_path %> </li> <% end %> diff --git a/app/views/application/_results.html.erb b/app/views/application/_results.html.erb index ec62991..a7dc002 100644 --- a/app/views/application/_results.html.erb +++ b/app/views/application/_results.html.erb @@ -13,7 +13,7 @@ <p>You planted <%= link_to result[:hearth_planting].item.name, item_path(result[:hearth_planting].item) %> in the loam.</p> <% when "activity" %> - <p>You realized how to <%= result[:activity].name %>!</p> + <p>You learned how to <%= result[:activity].name %>!</p> <% when "monster" %> <p>You encountered a <%= result[:monster].name %>.</p> <p class="text-xs italic"><%= result[:monster].description %></p> @@ -24,6 +24,8 @@ <p class="text-xs">You gained <%= result[:xp] %> <%= result[:skill].name %> XP.</p> <% when "title" %> <p>You earned the title <%= render "application/components/text/title", title: result[:title] %>!</p> + <% when "condition" %> + <p>You gained the <%= result[:condition].name %> condition.</p> <% when "message" %> <p><%= result[:body] %></p> <% when "warning" %> diff --git a/app/views/application/_timer.html.erb b/app/views/application/_timer.html.erb index d683c6a..51d9b81 100644 --- a/app/views/application/_timer.html.erb +++ b/app/views/application/_timer.html.erb @@ -1,61 +1,63 @@ -<% if current_char.activity %> - <h2 class="text-lg text-display text-center"><%= current_char.activity.name %></h2> - <div data-controller="timer" - data-timer-time-remaining-value="<%= current_char.activity_time_remaining %>" - data-timer-activity-duration-value="<%= current_char.activity_duration - current_char.rested_duration_to_spend_on_activity %>" - data-timer-post-url-value="<%= finish_activity_url %>"> - <div class="text-center"> - <span data-timer-target="timer" class="text-xl text-display"></span> - </div> - <div class="border border-gray-800 h-4 my-1"> - <div data-timer-target="progressBar" class="bg-gray-600 h-full" - style="width: <%= current_char.percentage_of_activity_completed %>%"> +<% if current_char %> + <% if current_char.activity %> + <h2 class="text-lg text-display text-center"><%= current_char.activity.name %></h2> + <div data-controller="timer" + data-timer-time-remaining-value="<%= current_char.activity_time_remaining %>" + data-timer-activity-duration-value="<%= current_char.activity_duration - current_char.rested_duration_to_spend_on_activity %>" + data-timer-post-url-value="<%= finish_activity_url %>"> + <div class="text-center"> + <span data-timer-target="timer" class="text-xl text-display"></span> + </div> + <div class="border border-gray-800 h-4 my-1"> + <div data-timer-target="progressBar" class="bg-gray-600 h-full" + style="width: <%= current_char.percentage_of_activity_completed %>%"> + </div> </div> </div> - </div> - - <% most_recent_cs = current_char.character_skills.order(:updated_at).last %> - <div class="text-center text-sm"> - <div class="text-xs"><%= most_recent_cs.skill.name %> level <%= most_recent_cs.level %></div> - <div><%= most_recent_cs.xp_to_next_level %> XP to next level</div> - </div> - <div class="text-center my-2"> - <%= button_to "Stop", stop_activity_path, class: "text-sm" %> - </div> - - <div class="text-center text-xs my-2"> - <% if current_char.activity.gid.include?("beastslay") %> - <%= current_char.wounds %> / <%= pluralize(current_char.max_wounds, "wound") %> - <% end %> - <% current_char.active_states.each do |state| %> - <div> - <%= state.condition.name %> - </div> - <div> - (expires in <%= distance_of_time_in_words_to_now(state.expires_at) %>) - </div> - <% end %> - </div> + <% most_recent_cs = current_char.character_skills.order(:updated_at).last %> + <div class="text-center text-sm"> + <div class="text-xs"><%= most_recent_cs.skill.name %> level <%= most_recent_cs.level %></div> + <div><%= most_recent_cs.xp_to_next_level %> XP to next level</div> + </div> -<% else %> - <div class="text-center"> - <% if current_char.resting? %> - <p>You're resting.</p> - <% else %> - <p>You're not doing anything.</p> - <% end %> + <div class="text-center my-2"> + <%= button_to "Stop", stop_activity_path, class: "text-sm" %> + </div> - <div class="my-2"> - <%= button_to current_char.resting? ? "Stop Resting" : "Start Resting", toggle_resting_path %> + <div class="text-center text-xs my-2"> + <% if current_char.activity.gid.include?("beastslay") %> + <%= current_char.wounds %> / <%= pluralize(current_char.max_wounds, "wound") %> + <% end %> + <% current_char.active_states.each do |state| %> + <div> + <%= state.condition.name %> + </div> + <div> + (expires in <%= distance_of_time_in_words_to_now(state.expires_at) %>) + </div> + <% end %> </div> - <div class="text-xs"> - You have <%= distance_of_time_in_words_to_now(current_char.rested_until) %> of rested time. + <% else %> + <div class="text-center"> <% if current_char.resting? %> - This does not include time from your current rest. That time will be added when you stop resting. + <p>You're resting.</p> + <% else %> + <p>You're not doing anything.</p> <% end %> + + <div class="my-2"> + <%= button_to current_char.resting? ? "Stop Resting" : "Start Resting", toggle_resting_path %> + </div> + + <div class="text-xs"> + You have <%= distance_of_time_in_words_to_now(current_char.rested_until) %> of rested time. + <% if current_char.resting? %> + This does not include time from your current rest. That time will be added when you stop resting. + <% end %> + </div> </div> - </div> + <% end %> <% end %> diff --git a/app/views/application/components/text/_title.html.erb b/app/views/application/components/text/_title.html.erb index a7c2a7e..5e1d5a2 100644 --- a/app/views/application/components/text/_title.html.erb +++ b/app/views/application/components/text/_title.html.erb @@ -10,6 +10,20 @@ <span class="text-purple-200">Aspirant</span> <% when "sentinel" %> <span class="text-gray-500">S</span><span class="text-gray-400">e</span><span class="text-gray-300">n</span><span class="text-gray-200">ti</span><span class="text-gray-300">n</span><span class="text-gray-400">e</span><span class="text-gray-500">l</span> + <% when "spiteful" %> + <span class="text-transparent bg-clip-text bg-gradient-to-b from-gray-500 to-red-900">Spiteful</span> + <% when "hateful" %> + <span class="text-transparent bg-clip-text bg-gradient-to-b from-gray-600 to-red-800">Hateful</span> + <% when "vicious" %> + <span class="text-transparent bg-clip-text bg-gradient-to-b from-gray-700 to-red-700">Vicious</span> + <% when "slayer" %> + <span class="text-red-400">Slayer</span> + <% when "butcher" %> + <span class="text-red-500">Butcher</span> + <% when "slaughterer" %> + <span class="text-red-600">Slaughterer</span> + <% when "massacrer" %> + <span class="text-red-700">Massacrer</span> <% else %> <%= title.name %> <% end %> diff --git a/app/views/characters/bestiary/index.html.erb b/app/views/characters/bestiary/index.html.erb new file mode 100644 index 0000000..f7376e9 --- /dev/null +++ b/app/views/characters/bestiary/index.html.erb @@ -0,0 +1,22 @@ +<h1 class="text-3xl mb-4">Bestiary</h1> + +<% if @monster_kills.any? %> + <table class="table-auto"> + <thead> + <tr> + <th class="table-header-padded">Monster</th> + <th class="table-header-padded">Kills</th> + </tr> + </thead> + <tbody> + <% @monster_kills.ordered_by_monster_name.each do |mk| %> + <tr> + <td class="table-cell-padded"><%= mk.monster.name %></td> + <td class="table-cell-padded"><%= mk.quantity %></td> + </tr> + <% end %> + </tbody> + </table> +<% else %> + <p>You haven't killed any monsters yet.</p> +<% end %> diff --git a/app/views/characters/items/index.html.erb b/app/views/characters/items/index.html.erb index 671e68f..57e8531 100644 --- a/app/views/characters/items/index.html.erb +++ b/app/views/characters/items/index.html.erb @@ -35,6 +35,10 @@ character_items: @character.character_items.ordered_by_item_name.select { |ci| ci.item.has_tag?("currency") } %> +<%= render "characters/items/inventory_section", heading: "Omens", + character_items: @character.character_items.ordered_by_item_name.select { |ci| + ci.item.has_tag?("omen") } %> + <%= render "characters/items/inventory_section", heading: "Seeds", character_items: @character.character_items.ordered_by_item_name.select { |ci| ci.item.has_tag?("seed") } %> diff --git a/app/views/characters/show.html.erb b/app/views/characters/show.html.erb index a7b7c0e..ce8c461 100644 --- a/app/views/characters/show.html.erb +++ b/app/views/characters/show.html.erb @@ -5,6 +5,7 @@ <div class="text-lg text-display mb-4"> <ul class="flex flex-row"> <li class="mr-2"><%= link_to "Titles", character_titles_path(@character) %></li> + <li class="mr-2"><%= link_to "Bestiary", character_bestiary_path(@character) %></li> <li class="mr-2"><%= link_to "Rankings", character_rankings_path(@character) %></li> </ul> </div> @@ -30,7 +31,7 @@ <div class="my-2"> <% if @character == current_char %> <h2 class="text-xl mb-2">Combat Styles</h2> - <%= form_with url: character_combat_styles_path(character_id: @character) do |f| %> + <%= form_with url: combat_styles_character_path(@character) do |f| %> <%= f.label :offensive_style, "Offensive" %> <%= f.select :offensive_style, Character.offensive_styles.keys.to_a, selected: @character.offensive_style %> @@ -149,31 +150,6 @@ </div> </div> -<div class="my-6"> - <h2 class="text-xl mb-4">Skills</h2> - - <table class="table-auto mb-8"> - <thead> - <tr> - <th class="table-header-padded">Skill</th> - <th class="table-header-padded">Level</th> - <th class="table-header-padded">XPTNL</th> - <th class="table-header-padded">Total XP</th> - </tr> - </thead> - <tbody> - <% @character.character_skills.ordered_by_skill_name.each do |cs| %> - <tr> - <td class="table-cell-padded"><%= cs.skill.name %></td> - <td class="table-cell-padded"><%= cs.level %></td> - <td class="table-cell-padded"><%= cs.xp_to_next_level %></td> - <td class="table-cell-padded"><%= cs.xp %></td> - </tr> - <% end %> - </tbody> - </table> -</div> - <% if @character == current_char %> <%= link_to "Manage account", edit_user_registration_path, class: "text-sm" %> <% end %> diff --git a/app/views/characters/skills/_infix_slot.html.erb b/app/views/characters/skills/_infix_slot.html.erb new file mode 100644 index 0000000..0113596 --- /dev/null +++ b/app/views/characters/skills/_infix_slot.html.erb @@ -0,0 +1,3 @@ +<div class="flex justify-between items-center border-t border-gray-700 p-2"> + <%= yield %> +</div> diff --git a/app/views/characters/skills/index.html.erb b/app/views/characters/skills/index.html.erb new file mode 100644 index 0000000..af40e38 --- /dev/null +++ b/app/views/characters/skills/index.html.erb @@ -0,0 +1,58 @@ +<h2 class="text-3xl mb-2">Skills</h2> +<div class="grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> + <% @character.character_skills.ordered_by_skill_name.each do |cs| %> + <div> + <div class="rounded border border-gray-700"> + <div class="flex p-1"> + <div class="flex-grow"> + <div class="text-xl text-display mb-1"> + <%= cs.skill.name %> + </div> + <div class="flex items-center text-xs space-x-4"> + <div><span class="bg-gray-700 px-1 py-0.5 rounded mr-1">XP</span><%= cs.xp %></div> + <div><span class="bg-gray-700 px-1 py-0.5 rounded mr-1">TNL</span><%= cs.xp_to_next_level %></div> + </div> + </div> + <div class="text-xl m-2 text-display"> + <%= cs.level %> + </div> + </div> + <div class="border border-gray-700 h-2 mt-1 -mb-px -mx-px"> + <div class="bg-gray-600 h-full" style="width: <%= cs.percentage_of_skill_level_completed %>%"> + </div> + </div> + <% @character.item_infixes.where(skill: cs.skill).each do |ii| %> + <%= render "characters/skills/infix_slot" do %> + <div> + <%= ii.item.name %> + </div> + <div> + <%= button_to "Remove", character_item_infix_path(id: ii.id), method: :delete %> + </div> + <% end %> + <% end %> + <% @character.available_infixes(cs.skill).times do %> + <%= render "characters/skills/infix_slot" do %> + <%# TODO: Don't load all into memory %> + <% infixable_items = @character.items.select {|i| i.infixable?(cs.skill)} %> + <% if infixable_items.any? %> + <%= form_with url: character_item_infixes_path, class: "w-full" do |f| %> + <div class="flex space-x-1"> + <div class="flex-grow"> + <%= f.select :item_id, infixable_items.map { |i| [i.name, i.id]}, {}, class: "w-full" %> + <%= f.hidden_field :skill_id, value: cs.skill.id %> + </div> + <div> + <%= f.submit "Infix" %> + </div> + </div> + <% end %> + <% else %> + <div class="text-gray-500">No omens to infix.</div> + <% end %> + <% end %> + <% end %> + </div> + </div> + <% end %> +</div> diff --git a/app/views/characters/spells/index.html.erb b/app/views/characters/spells/index.html.erb new file mode 100644 index 0000000..5f415a3 --- /dev/null +++ b/app/views/characters/spells/index.html.erb @@ -0,0 +1,10 @@ +<h2 class="text-3xl mb-2">Spells</h2> +<div data-controller="activity-select"> + <%= form_with url: start_activity_path, method: :post do |f| %> + <%= f.select :id, @spell_activities.map { |a| [a.name, a.id] }, {}, + { data: { activity_select_target: "select", action: "activity-select#load" } } %> + <%= f.number_field :actions, value: 1, size: 5, min: 1, max: 2_000_000_000 %> + <%= f.submit "Cast" %> + <% end %> + <div data-activity-select-target="output" class="my-1"></div> +</div> diff --git a/app/views/game/finish_activity.js.erb b/app/views/game/finish_activity.js.erb index fa3d228..f281986 100644 --- a/app/views/game/finish_activity.js.erb +++ b/app/views/game/finish_activity.js.erb @@ -4,10 +4,16 @@ var resultControlsDiv = document.getElementById("activity_controls"); var outputHTML = "<%= j render(partial: "application/results", locals: { results: @results }) %>" if (resultOutputDiv) { - resultOutputDiv.innerHTML += outputHTML; - // If scrolled to the bottom or near the bottom, then smooth scroll to the bottom. + // (Check before adding output, because a large output could prevent the scroll when it shouldn't.) + var shouldScroll = false; if ((resultOutputDiv.scrollTop + 100) >= (resultOutputDiv.scrollHeight - resultOutputDiv.offsetHeight)) { + shouldScroll = true; + } + + resultOutputDiv.innerHTML += outputHTML; + + if (shouldScroll) { resultOutputDiv.scrollTo({ top: resultOutputDiv.scrollHeight, left: 0, behavior: 'smooth' }); diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 309dc29..048a6ed 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -40,6 +40,8 @@ </div> <div class="game-container-box overflow-auto text-sm p-2 flex-grow" id="result_output" style="max-height: 60%" + data-controller="results" + data-results-target="output" data-turbolinks-permanent> </div> </div> |