summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/controllers/game_controller.rb246
-rw-r--r--app/lib/activity_processor.rb258
-rw-r--r--app/models/character.rb2
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]