diff options
author | David Gay <david@davidgay.org> | 2021-06-05 17:15:18 -0400 |
---|---|---|
committer | David Gay <david@davidgay.org> | 2021-06-05 17:15:18 -0400 |
commit | 4e89b79597f87057b866b715173c0a6da77c8cfa (patch) | |
tree | b4c4474377128c58d460dd3eda49a6e5077a2676 /app | |
parent | 569db9a18911bf954914b0ace678ca79e98cf12a (diff) |
Split activity processing code out into a new class
Diffstat (limited to 'app')
-rw-r--r-- | app/controllers/application_controller.rb | 4 | ||||
-rw-r--r-- | app/controllers/game_controller.rb | 246 | ||||
-rw-r--r-- | app/lib/activity_processor.rb | 258 | ||||
-rw-r--r-- | app/models/character.rb | 2 |
4 files changed, 260 insertions, 250 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 81fb58a..fc38d15 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,10 +11,6 @@ class ApplicationController < ActionController::Base redirect_to new_character_path if (current_user && current_char.nil?) end - def roll(sides) - rand(sides) + 1 - end - private def start_activity(activity) if current_char.resting? diff --git a/app/controllers/game_controller.rb b/app/controllers/game_controller.rb index 82b7688..39ea6c9 100644 --- a/app/controllers/game_controller.rb +++ b/app/controllers/game_controller.rb @@ -22,250 +22,6 @@ class GameController < ApplicationController end def finish_activity - @results = [] - return unless current_char.activity_time_remaining <= 0 - activity = current_char.activity - - if current_char.resting? - @results.replace([{ type: "error", message: "You can't do anything while you're resting." }]) - current_char.stop_activity - return - end - - 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 - if current_char.rested_duration > 0 - remaining_rested_duration = current_char.rested_duration - current_char.rested_duration_to_spend_on_activity - current_char.update!(rested_duration: remaining_rested_duration) - end - - current_char.pay_cost_for(activity) - - activity.whatnot[:results].each do |result| - type = result[:type] - case type - when "xp" - handle_xp_result(result) - when "monster" - raise TooManyWoundsError unless current_char.can_fight? - next if rand > (result[:chance] || 1) - - @results.push({ type: "br" }) - - 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] - 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" - handle_item_result(result) - 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 "hearth_planting" - unless current_char.hearth.available_planting_spots > 0 - @results.replace([{ type: "error", message: "You're out of space to plant seeds." }]) - current_char.stop_activity - return - end - item = Item.find_by_gid(result[:gid]) - hp = current_char.hearth.hearth_plantings.create(item: item) - @results.push({ type: type, hearth_planting: hp }) - 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] - 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." }) - return - end - else - current_char.update(activity_started_at: Time.now) - end - - unless @results.any? - @results.push({ type: "message", body: "You come up empty." }) - end - - # HACK: To display any titles that were gained indirectly (not as part of the activity results). - current_char.title_awards.where(created_at: 5.seconds.ago..).each do |title_award| - @results.push({ type: "title", title: title_award.title }) - end - end - rescue ItemQuantityError - current_char.stop_activity - @results.replace([{ type: "error", message: "You don't have enough items to complete this activity." }]) - rescue HearthPlantingError - current_char.stop_activity - @results.replace([{ type: "error", message: "You don't have that crop planted." }]) - 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." }]) + @results = ActivityProcessor.new(current_char).results 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(20) + char.speed - mon_initative = roll(20) + 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 - blocked_damage = [blocked_damage, (dealt_damage - 1)].min - 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 - return - 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" - handle_title_result(award_data) - when "xp" - handle_xp_result(award_data) - when "item" - handle_item_result(award_data) - else - raise "Invalid award type string (#{award_data[:type]})" - end - end - end - break - end - end - end - - def handle_title_result(data) - if current_char.award_title(data[:gid]) - @results.push({ type: "title", title: title }) - end - end - - def handle_xp_result(data) - skill = Skill.find_by_gid(data[:skill]) - amount = data[:base] - current_char.add_skill_xp(skill, amount) - @results.push({ type: "xp", skill: skill, xp: amount }) - end - - def handle_item_result(data) - return if rand > (data[:chance] || 1) - - if data[:table] - table_roll = rand - - 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] - - if table_roll >= score - give_item(table_entry, quantity) - - table_entry[:titles]&.each do |title_data| - handle_title_result(title_data) - end - break - end - end - else - min_quantity = data[:min_quantity] || data[:quantity] || 1 - max_quantity = data[:max_quantity] || data[:quantity] || 1 - quantity = rand(min_quantity..max_quantity) - give_item(data, quantity) - end - end end diff --git a/app/lib/activity_processor.rb b/app/lib/activity_processor.rb new file mode 100644 index 0000000..004f8de --- /dev/null +++ b/app/lib/activity_processor.rb @@ -0,0 +1,258 @@ +class ActivityProcessor + attr_reader :character, :activity, :results + + def initialize(character) + @character = character + @activity = character.activity + @results = [] + process + end + + def process + return unless @character.activity_time_remaining <= 0 + if @character.resting? + @results.replace([{ type: "error", message: "You can't do anything while you're resting." }]) + @character.stop_activity + return + end + + unless @character.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 }]) + @character.stop_activity + return + end + + Character.transaction do + if @character.rested_duration > 0 + remaining_rested_duration = @character.rested_duration - @character.rested_duration_to_spend_on_activity + @character.update!(rested_duration: remaining_rested_duration) + end + + @character.pay_activity_cost + + @activity.whatnot[:results].each do |result| + type = result[:type] + case type + when "xp" + handle_xp_result(result) + when "monster" + raise TooManyWoundsError unless @character.can_fight? + next if rand > (result[:chance] || 1) + + @results.push({ type: "br" }) + + if result[:table].pluck(:gid).include?(@character.trophy_monster_gid) + monster = Monster.find_by_gid(@character.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] + if table_roll >= score + monster = Monster.find_by_gid(table_entry[:gid]) + @results.push({ type: type, monster: monster }) + resolve_combat_with(monster) + break + end + end + when "item" + handle_item_result(result) + when "hearth_amenity" + bhi = @character.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 "hearth_planting" + unless @character.hearth.available_planting_spots > 0 + @results.replace([{ type: "error", message: "You're out of space to plant seeds." }]) + @character.stop_activity + return + end + item = Item.find_by_gid(result[:gid]) + hp = @character.hearth.hearth_plantings.create(item: item) + @results.push({ type: type, hearth_planting: hp }) + 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] + if table_roll >= score + new_activity = Activity.find_by_gid(table_entry[:gid]) + unless @character.learned_activities.exists?(activity: new_activity) + @character.learned_activities.create(activity: new_activity) + @results.push({ type: type, activity: new_activity }) + end + end + end + else + raise "Invalid result type (#{type})" # TODO: Improve this. + end + end + + if @character.activity && @character.queued_actions + if @character.queued_actions > 0 + @character.queued_actions -= 1 + @character.activity_started_at = Time.now + @character.save + else + @character.stop_activity + @results.push({ type: "message", body: "You have finished your work." }) + return + end + else + @character.update(activity_started_at: Time.now) + end + + unless @results.any? + @results.push({ type: "message", body: "You come up empty." }) + end + + # HACK: To display any titles that were gained indirectly (not as part of the activity results). + @character.title_awards.where(created_at: 5.seconds.ago..).each do |title_award| + @results.push({ type: "title", title: title_award.title }) + end + end + rescue ItemQuantityError + @character.stop_activity + @results.replace([{ type: "error", message: "You don't have enough items to complete this activity." }]) + rescue HearthPlantingError + @character.stop_activity + @results.replace([{ type: "error", message: "You don't have that crop planted." }]) + rescue TooManyWoundsError + @character.stop_activity + @results.replace([{ type: "error", + message: "You can't fight in your condition. You'll have to heal a wound." }]) + end + + private + def roll(sides) + rand(sides) + 1 + end + + 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| + @character.add_skill_xp(award[:skill], (award[:amount] * quantity)) + end + end + @character.shift_item(item, quantity) + @results.push({ type: "item", item: item, quantity: quantity, xp: xp_awards }) + end + + def handle_title_result(data) + if @character.award_title(data[:gid]) + @results.push({ type: "title", title: title }) + end + end + + def handle_xp_result(data) + skill = Skill.find_by_gid(data[:skill]) + amount = data[:base] + @character.add_skill_xp(skill, amount) + @results.push({ type: "xp", skill: skill, xp: amount }) + end + + def handle_item_result(data) + return if rand > (data[:chance] || 1) + + if data[:table] + table_roll = rand + + 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] + + if table_roll >= score + give_item(table_entry, quantity) + + table_entry[:titles]&.each do |title_data| + handle_title_result(title_data) + end + break + end + end + else + min_quantity = data[:min_quantity] || data[:quantity] || 1 + max_quantity = data[:max_quantity] || data[:quantity] || 1 + quantity = rand(min_quantity..max_quantity) + give_item(data, quantity) + end + end + + def resolve_combat_with(mon) + char = @character + char_hp = @character.max_hp + mon_hp = mon.max_hp + combat_message = ->(msg) { @results.push({ type: "message", body: "[#{char_hp}/#{char.max_hp}] #{msg}" }) } + char_initiative = roll(20) + char.speed + mon_initative = roll(20) + 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 + blocked_damage = [blocked_damage, (dealt_damage - 1)].min + 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 + return + 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" + handle_title_result(award_data) + when "xp" + handle_xp_result(award_data) + when "item" + handle_item_result(award_data) + else + raise "Invalid award type string (#{award_data[:type]})" + end + end + end + break + end + end + end +end diff --git a/app/models/character.rb b/app/models/character.rb index b4e597b..91f0a66 100644 --- a/app/models/character.rb +++ b/app/models/character.rb @@ -100,7 +100,7 @@ class Character < ApplicationRecord self.states.all.select { |s| !s.expired? } end - def pay_cost_for(activity) + def pay_activity_cost CharacterItem.transaction do activity.whatnot[:cost]&.each do |cost| case cost[:type] |