summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/characters/bestiary_controller.rb12
-rw-r--r--app/controllers/characters/hearth_controller.rb3
-rw-r--r--app/controllers/characters/item_infixes_controller.rb27
-rw-r--r--app/controllers/characters/items_controller.rb12
-rw-r--r--app/controllers/characters/rankings_controller.rb2
-rw-r--r--app/controllers/characters/skills_controller.rb15
-rw-r--r--app/controllers/characters/spells_controller.rb9
-rw-r--r--app/controllers/characters_controller.rb15
-rw-r--r--app/controllers/leaderboard_controller.rb2
-rw-r--r--app/errors/item_infix_error.rb2
-rw-r--r--app/javascript/channels/chat_room_channel.js11
-rw-r--r--app/javascript/controllers/results_controller.js13
-rw-r--r--app/lib/activity_processor.rb63
-rw-r--r--app/models/character.rb81
-rw-r--r--app/models/character_skill.rb16
-rw-r--r--app/models/concerns/has_costs_and_requirements.rb8
-rw-r--r--app/models/equipment.rb19
-rw-r--r--app/models/item.rb7
-rw-r--r--app/models/item_infix.rb21
-rw-r--r--app/models/monster_kill.rb22
-rw-r--r--app/views/application/_navbar.html.erb20
-rw-r--r--app/views/application/_results.html.erb4
-rw-r--r--app/views/application/_timer.html.erb100
-rw-r--r--app/views/application/components/text/_title.html.erb14
-rw-r--r--app/views/characters/bestiary/index.html.erb22
-rw-r--r--app/views/characters/items/index.html.erb4
-rw-r--r--app/views/characters/show.html.erb28
-rw-r--r--app/views/characters/skills/_infix_slot.html.erb3
-rw-r--r--app/views/characters/skills/index.html.erb58
-rw-r--r--app/views/characters/spells/index.html.erb10
-rw-r--r--app/views/game/finish_activity.js.erb10
-rw-r--r--app/views/layouts/application.html.erb2
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>