traveller_rpg 0.0.1.1 → 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.
@@ -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