dd-next-encounters 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|