coop_al 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rubocop.yml +9 -0
- data/Gemfile +3 -0
- data/README.md +89 -0
- data/Rakefile +10 -0
- data/coop.gemspec +29 -0
- data/examples/filler.rb +9 -0
- data/examples/test.rb +15 -0
- data/exe/coop +131 -0
- data/lib/coop_al/adventure.rb +52 -0
- data/lib/coop_al/adventure_generator.rb +35 -0
- data/lib/coop_al/bestiary.rb +51 -0
- data/lib/coop_al/bestiary_generator.rb +3 -0
- data/lib/coop_al/bestiary_populator.rb +14 -0
- data/lib/coop_al/chapter.rb +51 -0
- data/lib/coop_al/chapter_generator.rb +32 -0
- data/lib/coop_al/encounter.rb +148 -0
- data/lib/coop_al/encounter_generator.rb +67 -0
- data/lib/coop_al/exception.rb +7 -0
- data/lib/coop_al/item.rb +30 -0
- data/lib/coop_al/library.rb +54 -0
- data/lib/coop_al/library_generator.rb +4 -0
- data/lib/coop_al/loot.rb +49 -0
- data/lib/coop_al/loot_generator.rb +42 -0
- data/lib/coop_al/monster.rb +19 -0
- data/lib/coop_al/monster_definition.rb +18 -0
- data/lib/coop_al/path.rb +65 -0
- data/lib/coop_al/path_follower.rb +34 -0
- data/lib/coop_al/random_encounter.rb +30 -0
- data/lib/coop_al/random_encounter_generator.rb +37 -0
- data/lib/coop_al/session.rb +55 -0
- data/lib/coop_al/session_date_generator.rb +43 -0
- data/lib/coop_al/session_encounter.rb +76 -0
- data/lib/coop_al/session_log.rb +51 -0
- data/lib/coop_al/state.rb +85 -0
- data/lib/coop_al/state_reporter.rb +72 -0
- data/lib/coop_al/trace.rb +29 -0
- data/lib/coop_al/treasure.rb +21 -0
- data/lib/coop_al/treasure_tables.rb +28 -0
- data/lib/coop_al/value.rb +167 -0
- data/lib/coop_al/version.rb +3 -0
- data/lib/coop_al/xp.rb +86 -0
- data/lib/coop_al.rb +34 -0
- data/res/srd.rb +400 -0
- metadata +188 -0
@@ -0,0 +1,148 @@
|
|
1
|
+
module CoopAl
|
2
|
+
##
|
3
|
+
# Encounter
|
4
|
+
#
|
5
|
+
class Encounter
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
def initialize(name, parent)
|
9
|
+
@name = name
|
10
|
+
@parent = parent
|
11
|
+
@monsters = []
|
12
|
+
@xp = 0
|
13
|
+
@loot = Loot.empty
|
14
|
+
@sub_encounters = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def adventure_name
|
18
|
+
@parent.adventure_name
|
19
|
+
end
|
20
|
+
|
21
|
+
def full_name
|
22
|
+
if @parent.nil?
|
23
|
+
@name
|
24
|
+
else
|
25
|
+
@parent.full_name + ' - ' + @name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_sub_encounter(encounter)
|
30
|
+
@sub_encounters << encounter
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_monster(monster)
|
34
|
+
@monsters << monster
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_treasure(treasure)
|
38
|
+
@loot.add_treasure(treasure)
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_item(item)
|
42
|
+
@loot.add_item(item)
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_xp(amount)
|
46
|
+
@xp += amount
|
47
|
+
end
|
48
|
+
|
49
|
+
def run(state, log)
|
50
|
+
run_sub_encounters(state, log)
|
51
|
+
trace unless empty?
|
52
|
+
log.record_encounter(adventure_name, full_name, monster_names, total_xp, total_loot)
|
53
|
+
state.add_xp(total_xp)
|
54
|
+
state.add_loot(total_loot)
|
55
|
+
end
|
56
|
+
|
57
|
+
def empty?
|
58
|
+
@monsters.empty? && @xp.zero? && @loot.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def trace
|
64
|
+
Trace.instance.info("Encounter: #{full_name}")
|
65
|
+
Trace.instance.info("Fighting: #{all_monster_names}") unless @monsters.empty?
|
66
|
+
trace_xp
|
67
|
+
trace_loot
|
68
|
+
end
|
69
|
+
|
70
|
+
def trace_xp
|
71
|
+
Trace.instance.info("Monster XP: #{monster_xp}") unless @monsters.empty?
|
72
|
+
Trace.instance.info("Encounter XP: #{encounter_xp}") unless @xp.zero?
|
73
|
+
end
|
74
|
+
|
75
|
+
def trace_loot
|
76
|
+
trace_treasure
|
77
|
+
trace_items
|
78
|
+
end
|
79
|
+
|
80
|
+
def trace_treasure
|
81
|
+
Trace.instance.info("Encounter Treasure: #{encounter_treasure}") unless encounter_loot.treasures.empty?
|
82
|
+
Trace.instance.info("Monster Treasure: #{monster_treasure}") unless monster_loot.treasures.empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
def trace_items
|
86
|
+
Trace.instance.info("Encounter Items: #{encounter_items}") unless encounter_loot.items.empty?
|
87
|
+
Trace.instance.info("Monster Items: #{monster_items}") unless monster_loot.items.empty?
|
88
|
+
end
|
89
|
+
|
90
|
+
def all_monster_names
|
91
|
+
@monsters.map(&:id).join(', ')
|
92
|
+
end
|
93
|
+
|
94
|
+
def total_xp
|
95
|
+
encounter_xp + monster_xp
|
96
|
+
end
|
97
|
+
|
98
|
+
def encounter_xp
|
99
|
+
@xp
|
100
|
+
end
|
101
|
+
|
102
|
+
def monster_xp
|
103
|
+
@monsters.inject(0) do |total, monster|
|
104
|
+
total + monster.xp
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def total_loot
|
109
|
+
encounter_loot + monster_loot
|
110
|
+
end
|
111
|
+
|
112
|
+
def encounter_loot
|
113
|
+
@loot
|
114
|
+
end
|
115
|
+
|
116
|
+
def encounter_treasure
|
117
|
+
encounter_loot.treasures.map(&:to_s).join(', ')
|
118
|
+
end
|
119
|
+
|
120
|
+
def encounter_items
|
121
|
+
encounter_loot.items.map(&:to_s).join(', ')
|
122
|
+
end
|
123
|
+
|
124
|
+
def monster_loot
|
125
|
+
@monsters.inject(Loot.empty) do |loot, monster|
|
126
|
+
loot + monster.loot
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def monster_items
|
131
|
+
monster_loot.items.map(&:to_s).join(', ')
|
132
|
+
end
|
133
|
+
|
134
|
+
def monster_treasure
|
135
|
+
monster_loot.treasures.map(&:to_s).join(', ')
|
136
|
+
end
|
137
|
+
|
138
|
+
def monster_names
|
139
|
+
@monsters.map(&:to_s)
|
140
|
+
end
|
141
|
+
|
142
|
+
def run_sub_encounters(state, log)
|
143
|
+
@sub_encounters.each do |encounter|
|
144
|
+
encounter.run(state, log)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module CoopAl
|
2
|
+
##
|
3
|
+
# EncounterGenerator
|
4
|
+
#
|
5
|
+
class EncounterGenerator
|
6
|
+
def initialize(name, parent, bestiary)
|
7
|
+
@bestiary = bestiary
|
8
|
+
@encounter = Encounter.new(name, parent)
|
9
|
+
end
|
10
|
+
|
11
|
+
def generate_encounter(&blk)
|
12
|
+
instance_eval(&blk)
|
13
|
+
@encounter
|
14
|
+
end
|
15
|
+
|
16
|
+
def monsters(count, id, treasure = :default)
|
17
|
+
expand_count(count).times do
|
18
|
+
@encounter.add_monster(@bestiary.create(id, treasure, @encounter))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def monster(id, treasure = :default)
|
23
|
+
monsters(1, id, treasure)
|
24
|
+
end
|
25
|
+
|
26
|
+
def treasure(value, description = nil)
|
27
|
+
@encounter.add_treasure(Treasure.new(value, description))
|
28
|
+
end
|
29
|
+
|
30
|
+
def items(count, description)
|
31
|
+
@encounter.add_item(Item.new(expand_count(count), description, @encounter))
|
32
|
+
end
|
33
|
+
|
34
|
+
def item(description)
|
35
|
+
items(1, description)
|
36
|
+
end
|
37
|
+
|
38
|
+
def encounter(name, &blk)
|
39
|
+
generator = EncounterGenerator.new(name, @encounter, @bestiary)
|
40
|
+
@encounter.add_sub_encounter(generator.generate_encounter(&blk))
|
41
|
+
end
|
42
|
+
|
43
|
+
def random(name, &blk)
|
44
|
+
generator = RandomEncounterGenerator.new(name, @encounter, @bestiary)
|
45
|
+
@encounter.add_sub_encounter(generator.generate_encounter(&blk))
|
46
|
+
end
|
47
|
+
|
48
|
+
def xp(amount)
|
49
|
+
@encounter.add_xp(amount)
|
50
|
+
end
|
51
|
+
|
52
|
+
def npc(cr)
|
53
|
+
xp(XpRewardTable.new[cr])
|
54
|
+
end
|
55
|
+
|
56
|
+
def npcs(count, cr)
|
57
|
+
xp(count * XpRewardTable.new[cr])
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def expand_count(count)
|
63
|
+
return roll_dice(count) if count.is_a?(String)
|
64
|
+
count
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/coop_al/item.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module CoopAl
|
2
|
+
##
|
3
|
+
# Item
|
4
|
+
#
|
5
|
+
class Item
|
6
|
+
attr_reader :description
|
7
|
+
|
8
|
+
def initialize(count, description, encounter)
|
9
|
+
@count = count
|
10
|
+
@description = description
|
11
|
+
@encounter = encounter
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
if @count == 1
|
16
|
+
@description
|
17
|
+
else
|
18
|
+
"#{@description} (#{@count})"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def description_with_origin
|
23
|
+
"#{self} (from #{origin})"
|
24
|
+
end
|
25
|
+
|
26
|
+
def origin
|
27
|
+
@encounter.full_name
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module CoopAl
|
4
|
+
##
|
5
|
+
# Library
|
6
|
+
#
|
7
|
+
class Library
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@adventures = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def empty?
|
15
|
+
@adventures.empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_adventure(adventure)
|
19
|
+
raise Exception, 'Duplicate adventure' if @adventures.key?(adventure.name)
|
20
|
+
@adventures[adventure.name] = adventure
|
21
|
+
end
|
22
|
+
|
23
|
+
def adventure?(name)
|
24
|
+
@adventures.key?(name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def adventure(name)
|
28
|
+
@adventures[name]
|
29
|
+
end
|
30
|
+
|
31
|
+
def path?(path)
|
32
|
+
raise Exception, "Cannot resolve relative path (#{path})" if path.relative?
|
33
|
+
return false unless @adventures.key?(path.adventure)
|
34
|
+
@adventures[path.adventure].chapter?(path.chapter)
|
35
|
+
end
|
36
|
+
|
37
|
+
def resolve(path)
|
38
|
+
raise Exception, "Adventure (#{path.adventure}) not found" unless @adventures.key?(path.adventure)
|
39
|
+
@adventures[path.adventure].chapter(path.chapter)
|
40
|
+
end
|
41
|
+
|
42
|
+
def all_entries
|
43
|
+
@adventures.values.inject([]) { |a, e| a + e.all_entries }
|
44
|
+
end
|
45
|
+
|
46
|
+
def available_paths_from(path)
|
47
|
+
return all_entries if path.root?
|
48
|
+
current_chapter = resolve(path)
|
49
|
+
paths = current_chapter.links
|
50
|
+
paths << Path.root if current_chapter.links_to_downtime?
|
51
|
+
paths
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/coop_al/loot.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module CoopAl
|
2
|
+
##
|
3
|
+
# Loot
|
4
|
+
#
|
5
|
+
class Loot
|
6
|
+
attr_reader :treasures, :items
|
7
|
+
|
8
|
+
def initialize(treasures, items)
|
9
|
+
@treasures = treasures
|
10
|
+
@items = items
|
11
|
+
end
|
12
|
+
|
13
|
+
def empty?
|
14
|
+
return false unless @treasures.empty?
|
15
|
+
return false unless @items.empty?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_treasure(treasure)
|
20
|
+
@treasures << treasure
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_item(item)
|
24
|
+
@items << item
|
25
|
+
end
|
26
|
+
|
27
|
+
def +(other)
|
28
|
+
@treasures += other.treasures
|
29
|
+
@items += other.items
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def treasure_value
|
34
|
+
@treasures.inject(Value.new) { |a, e| a + e.value }
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.empty
|
38
|
+
Loot.new([], [])
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.from_treasure(treasure)
|
42
|
+
Loot.new([treasure], [])
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.from_item(item)
|
46
|
+
Loot.new([], [item])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module CoopAl
|
2
|
+
##
|
3
|
+
# LootGenerator
|
4
|
+
#
|
5
|
+
class LootGenerator
|
6
|
+
def initialize
|
7
|
+
end
|
8
|
+
|
9
|
+
def generate(cr, treasure)
|
10
|
+
return Loot.empty if treasure == :no_treasure
|
11
|
+
return generate_individual(cr) if treasure == :individual
|
12
|
+
generate_hoard(cr)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def generate_individual(cr)
|
18
|
+
table_name = individual_treasure_table_by_cr(cr)
|
19
|
+
treasure = Treasure.new(roll_on(table_name))
|
20
|
+
Loot.from_treasure(treasure)
|
21
|
+
end
|
22
|
+
|
23
|
+
def individual_treasure_table_by_cr(cr)
|
24
|
+
value = cr_value(cr)
|
25
|
+
return :individual_treasure_cr_0_4 if value <= 4
|
26
|
+
return :individual_treasure_cr_5_10 if value <= 10
|
27
|
+
return :individual_treasure_cr_11_16 if value <= 16
|
28
|
+
:individual_treasure_cr_17_
|
29
|
+
end
|
30
|
+
|
31
|
+
def cr_value(cr)
|
32
|
+
return 0.125 if cr == :cr1_8
|
33
|
+
return 0.25 if cr == :cr1_4
|
34
|
+
return 0.5 if cr == :cr1_2
|
35
|
+
cr[2..-1].to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
def generate_hoard(_cr)
|
39
|
+
raise 'Hoard loot not implemented'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module CoopAl
|
2
|
+
##
|
3
|
+
# Monster
|
4
|
+
#
|
5
|
+
class Monster
|
6
|
+
attr_reader :id, :xp, :loot, :encounter
|
7
|
+
|
8
|
+
def initialize(id, xp, loot, encounter)
|
9
|
+
@id = id
|
10
|
+
@xp = xp
|
11
|
+
@loot = loot
|
12
|
+
@encounter = encounter
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
@id.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module CoopAl
|
2
|
+
##
|
3
|
+
# MonsterDefinition
|
4
|
+
#
|
5
|
+
class MonsterDefinition
|
6
|
+
attr_reader :id, :cr, :treasure
|
7
|
+
|
8
|
+
def initialize(id, cr, treasure)
|
9
|
+
@id = id
|
10
|
+
@cr = cr
|
11
|
+
@treasure = treasure
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
@id == other.id && @cr == other.cr && @treasure == other.treasure
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/coop_al/path.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
module CoopAl
|
2
|
+
##
|
3
|
+
# Path
|
4
|
+
#
|
5
|
+
class Path
|
6
|
+
attr_reader :adventure, :chapter
|
7
|
+
|
8
|
+
def initialize(adventure, chapter)
|
9
|
+
@adventure = adventure
|
10
|
+
@chapter = chapter
|
11
|
+
end
|
12
|
+
|
13
|
+
def root?
|
14
|
+
@adventure.nil? && @chapter.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def relative?
|
18
|
+
@adventure.nil? && !@chapter.nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
def absolute?
|
22
|
+
!relative?
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.absolute(adventure, chapter)
|
26
|
+
Path.new(adventure, chapter)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.relative(chapter)
|
30
|
+
Path.new(nil, chapter)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.root
|
34
|
+
Path.new(nil, nil)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.parse(path)
|
38
|
+
tokens = path.split('/').map(&:to_sym)
|
39
|
+
raise "Invalid path #{path}" if tokens.count > 2
|
40
|
+
return absolute(tokens[0], tokens[1]) if tokens.count == 2
|
41
|
+
return root if tokens[0] == :downtime
|
42
|
+
relative(tokens[0])
|
43
|
+
end
|
44
|
+
|
45
|
+
def ==(other)
|
46
|
+
to_s == other.to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
def +(other)
|
50
|
+
local_path = other.is_a?(Path) ? other : Path.parse(other)
|
51
|
+
return local_path if local_path.absolute?
|
52
|
+
raise 'Cannot add two relative paths' if relative?
|
53
|
+
Path.absolute(@adventure, other.chapter)
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
adventure_s + @chapter.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
def adventure_s
|
61
|
+
return @adventure.to_s + '/' if absolute?
|
62
|
+
''
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module CoopAl
|
2
|
+
##
|
3
|
+
# PathFollower
|
4
|
+
#
|
5
|
+
class PathFollower
|
6
|
+
attr_reader :state
|
7
|
+
|
8
|
+
def initialize(library, starting_state)
|
9
|
+
@library = library
|
10
|
+
@state = starting_state
|
11
|
+
end
|
12
|
+
|
13
|
+
def follow(paths, log)
|
14
|
+
paths.each do |path|
|
15
|
+
follow_path(path, log)
|
16
|
+
end
|
17
|
+
@state
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def follow_path(path, log)
|
23
|
+
if path.root?
|
24
|
+
log.record_downtime(@library.resolve(@state.current_path).adventure_name)
|
25
|
+
@state.apply_path(path)
|
26
|
+
else
|
27
|
+
raise Exception, "#{path} not a valid next path" unless @library.path?(@state.current_path + path)
|
28
|
+
raise Exception, "Cannot repeat path (#{path})" if @state.history_includes?(@state.current_path + path)
|
29
|
+
@state.apply_path(path)
|
30
|
+
@library.resolve(@state.current_path).follow(@state, log)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module CoopAl
|
2
|
+
##
|
3
|
+
# RandomEncounter
|
4
|
+
#
|
5
|
+
class RandomEncounter
|
6
|
+
def initialize(name, parent)
|
7
|
+
@name = name
|
8
|
+
@parent = parent
|
9
|
+
@entries = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_entry(entry)
|
13
|
+
@entries << entry
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_entry(roll, entry)
|
17
|
+
@entries[roll - 1] = entry
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_range(range, entry)
|
21
|
+
range.each do |i|
|
22
|
+
set_entry(i, entry)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def run(state, log)
|
27
|
+
@entries[roll_dice("d#{@entries.count}") - 1].run(state, log)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module CoopAl
|
2
|
+
##
|
3
|
+
# RandomEncounterGenerator
|
4
|
+
#
|
5
|
+
class RandomEncounterGenerator
|
6
|
+
def initialize(name, parent, bestiary)
|
7
|
+
@name = name
|
8
|
+
@parent = parent
|
9
|
+
@bestiary = bestiary
|
10
|
+
@encounter = RandomEncounter.new(name, parent)
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate_encounter(&blk)
|
14
|
+
instance_eval(&blk)
|
15
|
+
@encounter
|
16
|
+
end
|
17
|
+
|
18
|
+
def fixed(*args, &blk)
|
19
|
+
generator = EncounterGenerator.new(@name, @parent, @bestiary)
|
20
|
+
entry = generator.generate_encounter(&blk)
|
21
|
+
if args.empty?
|
22
|
+
@encounter.add_entry(entry)
|
23
|
+
else
|
24
|
+
roll = args.shift
|
25
|
+
if roll.is_a?(Integer)
|
26
|
+
@encounter.set_entry(roll, entry)
|
27
|
+
elsif roll.is_a?(Range)
|
28
|
+
@encounter.set_range(roll, entry)
|
29
|
+
else
|
30
|
+
raise "Inappropriate roll descriptor (#{roll})"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
alias f fixed
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module CoopAl
|
2
|
+
##
|
3
|
+
# Session
|
4
|
+
#
|
5
|
+
class Session
|
6
|
+
def initialize(number, date_generator, adventure_name, dm_name, starting_xp, starting_treasure, encounter_count)
|
7
|
+
@number = number
|
8
|
+
@date_generator = date_generator
|
9
|
+
@adventure_name = adventure_name
|
10
|
+
@dm_name = dm_name
|
11
|
+
@starting_xp = starting_xp
|
12
|
+
@starting_treasure = starting_treasure
|
13
|
+
@encounter_count = encounter_count
|
14
|
+
@encounters = []
|
15
|
+
|
16
|
+
@date_generator.add_session
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_encounter(encounter)
|
20
|
+
@encounters << encounter
|
21
|
+
end
|
22
|
+
|
23
|
+
def done?
|
24
|
+
@encounters.count { |e| e.counts? } == @encounter_count
|
25
|
+
end
|
26
|
+
|
27
|
+
def dump(s)
|
28
|
+
s.puts "Adventure: #{@adventure_name}"
|
29
|
+
s.puts "Session ##{@number}: #{@date_generator.session(@number)}"
|
30
|
+
s.puts "DM: #{@dm_name}"
|
31
|
+
s.puts "Starting XP: #{@starting_xp} (level #{level(@starting_xp)})"
|
32
|
+
s.puts "XP Earned: #{xp_earned}"
|
33
|
+
s.puts "XP Total: #{@starting_xp + xp_earned} (level #{level(@starting_xp + xp_earned)})"
|
34
|
+
s.puts "Starting Treasure: #{@starting_treasure}"
|
35
|
+
s.puts "Treasure +/-: #{treasure_earned}"
|
36
|
+
s.puts "Treasure Total: #{@starting_treasure + treasure_earned}"
|
37
|
+
@encounters.each { |e| e.dump(s) if e.counts? }
|
38
|
+
s.puts
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def xp_earned
|
44
|
+
@encounters.inject(0) { |a, e| a + e.xp }
|
45
|
+
end
|
46
|
+
|
47
|
+
def treasure_earned
|
48
|
+
@encounters.inject(Value.new) { |a, e| a + e.treasure }
|
49
|
+
end
|
50
|
+
|
51
|
+
def level(xp)
|
52
|
+
XpRequirementTable.new.level_from_xp(xp)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|