dd-next-encounters 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +10 -0
- data/lib/data/monsters_manual_content.rb +8996 -0
- data/lib/data/xp_difficulty_table.rb +23 -0
- data/lib/encounters/encounter.rb +42 -0
- data/lib/encounters/lair.rb +86 -0
- data/lib/monsters/monster.rb +36 -0
- data/lib/monsters/monsters_group.rb +30 -0
- data/lib/monsters/monsters_manual.rb +107 -0
- metadata +9 -1
@@ -0,0 +1,23 @@
|
|
1
|
+
module XpDifficultyTable
|
2
|
+
XP_DIFFICULTY_TABLE =
|
3
|
+
{1=>{:easy=>25, :medium=>50, :hard=>75, :deadly=>100},
|
4
|
+
2=>{:easy=>50, :medium=>100, :hard=>150, :deadly=>200},
|
5
|
+
3=>{:easy=>75, :medium=>150, :hard=>225, :deadly=>400},
|
6
|
+
4=>{:easy=>125, :medium=>250, :hard=>375, :deadly=>500},
|
7
|
+
5=>{:easy=>250, :medium=>500, :hard=>750, :deadly=>1100},
|
8
|
+
6=>{:easy=>300, :medium=>600, :hard=>900, :deadly=>1400},
|
9
|
+
7=>{:easy=>350, :medium=>750, :hard=>1100, :deadly=>1700},
|
10
|
+
8=>{:easy=>450, :medium=>900, :hard=>1400, :deadly=>2100},
|
11
|
+
9=>{:easy=>550, :medium=>1100, :hard=>1600, :deadly=>2400},
|
12
|
+
10=>{:easy=>600, :medium=>1200, :hard=>1900, :deadly=>2800},
|
13
|
+
11=>{:easy=>800, :medium=>1600, :hard=>2400, :deadly=>3600},
|
14
|
+
12=>{:easy=>1000, :medium=>2000, :hard=>3000, :deadly=>4500},
|
15
|
+
13=>{:easy=>1100, :medium=>2200, :hard=>3400, :deadly=>5100},
|
16
|
+
14=>{:easy=>1250, :medium=>2500, :hard=>3800, :deadly=>5700},
|
17
|
+
15=>{:easy=>1400, :medium=>2800, :hard=>4300, :deadly=>6400},
|
18
|
+
16=>{:easy=>1600, :medium=>3200, :hard=>4800, :deadly=>7200},
|
19
|
+
17=>{:easy=>2000, :medium=>3900, :hard=>5900, :deadly=>8800},
|
20
|
+
18=>{:easy=>2100, :medium=>4200, :hard=>6300, :deadly=>9500},
|
21
|
+
19=>{:easy=>2400, :medium=>4900, :hard=>7300, :deadly=>10900},
|
22
|
+
20=>{:easy=>2800, :medium=>5700, :hard=>8500, :deadly=>12700}}
|
23
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class Encounter
|
2
|
+
|
3
|
+
def initialize( party_xp_level )
|
4
|
+
@monsters = []
|
5
|
+
@party_xp_level = party_xp_level
|
6
|
+
end
|
7
|
+
|
8
|
+
# Return true or false. Monster added or not
|
9
|
+
def add_monster_if_possible( monster )
|
10
|
+
if can_add_monster?( monster )
|
11
|
+
@monsters << monster
|
12
|
+
return true
|
13
|
+
end
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def can_add_monster?( monster )
|
18
|
+
encounter_value( @monsters + [ monster ] ) <= @party_xp_level
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
@monsters.group_by {|i| i.key}.map{ |_, v| "#{v.count} #{v.first.name}"}.join( ', ' )
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def encounter_value( encounter )
|
28
|
+
encounter.map{ |e| e.xp_value }.reduce(&:+) * get_encounter_multiplier( encounter )
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_encounter_multiplier( encounter )
|
32
|
+
count = encounter.count
|
33
|
+
mul = 1
|
34
|
+
mul = 1.5 if count >= 2
|
35
|
+
mul = 2 if count >= 3
|
36
|
+
mul = 2.5 if count >= 7
|
37
|
+
mul = 3 if count >= 11
|
38
|
+
mul = 4 if count >= 15
|
39
|
+
mul
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require_relative 'encounter'
|
2
|
+
require_relative '../data/xp_difficulty_table'
|
3
|
+
|
4
|
+
class Lair
|
5
|
+
|
6
|
+
include XpDifficultyTable
|
7
|
+
|
8
|
+
AVAILABLE_ENCOUNTER_LEVEL=[ :easy, :medium, :hard, :deadly ]
|
9
|
+
|
10
|
+
def initialize( *encounters_types )
|
11
|
+
@monster_manual = MonstersManual.new
|
12
|
+
@monsters = nil
|
13
|
+
@xp_difficulty_table = XP_DIFFICULTY_TABLE
|
14
|
+
@encounters_types = encounters_types
|
15
|
+
@encounters={}
|
16
|
+
end
|
17
|
+
|
18
|
+
def read_manuals
|
19
|
+
read_monster_manual
|
20
|
+
end
|
21
|
+
|
22
|
+
def groups
|
23
|
+
@monster_manual.validate_loaded
|
24
|
+
@monster_manual.groups.keys
|
25
|
+
end
|
26
|
+
|
27
|
+
# encounter_level : :easy, :medium, :hard, :deadly
|
28
|
+
def get_encounter( encounter_level, *hero_level )
|
29
|
+
@monster_manual.validate_loaded
|
30
|
+
|
31
|
+
raise "Bad encounter level : #{encounter_level.inspect}. Available encounter level : #{AVAILABLE_ENCOUNTER_LEVEL.inspect}" unless AVAILABLE_ENCOUNTER_LEVEL.include?( encounter_level )
|
32
|
+
party_xp_level = hero_level.map{ |hl| @xp_difficulty_table[hl][encounter_level] }.reduce(&:+)
|
33
|
+
|
34
|
+
encounter = Encounter.new( party_xp_level )
|
35
|
+
|
36
|
+
# Choose a random encounter type
|
37
|
+
encounter_type = @encounters_types.sample
|
38
|
+
bosses = @encounters[encounter_type][:bosses]
|
39
|
+
troops = @encounters[encounter_type][:troops]
|
40
|
+
|
41
|
+
# Choose a random boss
|
42
|
+
boss = bosses.sample if !bosses.empty? && rand( 1 .. 2 ) == 1
|
43
|
+
encounter.add_monster_if_possible( boss ) if boss
|
44
|
+
|
45
|
+
# Choose a random monster
|
46
|
+
monster = get_corresponding_monsters( troops, party_xp_level ).sample
|
47
|
+
|
48
|
+
loop do
|
49
|
+
break unless encounter.add_monster_if_possible( monster )
|
50
|
+
end
|
51
|
+
|
52
|
+
encounter
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def get_corresponding_monsters( troops, party_xp_level)
|
58
|
+
troops.map{ |m| m if m.xp_value < party_xp_level }.compact
|
59
|
+
end
|
60
|
+
|
61
|
+
def read_monster_manual
|
62
|
+
@monster_manual.load
|
63
|
+
validate_encounters_types
|
64
|
+
|
65
|
+
@monsters = @monster_manual.select( sources: [ 'Basic Rules', 'Monster Manual' ] )
|
66
|
+
|
67
|
+
@encounters_types.each do |encounter_type|
|
68
|
+
@encounters[encounter_type] ||= { troops: [], bosses: [] }
|
69
|
+
@encounters[encounter_type][:troops] = @monster_manual.groups[encounter_type]&.troops
|
70
|
+
@encounters[encounter_type][:bosses] = @monster_manual.groups[encounter_type]&.bosses
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def validate_encounters_types
|
77
|
+
@encounters_types.each do |encounter_type|
|
78
|
+
unless @monster_manual.groups.include?( encounter_type )
|
79
|
+
raise "Bad lair type : #{encounter_type.inspect}" + ". Available lairs types : #{@monster_manual.groups.keys}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
@encounters_types = @monster_manual.groups.keys if @encounters_types.empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class Object
|
4
|
+
private
|
5
|
+
|
6
|
+
def set_instance_variables(binding, *variables)
|
7
|
+
variables.each do |var|
|
8
|
+
instance_variable_set("@#{var}", eval(var.to_s, binding))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
class Monster
|
15
|
+
|
16
|
+
attr_reader :challenge, :name, :type, :source, :key, :groups
|
17
|
+
attr_accessor :xp_value, :boss
|
18
|
+
|
19
|
+
def initialize( challenge, name, type, source )
|
20
|
+
set_instance_variables(binding, *local_variables)
|
21
|
+
@key = @name.gsub( /[ -]/, '_' ).gsub( 'é', 'e' ).delete( "()'’“”" ).downcase.to_sym
|
22
|
+
@groups = []
|
23
|
+
@accepted_bosses = []
|
24
|
+
@boss = false
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_groups( groups )
|
28
|
+
@groups ||= []
|
29
|
+
@groups += groups
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_hash
|
33
|
+
{ key: @key, challenge: @challenge, name: @name, type: @type, source: @source, xp_value: @xp_value, boss: @boss, groups: @groups }
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class MonstersGroup
|
2
|
+
|
3
|
+
attr_reader :troops, :bosses
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@troops = []
|
7
|
+
@bosses = []
|
8
|
+
# This mean that this group is inferior to the listed groups. They wont accept
|
9
|
+
@groups_inferiority = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_monster( monster )
|
13
|
+
@troops << monster unless monster.boss
|
14
|
+
@bosses << monster if monster.boss
|
15
|
+
end
|
16
|
+
|
17
|
+
# def to_s
|
18
|
+
# { troops: @troops.map{ |m| m.key }, bosses: @bosses.map{ |m| m.key } }
|
19
|
+
# end
|
20
|
+
|
21
|
+
def to_hash
|
22
|
+
{ troops: @troops.map{ |m| m.key }, bosses: @bosses.map{ |m| m.key } }
|
23
|
+
end
|
24
|
+
|
25
|
+
def from_hash( monsters, hash )
|
26
|
+
@troops = hash[:troops].map{ |tk| monsters[tk] }
|
27
|
+
@bosses = hash[:bosses].map{ |tk| monsters[tk] }
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'pp'
|
3
|
+
require_relative 'monster'
|
4
|
+
require_relative 'monsters_group'
|
5
|
+
require_relative '../../lib/data/monsters_manual_content'
|
6
|
+
|
7
|
+
class MonstersManual
|
8
|
+
|
9
|
+
attr_reader :monsters, :groups
|
10
|
+
|
11
|
+
include MonstersManualContent
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@monsters = {}
|
15
|
+
@sources = {}
|
16
|
+
@challenges = {}
|
17
|
+
@types = {}
|
18
|
+
@groups = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def load
|
22
|
+
@monsters = {}
|
23
|
+
MONSTERS_MANUAL_CONTENT[:monsters].each do |m|
|
24
|
+
monster = Monster.new( m[:challenge], m[:name], m[:type], m[:source] )
|
25
|
+
monster.xp_value = m[:xp_value]
|
26
|
+
monster.boss = m[:boss]
|
27
|
+
monster.add_groups( m[:groups] )
|
28
|
+
@monsters[ monster.key ] = monster
|
29
|
+
end
|
30
|
+
|
31
|
+
@sources = MONSTERS_MANUAL_CONTENT[:sources]
|
32
|
+
@challenges = MONSTERS_MANUAL_CONTENT[:challenges]
|
33
|
+
@types = MONSTERS_MANUAL_CONTENT[:types]
|
34
|
+
|
35
|
+
MONSTERS_MANUAL_CONTENT[:groups].each do |k, group_hash|
|
36
|
+
group = MonstersGroup.new
|
37
|
+
group.from_hash( @monsters, group_hash )
|
38
|
+
@groups[k] = group
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def save( filename )
|
43
|
+
monster_manual = {
|
44
|
+
monsters: @monsters.map{ |_, m| m.to_hash },
|
45
|
+
sources: @sources,
|
46
|
+
challenges: @challenges,
|
47
|
+
types: @types,
|
48
|
+
groups: Hash[ @groups.map{ |k, g| [ k, g.to_hash ] } ]
|
49
|
+
}
|
50
|
+
File.open( filename, 'w' ) do |f|
|
51
|
+
f.puts 'module MonstersManualContent'
|
52
|
+
f.puts "\t MONSTERS_MANUAL_CONTENT = "
|
53
|
+
PP.pp(monster_manual,f)
|
54
|
+
f.puts 'end'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def sources
|
59
|
+
@sources.keys.sort
|
60
|
+
end
|
61
|
+
|
62
|
+
def challenges
|
63
|
+
@challenges.keys.sort
|
64
|
+
end
|
65
|
+
|
66
|
+
def types
|
67
|
+
@types.keys.sort
|
68
|
+
end
|
69
|
+
|
70
|
+
def select( sources: :all, types: :all, min_challenge: :none, max_challenge: :none )
|
71
|
+
validate_loaded
|
72
|
+
|
73
|
+
sources_ids = ( sources == :all ? @sources.values.flatten : sources.map{ |s| @sources[s] }.flatten )
|
74
|
+
types_ids = ( types == :all ? @types.values.flatten : types.map{ |t| @types[t] }.flatten )
|
75
|
+
|
76
|
+
challenges = @challenges.keys
|
77
|
+
challenges.reject!{ |c| c > max_challenge } if max_challenge != :none
|
78
|
+
challenges.reject!{ |c| c < min_challenge } if min_challenge != :none
|
79
|
+
challenges_ids = challenges.map{ |c| @challenges[ c ] }.flatten
|
80
|
+
|
81
|
+
monsters_ids = sources_ids & types_ids & challenges_ids
|
82
|
+
monsters_ids.map{ |m| @monsters[m] }
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_monster( monster )
|
86
|
+
@monsters[monster.key]=monster
|
87
|
+
|
88
|
+
@sources[monster.source] ||= []
|
89
|
+
@sources[monster.source] << monster.key
|
90
|
+
|
91
|
+
@challenges[monster.challenge] ||= []
|
92
|
+
@challenges[monster.challenge] << monster.key
|
93
|
+
|
94
|
+
@types[monster.type] ||= []
|
95
|
+
@types[monster.type] << monster.key
|
96
|
+
|
97
|
+
monster.groups.each do |group|
|
98
|
+
@groups[group] ||= MonstersGroup.new
|
99
|
+
@groups[group].add_monster( monster )
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def validate_loaded
|
104
|
+
raise 'Monster manual not loadad' if @monsters.empty?
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dd-next-encounters
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cédric Zuger
|
@@ -30,7 +30,15 @@ executables: []
|
|
30
30
|
extensions: []
|
31
31
|
extra_rdoc_files: []
|
32
32
|
files:
|
33
|
+
- README.md
|
34
|
+
- lib/data/monsters_manual_content.rb
|
35
|
+
- lib/data/xp_difficulty_table.rb
|
33
36
|
- lib/dd-next-encounters.rb
|
37
|
+
- lib/encounters/encounter.rb
|
38
|
+
- lib/encounters/lair.rb
|
39
|
+
- lib/monsters/monster.rb
|
40
|
+
- lib/monsters/monsters_group.rb
|
41
|
+
- lib/monsters/monsters_manual.rb
|
34
42
|
homepage: https://github.com/czuger/dd-next-encounters
|
35
43
|
licenses:
|
36
44
|
- MIT
|