traveller_rpg 0.0.1.1 → 0.1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,62 +5,35 @@ module TravellerRPG
5
5
  class Error < RuntimeError; end
6
6
  class UnknownAssignment < Error; end
7
7
 
8
- #
9
- # Actually useful defaults
10
-
11
8
  TERM_YEARS = 4
12
- ADVANCED_EDUCATION = 8
13
9
 
14
10
  #
15
- # Examples -- but should not be used as actual defaults
16
-
17
- QUALIFICATION = [:default, 5]
18
-
19
- PERSONAL_SKILLS = Array.new(6) { :default }
20
- SERVICE_SKILLS = Array.new(6) { :default }
21
- ADVANCED_SKILLS = Array.new(6) { :default }
11
+ # Temporary defaults
22
12
 
23
- RANKS = { 0 => ['Rookie', :default, 0] }
24
- SPECIALIST = {
25
- default: {
26
- skills: Array.new(6) { :default },
27
- survival: [:default, 5],
28
- advancement: [:default, 5],
29
- ranks: RANKS,
30
- }
31
- }
32
-
33
- EVENTS = {
34
- 2 => nil,
35
- 3 => nil,
36
- 4 => nil,
37
- 5 => nil,
38
- 6 => nil,
39
- 7 => nil,
40
- 8 => nil,
41
- 9 => nil,
42
- 10 => nil,
43
- 11 => nil,
44
- 12 => nil,
13
+ EVENTS = Array.new(11) { |i|
14
+ "Event #{i+2}"
15
+ }.each.with_index.reduce({}) { |memo, (s,idx)|
16
+ memo.merge(idx + 2 => { :text => s })
45
17
  }
46
- MISHAPS = {
47
- 1 => nil,
48
- 2 => nil,
49
- 3 => nil,
50
- 4 => nil,
51
- 5 => nil,
52
- 6 => nil,
18
+ MISHAPS = Array.new(6) { |i|
19
+ "Mishap #{i+1}"
20
+ }.each.with_index.reduce({}) { |memo, (s,idx)|
21
+ memo.merge(idx + 1 => { :text => s })
53
22
  }
54
23
 
55
- MUSTER_OUT = {
56
- 1 => [0, 'Default'],
57
- 2 => [0, 'Default'],
58
- 3 => [5, 'Default'],
59
- 4 => [5, 'Default'],
60
- 5 => [10, 'Default'],
61
- 6 => [10, 'Default'],
62
- 7 => [5000, 'Default'], # Gambler DM +1
63
- }
24
+ #
25
+ # Needed to function
26
+
27
+ # ADVANCED_EDUCATION (false for Drifter)
28
+ # QUALIFICATION (false for Drifter)
29
+ # PERSONAL_SKILLS
30
+ # SERVICE_SKILLS
31
+ # ADVANCED_SKILLS (unless ADVANCED_EDUCATION is false)
32
+ # SPECIALIST
33
+ # EVENTS
34
+ # MISHAPS
35
+ # CREDITS
36
+ # BENEFITS
64
37
 
65
38
  def self.roll_check?(label, dm:, check:, roll: nil)
66
39
  roll ||= TravellerRPG.roll('2d6')
@@ -69,65 +42,120 @@ module TravellerRPG
69
42
  (roll + dm) >= check
70
43
  end
71
44
 
72
- def self.muster_roll(label, dm: 0)
73
- roll = TravellerRPG.roll('d6')
74
- clamped = (roll + dm).clamp(1, 7)
75
- puts "#{label} roll: #{roll} (DM #{dm}) = #{clamped}"
76
- clamped
45
+ # helper method -- handles :choose
46
+ def self.stat_check(hsh)
47
+ return hsh if hsh == false
48
+ raise(Error, hsh.inspect) unless hsh.is_a?(Hash) and hsh.size == 1
49
+ case hsh[:choose]
50
+ when Hash
51
+ s = TravellerRPG.choose("Choose stat check:",
52
+ *hsh[:choose].keys)
53
+ [s, hsh[:choose][s]]
54
+ when NilClass
55
+ s = hsh.keys.first
56
+ [s, hsh[s]]
57
+ else
58
+ raise(Error, "unexpected choose: #{hsh[:choose]}")
59
+ end
77
60
  end
78
61
 
79
- attr_reader :term, :active, :rank, :assignment
62
+ attr_reader :term, :status, :rank, :title, :assignment
80
63
 
81
- def initialize(char, term: 0, active: false, rank: 0)
64
+ def initialize(char, term: 0, status: :new, rank: 0)
82
65
  @char = char
83
66
 
84
67
  # career tracking
85
68
  @term = term
86
- @active = active
69
+ @status = status
87
70
  @rank = rank
88
71
  @term_mandate = nil
89
72
  @title = nil
73
+ @assignment = nil
74
+ end
75
+
76
+ def name
77
+ self.class.name.split('::').last
90
78
  end
91
79
 
92
80
  def officer?
93
81
  false
94
82
  end
95
83
 
96
- def activate(assignment = nil)
97
- @active = true
84
+ def active?
85
+ @status == :active
86
+ end
87
+
88
+ def finished?
89
+ [:mishap, :finished].include? @status
90
+ end
91
+
92
+ def must_remain?
93
+ @term_mandate == :must_remain
94
+ end
95
+
96
+ def must_exit?
97
+ @term_mandate == :must_exit
98
+ end
99
+
100
+ # move status from :new to :active
101
+ # take on an assignment
102
+ # take any rank 0 title or skill
103
+ #
104
+ def activate(asg = nil)
105
+ raise(Error, "can't activate status: #{@status}") unless @status == :new
98
106
  s = self.class::SPECIALIST
99
- if assignment
100
- raise(UnknownAssignment, assignment.inspect) unless s.key?(assignment)
101
- @assignment = assignment
107
+ if asg
108
+ raise(UnknownAssignment, asg.inspect) unless s.key?(asg)
109
+ @assignment = asg
102
110
  else
103
- @assignment = TravellerRPG.choose("Choose a specialty:", *s.keys)
111
+ @assignment = TravellerRPG.choose("Choose assignment:", *s.keys)
104
112
  end
105
- @title, skill, level = self.rank_benefit
106
- @char.train(skill, level) if skill
107
- self
113
+ @status = :active
114
+ self.take_rank_benefit
108
115
  end
109
116
 
110
- def active?
111
- !!@active
117
+ def specialty
118
+ if @assignment and self.class::SPECIALIST.key?(@assignment)
119
+ self.class::SPECIALIST[@assignment]
120
+ else
121
+ raise(Error, "unknown assignment: #{@assignment.inspect}")
122
+ end
123
+ end
124
+
125
+ def service_skills(choose: false)
126
+ if choose
127
+ self.class::SERVICE_SKILLS.map { |s|
128
+ if s.is_a? Hash
129
+ TravellerRPG.choose("Choose skill:", *s.fetch(:choose))
130
+ else
131
+ s
132
+ end
133
+ }
134
+ else
135
+ self.class::SERVICE_SKILLS.reduce([]) { |ary, s|
136
+ ary + (s.is_a?(Hash) ? s.fetch(:choose) : [s])
137
+ }
138
+ end
112
139
  end
113
140
 
114
141
  def qualify_check?(dm: 0)
115
- stat, check = self.class::QUALIFICATION
142
+ stat, check = self.class.stat_check(self.class::QUALIFICATION)
143
+ return true if stat == false # only for Drifter
116
144
  @char.log "#{self.name} qualification: #{stat} #{check}+"
117
145
  dm += @char.stats_dm(stat)
118
146
  self.class.roll_check?('Qualify', dm: dm, check: check)
119
147
  end
120
148
 
121
149
  def survival_check?(dm: 0)
122
- stat, check = self.specialty.fetch(:survival)
123
- @char.log "#{self.name} #{@assignment} survival: #{stat} #{check}+"
150
+ stat, check = self.class.stat_check(self.specialty.fetch(:survival))
151
+ @char.log "#{self.name} [#{@assignment}] survival: #{stat} #{check}+"
124
152
  dm += @char.stats_dm(stat)
125
153
  self.class.roll_check?('Survival', dm: dm, check: check)
126
154
  end
127
155
 
128
156
  def advancement_check?(dm: 0)
129
- stat, check = self.specialty.fetch(:advancement)
130
- @char.log "#{self.name} #{@assignment} advancement: #{stat} #{check}+"
157
+ stat, check = self.class.stat_check(self.specialty.fetch(:advancement))
158
+ @char.log "#{self.name} [#{@assignment}] advancement: #{stat} #{check}+"
131
159
  dm += @char.stats_dm(stat)
132
160
  roll = TravellerRPG.roll('2d6')
133
161
  if roll <= @term
@@ -141,91 +169,81 @@ module TravellerRPG
141
169
  end
142
170
 
143
171
  def advanced_education?
144
- @char.stats[:education] >= self.class::ADVANCED_EDUCATION
172
+ self.class::ADVANCED_EDUCATION and
173
+ @char.stats.education >= self.class::ADVANCED_EDUCATION
145
174
  end
146
175
 
147
- # any skills obtained start at level 1
176
+ def advancement_roll(dm: 0)
177
+ if self.advancement_check?(dm: dm)
178
+ self.advance_rank
179
+ self.training_roll
180
+ end
181
+ end
182
+
183
+ # any skills obtained start at level 1; always a bump (+1)
148
184
  def training_roll
149
- choices = [:personal, :service, :specialist]
150
- choices << :advanced if self.advanced_education?
151
- choices << :officer if self.officer?
185
+ choices = %w{Personal Service Specialist}
186
+ choices << 'Advanced' if self.advanced_education?
187
+ choices << 'Officer' if self.officer?
152
188
  choice = TravellerRPG.choose("Choose skills regimen:", *choices)
153
- roll = TravellerRPG.roll('d6')
154
- @char.log "Training roll: #{roll}"
155
- @char.train \
156
- case choice
157
- when :personal then self.class::PERSONAL_SKILLS.fetch(roll - 1)
158
- when :service then self.class::SERVICE_SKILLS.fetch(roll - 1)
159
- when :specialist
160
- self.class::SPECIALIST.dig(@assignment, :skills, roll - 1)
161
- when :advanced then self.class::ADVANCED_SKILLS.fetch(roll - 1)
162
- when :officer then self.class::OFFICER_SKILLS.fetch(roll - 1)
163
- end
189
+ roll = TravellerRPG.roll('d6', label: "#{choice} training")
190
+ skill =
191
+ case choice
192
+ when 'Personal' then self.class::PERSONAL_SKILLS.fetch(roll - 1)
193
+ when 'Service' then self.class::SERVICE_SKILLS.fetch(roll - 1)
194
+ when 'Specialist' then self.specialty.fetch(:skills).fetch(roll - 1)
195
+ when 'Advanced' then self.class::ADVANCED_SKILLS.fetch(roll - 1)
196
+ when 'Officer' then self.class::OFFICER_SKILLS.fetch(roll - 1)
197
+ end
198
+ if skill.is_a?(Hash)
199
+ skill = TravellerRPG.choose("Choose:", *skill.fetch(:choose))
200
+ end
201
+ # the "skill" could be a stat e.g. endurance
202
+ if @char.skills.known?(skill)
203
+ @char.skills.bump(skill)
204
+ else
205
+ @char.stats.bump(skill)
206
+ end
207
+ @char.log "Trained #{skill} +1"
164
208
  self
165
209
  end
166
210
 
167
- def event_roll(dm: 0)
168
- roll = TravellerRPG.roll('2d6')
169
- clamped = (roll + dm).clamp(2, 12)
170
- puts "Event roll: #{roll} (DM #{dm}) = #{clamped}"
171
- @char.log "Event: #{self.class::EVENTS.fetch(clamped) || roll}"
172
- # TODO: actually perform the event stuff
211
+ def event_roll
212
+ ev = self.class::EVENTS.fetch TravellerRPG.roll('2d6', label: 'Event')
213
+ @char.log "Event: #{ev.fetch(:text)}"
214
+ # TODO: actually perform the event stuff in ev['script']
173
215
  end
174
216
 
175
217
  def mishap_roll
176
- roll = TravellerRPG.roll('d6')
177
- puts "Mishap roll: #{roll}"
178
- @char.log "Mishap: #{self.class::MISHAPS.fetch(roll) || roll}"
179
- # TODO: actually perform the mishap stuff
218
+ mh = self.class::MISHAPS.fetch TravellerRPG.roll('d6', label: 'Mishap')
219
+ @char.log "Mishap: #{mh.fetch(:text)}"
220
+ # TODO: actually perform the mishap stuff in mh['script']
180
221
  end
181
222
 
182
223
  def advance_rank
183
224
  @rank += 1
184
225
  @char.log "Advanced career to rank #{@rank}"
185
- title, skill, level = self.rank_benefit
186
- if title
187
- @char.log "Awarded rank title: #{title}"
188
- @title = title
189
- end
190
- if skill
191
- @char.log "Achieved rank skill: #{skill} #{level}"
192
- @char.train(skill, level)
193
- end
194
- self
226
+ self.take_rank_benefit
195
227
  end
196
228
 
197
- def must_remain?
198
- @term_mandate == :must_remain
199
- end
200
-
201
- def must_exit?
202
- @term_mandate == :must_exit
229
+ # possibly nil
230
+ def rank_benefit
231
+ self.specialty.fetch(:ranks)[@rank]
203
232
  end
204
233
 
205
234
  def run_term
206
- raise(Error, "career is inactive") unless @active
235
+ raise(Error, "career is inactive") unless self.active?
207
236
  raise(Error, "must exit") if self.must_exit?
208
237
  @term += 1
209
238
  @char.log format("%s term %i started, age %i",
210
239
  self.name, @term, @char.age)
211
240
  self.training_roll
212
241
 
213
- # TODO: DM?
214
242
  if self.survival_check?
215
243
  @char.log format("%s term %i completed successfully.",
216
244
  self.name, @term)
217
245
  @char.age TERM_YEARS
218
-
219
- # TODO: DM?
220
- self.commission_roll if self.respond_to?(:commission_roll)
221
-
222
- # TODO: DM?
223
- if self.advancement_check?
224
- self.advance_rank
225
- self.training_roll
226
- end
227
-
228
- # TODO: DM?
246
+ self.advancement_roll # TODO: DM?
229
247
  self.event_roll
230
248
  else
231
249
  years = rand(TERM_YEARS) + 1
@@ -233,96 +251,120 @@ module TravellerRPG
233
251
  self.name, years)
234
252
  @char.age years
235
253
  self.mishap_roll
236
- @active = false
254
+ @status = :mishap
237
255
  end
256
+ self
238
257
  end
239
258
 
240
259
  def retirement_bonus
241
260
  @term >= 5 ? @term * 2000 : 0
242
261
  end
243
262
 
244
- def muster_out(dm: 0)
263
+ # returns the roll value with DM applied
264
+ def muster_roll
265
+ dm = @char.skills.check?('Gambler', 1) ? 1 : 0
266
+ TravellerRPG.roll('d6', label: 'Muster', dm: dm)
267
+ end
268
+
269
+ def muster_out
245
270
  @char.log "Muster out: #{self.name}"
246
271
  raise(Error, "career has not started") unless @term > 0
247
- dm += @char.skill_check?(:gambler, 1) ? 1 : 0
248
-
249
- # one cash and one benefit per term
250
- # except if last term suffered mishap, no benefit for that term
251
-
252
272
  cash_rolls = @term.clamp(0, 3 - @char.cash_rolls)
253
- benefit_rolls = @term
254
-
255
- if @active
256
- @char.log "Career is in good standing -- collect all benefits"
257
- @active = false
258
- else
259
- @char.log "Left career early -- lose benefit for last term"
273
+ total_ranks = self.officer? ? self.rank + self.enlisted_rank : self.rank
274
+ benefit_rolls = @term + ((total_ranks + 1) / 2).clamp(0, 3)
275
+
276
+ case @status
277
+ when :active
278
+ @char.log "Muster out: Career in good standing; collect all benefits"
279
+ when :mishap
280
+ @char.log "Muster out: Career ended early; lose last term benefit"
260
281
  benefit_rolls -= 1
282
+ when :new, :finished
283
+ raise "invalid status: #{@status}"
284
+ else
285
+ raise "unknown status: #{@status}"
261
286
  end
262
287
 
288
+ # Collect "muster out" benefits
263
289
  cash_rolls.times {
264
- clamped = self.class.muster_roll('Cash', dm: dm)
265
- @char.cash_roll self.class::MUSTER_OUT.fetch(clamped).first
290
+ @char.cash_roll self.class::CREDITS.fetch(self.muster_roll)
266
291
  }
267
292
  benefit_rolls.times {
268
- clamped = self.class.muster_roll('Benefits', dm: dm)
269
- @char.benefit self.class::MUSTER_OUT.fetch(clamped).last
293
+ b = self.class::BENEFITS.fetch(self.muster_roll)
294
+ case b
295
+ when Array
296
+ b.each { |ben| @char.benefit ben }
297
+ when Hash
298
+ choice = TravellerRPG.choose("Choose benefit:", *b.fetch(:choose))
299
+ @char.benefit choice
300
+ else
301
+ @char.benefit b
302
+ end
270
303
  }
271
- @char.log "Retirement bonus: #{self.retirement_bonus}"
272
304
  @char.benefit self.retirement_bonus
305
+ @status = :finished
273
306
  self
274
307
  end
275
308
 
276
- def name
277
- self.class.name.split('::').last
278
- end
279
-
280
- def specialty
281
- self.class::SPECIALIST.fetch(@assignment)
282
- end
283
-
284
- # possibly nil
285
- def rank_benefit
286
- self.specialty.fetch(:ranks)[@rank]
287
- end
288
-
289
- def report(term: true, active: true, rank: true, spec: true)
309
+ def report(term: true, status: true, rank: true, spec: true)
290
310
  hsh = {}
291
311
  hsh['Term'] = @term if term
292
- hsh['Active'] = @active if active
312
+ hsh['Status'] = @status.to_s.capitalize if status
293
313
  hsh['Specialty'] = @assignment if spec
314
+ hsh['Title'] = @title if @title
294
315
  if rank
295
- hsh['Officer Rank'] = self.rank if self.officer?
296
- hsh['Rank'] = @rank
297
- hsh['Title'] = @title if @title
316
+ if self.officer?
317
+ hsh['Officer Rank'] = @officer
318
+ hsh['Enlisted Rank'] = @rank
319
+ else
320
+ hsh['Rank'] = @rank
321
+ end
298
322
  end
299
323
  report = ["Career: #{self.name}", "==="]
300
324
  hsh.each { |label, val|
301
- val = val.to_s.capitalize if val.is_a? Symbol
302
- report << format("%s: %s", label.to_s.rjust(10, ' '), val.to_s)
325
+ report << format("%s: %s", label.to_s.rjust(15, ' '), val)
303
326
  }
304
327
  report.join("\n")
305
328
  end
306
- end
307
329
 
330
+ protected
331
+
332
+ def take_rank_benefit
333
+ rb = self.rank_benefit or return self
334
+ label = self.officer? ? "officer rank" : "rank"
335
+ title, skill, stat, level = rb.values_at(:title, :skill, :stat, :level)
336
+ if title
337
+ @char.log "Awarded #{label} title: #{title}"
338
+ @title = title
339
+ end
340
+ if rb[:choose]
341
+ raise("unexpected choose: #{rb}") unless rb[:choose].is_a?(Array)
342
+ choices = rb[:choose].map { |h|
343
+ [h[:skill] || h[:stat], h[:level]].compact.join(' ')
344
+ }
345
+ choice = TravellerRPG.choose("Choose rank bonus:", *choices)
346
+ skill, stat, level = rb[:choose].select { |h|
347
+ choice == [h[:skill] || h[:stat], h[:level]].compact.join(' ')
348
+ }.first.values_at(:skill, :stat, :level)
349
+ raise "#{skill.inspect} or #{stat.inspect}" unless skill or stat
350
+ end
351
+ if skill
352
+ @char.log "Achieved #{label} bonus: #{skill} #{level}"
353
+ @char.skills.bump(skill, level)
354
+ end
355
+ if stat
356
+ @char.log "Achieved #{label} bonus: #{stat} #{level}"
357
+ @char.stats.bump(stat, level)
358
+ end
359
+ self
360
+ end
361
+ end
308
362
 
309
363
  #
310
- # MilitaryCareer adds Officer commission and parallel Officer ranks
364
+ # MilitaryCareer adds Officer commission opportunity which unlocks
365
+ # Officer skills and Officer ranks
311
366
 
312
367
  class MilitaryCareer < Career
313
-
314
- #
315
- # Actually useful defaults
316
-
317
- COMMISSION = [:social_status, 8]
318
-
319
- #
320
- # Examples -- but should not be used as actual defaults
321
-
322
- AGE_PENALTY = 40
323
- OFFICER_SKILLS = Array.new(6) { :default }
324
- OFFICER_RANKS = {}
325
-
326
368
  def initialize(char, **kwargs)
327
369
  super(char, **kwargs)
328
370
  @officer = false
@@ -335,26 +377,33 @@ module TravellerRPG
335
377
  end
336
378
 
337
379
  def commission_check?(dm: 0)
338
- stat, check = self.class::COMMISSION
380
+ stat, check = self.class.stat_check(self.class::COMMISSION)
339
381
  @char.log "#{self.name} commission: #{stat} #{check}"
340
382
  dm += @char.stats_dm(stat)
341
383
  self.class.roll_check?('Commission', dm: dm, check: check)
342
384
  end
343
385
 
344
- #
345
- # Achieve an officer commission
346
-
347
- def commission_roll(dm: 0)
348
- return if @officer
349
- if TravellerRPG.choose("Apply for commission?", :yes, :no) == :yes
350
- if self.commission_check?
386
+ def advancement_roll(dm: 0)
387
+ if !@officer and
388
+ (@term == 1 or @char.stats[:social_status] > 9) and true
389
+ # TravellerRPG.choose("Apply for commission?", :yes, :no) == :yes
390
+ comm_dm = @term > 1 ? dm - 1 : dm
391
+ if self.commission_check?(dm: comm_dm)
351
392
  @char.log "Became an officer!"
352
- @officer = 0 # officer rank
353
- self.advance_rank # officers start at rank 1
393
+ @officer = 1
394
+ self.take_rank_benefit
395
+
396
+ # skip normal advancement after successful commission
397
+ # but take the bonus training roll
398
+ self.training_roll
399
+
400
+ return self
354
401
  else
355
402
  @char.log "Commission was rejected"
356
403
  end
357
404
  end
405
+ # perform normal advancement unless commission was obtained
406
+ super(dm: dm)
358
407
  end
359
408
 
360
409
  #
@@ -380,15 +429,7 @@ module TravellerRPG
380
429
  return super unless @officer
381
430
  @officer += 1
382
431
  @char.log "Advanced career to officer rank #{@officer}"
383
- title, skill, level = self.rank_benefit
384
- if title
385
- @char.log "Awarded officer rank title: #{title}"
386
- @title = title
387
- end
388
- if skill
389
- @char.log "Achieved officer rank skill: #{skill} #{level}"
390
- @char.train(skill, level)
391
- end
432
+ self.take_rank_benefit
392
433
  end
393
434
  end
394
435
  end