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.
@@ -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.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