oakdex-battle 0.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,27 @@
1
+ require 'forwardable'
2
+
3
+ module Oakdex
4
+ class Battle
5
+ # Represents Pokemon Move with PP
6
+ class Move
7
+ extend Forwardable
8
+
9
+ attr_reader :max_pp
10
+ attr_accessor :pp
11
+
12
+ def_delegators :@move_type, :target, :priority, :accuracy,
13
+ :category, :power, :type, :stat_modifiers,
14
+ :in_battle_properties
15
+
16
+ def initialize(move_type, pp, max_pp)
17
+ @move_type = move_type
18
+ @pp = pp
19
+ @max_pp = max_pp
20
+ end
21
+
22
+ def name
23
+ @move_type.names['en']
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,148 @@
1
+ require 'forwardable'
2
+
3
+ module Oakdex
4
+ class Battle
5
+ # Represents one Action. One turn has many actions.
6
+ class MoveExecution
7
+ RECALL_PRIORITY = 6
8
+
9
+ extend Forwardable
10
+
11
+ def_delegators :@action, :battle, :turn, :pokemon,
12
+ :move, :trainer
13
+
14
+ attr_reader :action, :target
15
+
16
+ def initialize(action, target)
17
+ @action = action
18
+ @target = target
19
+ end
20
+
21
+ def hitting_probability
22
+ ((move.accuracy / 100.0) * (pokemon.accuracy / target.evasion)) * 1000
23
+ end
24
+
25
+ def hitting?
26
+ @hitting = rand(1..1000) <= hitting_probability ? 1 : 0
27
+ @hitting == 1
28
+ end
29
+
30
+ def execute
31
+ return if prevented_by_status_condition?
32
+ pokemon.change_pp_by(move.name, -1)
33
+ if hitting?
34
+ add_uses_move_log
35
+ execute_damage
36
+ execute_stat_modifiers
37
+ execute_status_conditions
38
+ execute_status_condition_callbacks
39
+ else
40
+ add_move_does_not_hit_log
41
+ end
42
+
43
+ battle.remove_fainted
44
+ end
45
+
46
+ private
47
+
48
+ def prevented_by_status_condition?
49
+ pokemon.status_conditions.reduce(false) do |res, condition|
50
+ res || condition.prevents_move?(self)
51
+ end
52
+ end
53
+
54
+ def execute_status_condition_callbacks
55
+ return if move.power.zero?
56
+ target.status_conditions.each do |c|
57
+ c.after_received_damage(self)
58
+ end
59
+ end
60
+
61
+ def execute_status_conditions
62
+ status_conditions.each do |condition|
63
+ execute_status_condition(condition)
64
+ end
65
+ end
66
+
67
+ def status_conditions
68
+ return [] if move.in_battle_properties.nil?
69
+ move.in_battle_properties['status_conditions'] || []
70
+ end
71
+
72
+ def execute_status_condition(condition)
73
+ return unless rand(1..100) <= condition['probability']
74
+ add_target_condition_added(condition['condition'])
75
+ target.add_status_condition(condition['condition'])
76
+ end
77
+
78
+ def execute_stat_modifiers
79
+ return if move.stat_modifiers.empty?
80
+ move.stat_modifiers.each do |stat_modifier|
81
+ modifier_target = stat_modifier['affects_user'] ? pokemon : target
82
+ stat = stat_modifier['stat']
83
+ stat = random_stat if stat == 'random'
84
+ if modifier_target.change_stat_by(stat.to_sym,
85
+ stat_modifier['change_by'])
86
+ add_changes_stat_log(modifier_target, stat,
87
+ stat_modifier['change_by'])
88
+ else
89
+ add_changes_no_stat_log(modifier_target, stat,
90
+ stat_modifier['change_by'])
91
+ end
92
+ end
93
+ end
94
+
95
+ def execute_damage
96
+ return if move.power.zero?
97
+ @damage = Damage.new(turn, self)
98
+ if @damage.damage > 0
99
+ add_received_damage_log
100
+ target.change_hp_by(-@damage.damage)
101
+ else
102
+ add_received_no_damage_log
103
+ end
104
+ end
105
+
106
+ def random_stat
107
+ (Pokemon::BATTLE_STATS + Pokemon::OTHER_STATS).sample
108
+ end
109
+
110
+ def add_log(*args)
111
+ battle.add_to_log(*args)
112
+ end
113
+
114
+ def add_changes_stat_log(target, stat, change_by)
115
+ add_log 'changes_stat', target.trainer.name, target.name,
116
+ stat, change_by
117
+ end
118
+
119
+ def add_changes_no_stat_log(target, stat, change_by)
120
+ add_log 'changes_no_stat', target.trainer.name, target.name,
121
+ stat, change_by
122
+ end
123
+
124
+ def add_target_condition_added(condition_name)
125
+ add_log 'target_condition_added', target.trainer.name, target.name,
126
+ condition_name
127
+ end
128
+
129
+ def add_uses_move_log
130
+ add_log 'uses_move', trainer.name, pokemon.name, move.name
131
+ end
132
+
133
+ def add_move_does_not_hit_log
134
+ add_log 'move_does_not_hit', trainer.name, pokemon.name, move.name
135
+ end
136
+
137
+ def add_received_damage_log
138
+ add_log 'received_damage', target.trainer.name, target.name,
139
+ move.name, @damage.damage
140
+ end
141
+
142
+ def add_received_no_damage_log
143
+ add_log 'received_no_damage', target.trainer.name, target.name,
144
+ move.name
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,168 @@
1
+ require 'forwardable'
2
+ require 'oakdex/battle/pokemon_stat'
3
+ require 'oakdex/battle/move'
4
+ require 'oakdex/battle/pokemon_factory'
5
+ require 'oakdex/battle/status_conditions'
6
+
7
+ module Oakdex
8
+ class Battle
9
+ # Represents detailed pokemon instance
10
+ class Pokemon
11
+ extend Forwardable
12
+
13
+ BATTLE_STATS = %i[hp atk def sp_atk sp_def speed]
14
+ OTHER_STATS = %i[accuracy evasion critical_hit]
15
+ STATUS_CONDITIONS = {
16
+ 'poison' => StatusConditions::Poison,
17
+ 'burn' => StatusConditions::Burn,
18
+ 'freeze' => StatusConditions::Freeze,
19
+ 'paralysis' => StatusConditions::Paralysis,
20
+ 'badly_poisoned' => StatusConditions::BadlyPoisoned,
21
+ 'sleep' => StatusConditions::Sleep
22
+ }
23
+
24
+ def_delegators :@species, :types
25
+
26
+ attr_accessor :trainer
27
+
28
+ def self.create(species_name, options = {})
29
+ species = Oakdex::Pokedex::Pokemon.find!(species_name)
30
+ Oakdex::Battle::PokemonFactory.create(species, options)
31
+ end
32
+
33
+ def initialize(species, attributes = {})
34
+ @species = species
35
+ @attributes = attributes
36
+ @attributes[:status_conditions] ||= []
37
+ reset_stats
38
+ end
39
+
40
+ def name
41
+ @species.names['en']
42
+ end
43
+
44
+ def moves
45
+ @attributes[:moves]
46
+ end
47
+
48
+ def current_hp
49
+ @attributes[:hp]
50
+ end
51
+
52
+ def status_conditions
53
+ @attributes[:status_conditions]
54
+ end
55
+
56
+ def moves_with_pp
57
+ moves.select { |m| m.pp > 0 }
58
+ end
59
+
60
+ def change_hp_by(hp_change)
61
+ @attributes[:hp] = if hp_change < 0
62
+ [@attributes[:hp] + hp_change, 0].max
63
+ else
64
+ [@attributes[:hp] + hp_change, hp].min
65
+ end
66
+ end
67
+
68
+ def change_pp_by(move_name, pp_change)
69
+ move = moves.find { |m| m.name == move_name }
70
+ return unless move
71
+ move.pp = if pp_change < 0
72
+ [move.pp + pp_change, 0].max
73
+ else
74
+ [move.pp + pp_change, move.max_pp].min
75
+ end
76
+ end
77
+
78
+ def change_stat_by(stat, change_by)
79
+ modifiers = stage_multipliers(stat)
80
+ stat_before = @stat_modifiers[stat]
81
+ min_value = modifiers.keys.first
82
+ max_value = modifiers.keys.last
83
+ @stat_modifiers[stat] = if change_by < 0
84
+ [stat_before + change_by, min_value].max
85
+ else
86
+ [stat_before + change_by, max_value].min
87
+ end
88
+ stat_before != @stat_modifiers[stat]
89
+ end
90
+
91
+ def add_status_condition(condition_name)
92
+ @attributes[:status_conditions] << status_condition(condition_name)
93
+ end
94
+
95
+ def remove_status_condition(condition)
96
+ @attributes[:status_conditions] = @attributes[:status_conditions]
97
+ .reject { |s| s == condition }
98
+ end
99
+
100
+ def reset_stats
101
+ @stat_modifiers = (BATTLE_STATS + OTHER_STATS - %i[hp]).map do |stat|
102
+ [stat, 0]
103
+ end.to_h
104
+ end
105
+
106
+ def level
107
+ PokemonStat.level_by_exp(@species.leveling_rate, @attributes[:exp])
108
+ end
109
+
110
+ def accuracy
111
+ stage(:accuracy)
112
+ end
113
+
114
+ def evasion
115
+ stage(:evasion)
116
+ end
117
+
118
+ def critical_hit_prob
119
+ stage(:critical_hit)
120
+ end
121
+
122
+ BATTLE_STATS.each do |stat|
123
+ define_method stat do
124
+ (initial_stat(stat) * stage(stat) *
125
+ status_condition_modifier(stat)).to_i
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ def status_condition_modifier(stat)
132
+ status_conditions.reduce(1.0) do |modifier, condition|
133
+ condition.stat_modifier(stat) * modifier
134
+ end
135
+ end
136
+
137
+ def status_condition(condition_name)
138
+ STATUS_CONDITIONS[condition_name].new(self)
139
+ end
140
+
141
+ def stage(stat)
142
+ multipliers = stage_multipliers(stat)
143
+ multipliers[@stat_modifiers[stat] || 0]
144
+ end
145
+
146
+ def initial_stat(stat)
147
+ PokemonStat.initial_stat(stat,
148
+ level: level,
149
+ nature: @attributes[:nature],
150
+ iv: @attributes[:iv],
151
+ ev: @attributes[:ev],
152
+ base_stats: @species.base_stats
153
+ )
154
+ end
155
+
156
+ def stage_multipliers(stat)
157
+ case stat
158
+ when :evasion, :accuracy
159
+ PokemonStat::STAGE_MULTIPLIERS_ACC_EVA
160
+ when :critical_hit
161
+ PokemonStat::STAGE_MULTIPLIERS_CRITICAL_HIT
162
+ else
163
+ PokemonStat::STAGE_MULTIPLIERS
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,118 @@
1
+ module Oakdex
2
+ class Battle
3
+ # Creates Pokemon instance and prefills attributes
4
+ class PokemonFactory
5
+ ATTRIBUTES = %i[exp gender ability nature hp iv ev moves]
6
+
7
+ class << self
8
+ def create(species, options = {})
9
+ factory = new(species, options)
10
+ attributes = Hash[ATTRIBUTES.map do |attr|
11
+ [attr, factory.send(attr)]
12
+ end]
13
+ Pokemon.new(species, attributes)
14
+ end
15
+ end
16
+
17
+ def initialize(species, options = {})
18
+ @species = species
19
+ @options = options
20
+ end
21
+
22
+ private
23
+
24
+ def moves
25
+ if @options[:moves]
26
+ @options[:moves].map do |move_data|
27
+ Move.new(
28
+ Oakdex::Pokedex::Move.find!(move_data[0]),
29
+ move_data[1],
30
+ move_data[2]
31
+ )
32
+ end
33
+ else
34
+ available_moves.sample(4).map do |move_name|
35
+ move_type = Oakdex::Pokedex::Move.find!(move_name)
36
+ Move.new(move_type, move_type.pp, move_type.pp)
37
+ end
38
+ end
39
+ end
40
+
41
+ def available_moves
42
+ @species.learnset.map do |m|
43
+ m['move'] if m['level'] && m['level'] <= level
44
+ end.compact
45
+ end
46
+
47
+ def ability
48
+ if @options['ability']
49
+ Oakdex::Pokedex::Ability.find!(@options['ability'])
50
+ else
51
+ Oakdex::Pokedex::Ability.find!(abilities.sample['name'])
52
+ end
53
+ end
54
+
55
+ def abilities
56
+ @species.abilities.select { |a| !a['hidden'] && !a['mega'] }
57
+ end
58
+
59
+ def exp
60
+ @options[:exp] || PokemonStat.exp_by_level(
61
+ @species.leveling_rate,
62
+ @options[:level]
63
+ )
64
+ end
65
+
66
+ def level
67
+ PokemonStat.level_by_exp(@species.leveling_rate, exp)
68
+ end
69
+
70
+ def hp
71
+ return @options[:hp] if @options[:hp]
72
+ PokemonStat.initial_stat(:hp,
73
+ level: level,
74
+ iv: iv,
75
+ ev: ev,
76
+ base_stats: @species.base_stats,
77
+ nature: nature
78
+ )
79
+ end
80
+
81
+ def iv
82
+ return @options[:iv] if @options[:iv]
83
+ @iv ||= Hash[Pokemon::BATTLE_STATS.map do |stat|
84
+ [stat, rand(0..31)]
85
+ end]
86
+ end
87
+
88
+ def ev
89
+ return @options[:ev] if @options[:ev]
90
+ @ev ||= Hash[Pokemon::BATTLE_STATS.map do |stat|
91
+ [stat, 0]
92
+ end]
93
+ end
94
+
95
+ def gender
96
+ return @options[:gender] if @options[:gender]
97
+ return 'neuter' unless @species.gender_ratios
98
+ calculate_gender
99
+ end
100
+
101
+ def calculate_gender
102
+ if rand(1..1000) <= @species.gender_ratios['male'] * 10
103
+ 'male'
104
+ else
105
+ 'female'
106
+ end
107
+ end
108
+
109
+ def nature(options = {})
110
+ @nature ||= if options[:nature]
111
+ Oakdex::Pokedex::Nature.find!(options[:nature])
112
+ else
113
+ Oakdex::Pokedex::Nature.all.values.sample
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end