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