class GameController < ApplicationController def stop_activity current_char.stop_activity redirect_to locations_path end def finish_activity @results = [] return unless current_char.activity_time_remaining <= 0 activity = current_char.activity unless current_char.can_do_activity?(activity) message = "You can't do this right now." message += " (requires #{activity.requirements&.join(", ")})" if activity.requirements.any? message += " (costs #{activity.costs&.join(", ")})" if activity.costs.any? @results.replace([{ type: "error", message: message }]) current_char.stop_activity return end Character.transaction do current_char.pay_cost_for(activity) activity.whatnot[:results].each do |result| type = result[:type] case type when "xp" skill = Skill.find_by_gid(result[:gid]) amount = result[:base] current_char.add_skill_xp(skill, amount) @results.push({ type: "xp", skill: skill, xp: amount }) when "monster" raise TooManyWoundsError unless current_char.can_fight? next if rand > (result[:chance] || 1) if result[:table].pluck(:gid).include?(current_char.trophy_monster_gid) monster = Monster.find_by_gid(current_char.trophy_monster_gid) @results.push({ type: type, monster: monster }) resolve_combat_with(monster) break end table_roll = rand result[:table].sort_by { |t| -t[:score] }.each do |table_entry| score = table_entry[:score] result[:table_scaling]&.each do |scale_entry| case scale_entry[:type] when "skill" score = score**(1 + (scale_entry[:scale_value] * current_char.skill_level(scale_entry[:gid]))) end end if table_roll >= score activity = Activity.find_by_gid(table_entry[:gid]) monster = Monster.find_by_gid(table_entry[:gid]) @results.push({ type: type, monster: monster }) resolve_combat_with(monster) break end end when "item" next if rand > (result[:chance] || 1) if result[:table] table_roll = rand result[:table].sort_by { |t| -t[:score] }.each do |table_entry| min_quantity = table_entry[:min_quantity] || table_entry[:quantity] || 1 max_quantity = table_entry[:max_quantity] || table_entry[:quantity] || 1 quantity = rand(min_quantity..max_quantity) score = table_entry[:score] table_scaling = result[:table_scaling] table_scaling&.each do |scale_entry| case scale_entry[:type] when "skill" score = score**(1 + (scale_entry[:scale_value] * current_char.skill_level(scale_entry[:gid]))) end end if table_roll >= score give_item(table_entry, quantity, with_xp: true) table_entry[:titles]&.each do |title_data| title = Title.find_by_gid(title_data[:gid]) if current_char.award_title(title) @results.push({ type: "title", title: title }) end end break end end else min_quantity = result[:min_quantity] || result[:quantity] || 1 max_quantity = result[:max_quantity] || result[:quantity] || 1 quantity = rand(min_quantity..max_quantity) give_item(result, quantity, with_xp: true) end when "hearth_amenity" bhi = current_char.hearth.built_hearth_amenities .find_or_initialize_by(hearth_amenity: HearthAmenity.find_by_gid(result[:gid])) bhi.update(level: result[:level]) @results.push({ type: type, hearth_amenity: bhi.hearth_amenity }) when "activity" next if rand > (result[:chance] || 1) table_roll = rand result[:table].sort_by { |t| -t[:score] }.each do |table_entry| score = table_entry[:score] result[:table_scaling]&.each do |scale_entry| case scale_entry[:type] when "skill" score = score**(1 + (scale_entry[:scale_value] * current_char.skill_level(scale_entry[:gid]))) end end if table_roll >= score activity = Activity.find_by_gid(table_entry[:gid]) unless current_char.learned_activities.exists?(activity: activity) current_char.learned_activities.create(activity: activity) @results.push({ type: type, activity: activity }) end end end else raise "Invalid result type (#{type})" # TODO: Improve this. end end if current_char.activity && current_char.queued_actions if current_char.queued_actions > 0 current_char.queued_actions -= 1 current_char.activity_started_at = Time.now current_char.save else current_char.stop_activity @results.push({ type: "message", body: "You have finished your work." }) end else current_char.update(activity_started_at: Time.now) end unless @results.any? @results.push({ type: "message", body: "You come up empty." }) end end rescue ItemQuantityError current_char.stop_activity @results.replace([{ type: "error", message: "You don't have enough items to complete this activity." }]) rescue TooManyWoundsError current_char.stop_activity @results.replace([{ type: "error", message: "You can't fight in your condition. You'll have to heal a wound." }]) end private def give_item(data, quantity, with_xp: false) item = Item.find_by_gid(data[:gid]) xp_awards = [] if with_xp xp_awards = data[:xp]&.map { |xpe| { skill: Skill.find_by_gid(xpe[:gid]), amount: xpe[:value] } } xp_awards&.each do |award| current_char.add_skill_xp(award[:skill], (award[:amount] * quantity)) end end current_char.shift_item(item, quantity) @results.push({ type: "item", item: item, quantity: quantity, xp: xp_awards }) end def resolve_combat_with(mon) char = current_char char_hp = current_char.max_hp mon_hp = mon.max_hp combat_message = ->(msg) { @results.push({ type: "message", body: "[#{char_hp}/#{char.max_hp}] #{msg}" }) } char_initiative = roll(10) + char.speed mon_initative = roll(10) + mon.speed if char_initiative > mon_initative turn_order = [[char, mon], [mon, char]] elsif mon_initative > char_initiative turn_order = [[mon, char], [char, mon]] else 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) evasion_roll = roll(20) + target.evasion(with_combat_style: true) if accuracy_roll >= evasion_roll dealt_damage = roll(4) + actor.power(with_combat_style: true) # TODO: Replace d4 with weapon damage if base_accuracy_roll == 20 combat_message.call("#{actor.name} landed a critical hit!") dealt_damage = dealt_damage * 2 end blocked_damage = (accuracy_roll >= (roll(20) + target.block(with_combat_style: true))) ? 0 : target.block_value resolved_damage = dealt_damage - blocked_damage actor == char ? mon_hp -= resolved_damage : char_hp -= resolved_damage damage_text = "#{resolved_damage} damage." damage_text += " (#{dealt_damage} - #{blocked_damage} blocked)" if blocked_damage > 0 combat_message.call("#{actor.name} hit for #{damage_text}") elsif evasion_roll > accuracy_roll combat_message.call("#{target.name} evaded #{actor.name}'s attack.") end if char_hp < 1 || mon_hp < 1 if char_hp < 1 @results.push({ type: "message", body: "You were defeated! You retreat, wounded." }) char.wounds += 1 char.save! unless char.can_fight? @results.push([{ type: "error", message: "You can't fight in your condition. You'll have to heal a wound." }]) char.stop_activity unless char.can_fight? end else @results.push({ type: "message", body: "You slew the #{mon.name}." }) mon.whatnot[:awards]&.each do |award_data| case award_data[:type] when "title" title = Title.find_by_gid(award_data[:gid]) if current_char.award_title(title) @results.push({ type: "title", title: title }) end when "xp" skill = Skill.find_by_gid(award_data[:skill]) amount = award_data[:base] char.add_skill_xp(skill, amount) @results.push({ type: "xp", skill: skill, xp: amount }) when "item" # TODO: This is basically duplicated from earlier next if rand > (award_data[:chance] || 1) if award_data[:table] table_roll = rand award_data[:table].sort_by { |t| -t[:score] }.each do |table_entry| min_quantity = table_entry[:min_quantity] || table_entry[:quantity] || 1 max_quantity = table_entry[:max_quantity] || table_entry[:quantity] || 1 quantity = rand(min_quantity..max_quantity) score = table_entry[:score] table_scaling = award_data[:table_scaling] table_scaling&.each do |scale_entry| case scale_entry[:type] when "skill" score = score**(1 + (scale_entry[:scale_value] * current_char.skill_level(scale_entry[:gid]))) end end if table_roll >= score give_item(table_entry, quantity) table_entry[:titles]&.each do |title_data| title = Title.find_by_gid(title_data[:gid]) if current_char.award_title(title) @results.push({ type: "title", title: title }) end end break end end else min_quantity = award_data[:min_quantity] || award_data[:quantity] || 1 max_quantity = award_data[:max_quantity] || award_data[:quantity] || 1 quantity = rand(min_quantity..max_quantity) give_item(award_data, quantity) end else raise "Invalid award type string (#{award_data[:type]})" end end end break end end end end