amar-tui 2.1.0 → 2.1.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. metadata +10 -120
  3. data/amar-tui.rb +0 -8195
  4. data/cli_enc_output.rb +0 -87
  5. data/cli_enc_output_new.rb +0 -433
  6. data/cli_enc_output_new_3tier.rb +0 -198
  7. data/cli_enc_output_new_compact.rb +0 -238
  8. data/cli_name_gen.rb +0 -21
  9. data/cli_npc_output.rb +0 -279
  10. data/cli_npc_output_new.rb +0 -700
  11. data/cli_town_output.rb +0 -39
  12. data/cli_weather_output.rb +0 -36
  13. data/includes/class_enc.rb +0 -341
  14. data/includes/class_enc_new.rb +0 -512
  15. data/includes/class_monster_new.rb +0 -551
  16. data/includes/class_npc.rb +0 -1378
  17. data/includes/class_npc_new.rb +0 -1221
  18. data/includes/class_npc_new.rb.backup +0 -706
  19. data/includes/class_npc_new_skills.rb +0 -153
  20. data/includes/class_town.rb +0 -237
  21. data/includes/d6s.rb +0 -40
  22. data/includes/equipment_tables.rb +0 -120
  23. data/includes/functions.rb +0 -67
  24. data/includes/includes.rb +0 -30
  25. data/includes/randomizer.rb +0 -15
  26. data/includes/spell_catalog.rb +0 -446
  27. data/includes/tables/armour.rb +0 -13
  28. data/includes/tables/chartype.rb +0 -4412
  29. data/includes/tables/chartype_new.rb +0 -765
  30. data/includes/tables/chartype_new_full.rb +0 -2713
  31. data/includes/tables/enc_specific.rb +0 -168
  32. data/includes/tables/enc_type.rb +0 -17
  33. data/includes/tables/encounters.rb +0 -99
  34. data/includes/tables/magick.rb +0 -169
  35. data/includes/tables/melee.rb +0 -36
  36. data/includes/tables/missile.rb +0 -17
  37. data/includes/tables/monster_stats_new.rb +0 -264
  38. data/includes/tables/month.rb +0 -18
  39. data/includes/tables/names.rb +0 -21
  40. data/includes/tables/personality.rb +0 -12
  41. data/includes/tables/race_templates.rb +0 -318
  42. data/includes/tables/religions.rb +0 -266
  43. data/includes/tables/spells_new.rb +0 -496
  44. data/includes/tables/tier_system.rb +0 -104
  45. data/includes/tables/town.rb +0 -71
  46. data/includes/tables/weather.rb +0 -41
  47. data/includes/town_relations.rb +0 -127
  48. data/includes/weather.rb +0 -108
  49. data/includes/weather2latex.rb +0 -114
  50. data/lib/rcurses.rb +0 -33
@@ -1,1221 +0,0 @@
1
- # New NPC Class for 3-Tier System
2
- # Implements the Characteristics > Attributes > Skills structure
3
-
4
- class NpcNew
5
- attr_accessor :name, :type, :level, :area, :sex, :age, :height, :weight
6
- attr_accessor :tiers, :social_status, :marks, :description
7
- attr_accessor :melee_weapon, :missile_weapon, :armor, :ENC, :spells
8
- # Original CLI weapon format accessors
9
- attr_accessor :melee1, :melee2, :melee3, :missile
10
- attr_accessor :melee1s, :melee2s, :melee3s, :missiles
11
- attr_accessor :melee1i, :melee1o, :melee1d, :melee1dam, :melee1hp
12
- attr_accessor :melee2i, :melee2o, :melee2d, :melee2dam, :melee2hp
13
- attr_accessor :melee3i, :melee3o, :melee3d, :melee3dam, :melee3hp
14
- attr_accessor :missileo, :missiledam, :missilerange
15
- attr_accessor :armour, :ap
16
-
17
- def initialize(name, type, level, area, sex, age, height, weight, description, predetermined_stats = nil)
18
- # Generate random values for missing data
19
- @name = name && !name.empty? ? name : generate_random_name(sex)
20
- @type = type
21
- @level = level.to_i
22
- @area = area && !area.empty? ? area : ["Amaronir", "Merisir", "Calaronir", "Feronir", "Rauinir"].sample
23
- @sex = sex && !sex.empty? ? sex : ["M", "F"].sample
24
-
25
- # Calculate realistic age based on experience level
26
- # Assuming training starts at 15, each level requires years of dedication
27
- if age.to_i > 0
28
- @age = age.to_i
29
- else
30
- base_age = 15 # Starting age for training
31
- years_training = case @level
32
- when 1 then rand(1..3) # 16-18 years old
33
- when 2 then rand(3..7) # 18-22 years old
34
- when 3 then rand(7..12) # 22-27 years old
35
- when 4 then rand(12..20) # 27-35 years old
36
- when 5 then rand(20..30) # 35-45 years old
37
- when 6 then rand(30..45) # 45-60 years old
38
- else rand(45..60) # 60-75 years old
39
- end
40
- @age = base_age + years_training
41
- end
42
-
43
- # Generate realistic height and weight based on Amar averages
44
- # Human average: 170cm height, 70kg weight
45
- # But allowing for strongmen up to 195cm/125kg
46
- if height.to_i > 0
47
- @height = height.to_i
48
- else
49
- # Base height around 160 + variation using open-ended d6 rolls
50
- # This gives a bell curve centered around 170cm
51
- base_height = 160
52
- variation = oD6 * 2 + oD6 + rand(10)
53
- @height = base_height + variation
54
- @height -= 5 if @sex == "F" # Females slightly shorter on average
55
- @height -= (3 * (16 - @age)) if @age < 17 # Youth adjustment
56
-
57
- # Type-based adjustments for larger/smaller builds
58
- case @type
59
- when /Warrior|Guard|Soldier|Body guard|Barbarian/
60
- @height += rand(0..10) # Warriors tend to be taller
61
- when /Thief|Assassin|Rogue/
62
- @height -= rand(0..5) # Rogues tend to be more average/shorter
63
- end
64
- end
65
-
66
- if weight.to_i > 0
67
- @weight = weight.to_i
68
- else
69
- # Weight based on height with more variation for different builds
70
- # Base formula: height - 120 + variation
71
- base_weight = @height - 120
72
-
73
- # Build variation based on character type
74
- build_modifier = case @type
75
- when /Warrior|Guard|Soldier|Body guard|Barbarian|Worker|Farmer/
76
- # Muscular builds: more weight for same height
77
- aD6 * 5 + rand(15) # Can add up to 55kg for strongman builds
78
- when /Noble|Merchant|Scholar|Sage|Priest|Mage|Wizard/
79
- # Average to lighter builds
80
- aD6 * 3 + rand(10) # Normal variation
81
- when /Thief|Assassin|Rogue|Scout/
82
- # Lean, agile builds
83
- aD6 * 2 + rand(10) # Lighter variation
84
- else
85
- # Default average build
86
- aD6 * 4 + rand(10)
87
- end
88
-
89
- @weight = base_weight + build_modifier
90
- @weight = [@weight, 40].max # Minimum weight of 40kg
91
- end
92
- @description = description
93
-
94
- # Initialize tier system structures
95
- @tiers = {
96
- "BODY" => {},
97
- "MIND" => {},
98
- "SPIRIT" => {}
99
- }
100
-
101
- # Initialize mark tracking for progression
102
- @marks = {
103
- "BODY" => {},
104
- "MIND" => {},
105
- "SPIRIT" => {}
106
- }
107
-
108
- # Set social status
109
- @social_status = ["S", "LC", "LMC", "MC", "UC", "N"].sample
110
-
111
- # Store predetermined stats for later use
112
- @predetermined_stats = predetermined_stats
113
-
114
- # Generate based on new tier system
115
- generate_tiers()
116
-
117
- # Generate spells if magic user
118
- generate_spells()
119
-
120
- # Select equipment
121
- select_equipment()
122
-
123
- # Initialize encumbrance
124
- @ENC = 0
125
- end
126
-
127
- private
128
-
129
- def generate_tiers
130
- # Load tier system and character templates
131
- unless defined?($TierSystem)
132
- load File.join($pgmdir, "includes/tables/tier_system.rb")
133
- end
134
- unless defined?($ChartypeNewFull)
135
- load File.join($pgmdir, "includes/tables/chartype_new_full.rb")
136
- end
137
-
138
- # Get template for this character type
139
- template = $ChartypeNewFull[@type] || $ChartypeNewFull["Commoner"]
140
-
141
- # Generate characteristics
142
- ["BODY", "MIND", "SPIRIT"].each do |char_name|
143
- char_base = template["characteristics"][char_name] || 2
144
-
145
- # Initialize attributes for this characteristic
146
- $TierSystem[char_name].each do |attr_name, attr_data|
147
- @tiers[char_name][attr_name] = {
148
- "level" => 0,
149
- "skills" => {}
150
- }
151
-
152
- # Set attribute base from template
153
- attr_key = "#{char_name}/#{attr_name}"
154
- if template["attributes"] && template["attributes"][attr_key]
155
- attr_base = template["attributes"][attr_key]
156
- else
157
- attr_base = attr_data["base"] || 0
158
- end
159
-
160
- # Calculate attribute level based on NPC level
161
- @tiers[char_name][attr_name]["level"] = calculate_tier_level(attr_base, @level, 0.8)
162
-
163
- # Initialize skills for this attribute
164
- if attr_data["skills"]
165
- # Convert array to hash for skills
166
- skill_list = attr_data["skills"]
167
- skill_list = [] unless skill_list.is_a?(Array)
168
-
169
- skill_list.each do |skill_name|
170
- # Check if template has specific skill values
171
- skill_key = "#{char_name}/#{attr_name}/#{skill_name}"
172
- skill_base = 0
173
-
174
- if template["skills"] && template["skills"][skill_key]
175
- skill_base = template["skills"][skill_key]
176
- end
177
-
178
- # Calculate skill level
179
- @tiers[char_name][attr_name]["skills"][skill_name] = calculate_tier_level(skill_base, @level, 0.6)
180
- end
181
- end
182
- end
183
- end
184
-
185
- # Add template-specific skills
186
- if template["skills"]
187
- template["skills"].each do |skill_path, base_value|
188
- parts = skill_path.split("/")
189
- next unless parts.length == 3
190
-
191
- char_name, attr_name, skill_name = parts
192
- next unless @tiers[char_name] && @tiers[char_name][attr_name]
193
-
194
- @tiers[char_name][attr_name]["skills"] ||= {}
195
- current = @tiers[char_name][attr_name]["skills"][skill_name] || 0
196
- new_value = calculate_tier_level(base_value, @level, 0.6)
197
- @tiers[char_name][attr_name]["skills"][skill_name] = [current, new_value].max
198
- end
199
- end
200
-
201
- # Add weapon skills from template
202
- add_weapon_skills(template)
203
-
204
- # Add Innate skills ONLY for specific character types
205
- innate_types = ["Witch (white)", "Witch (black)", "Sorcerer", "Summoner"]
206
- if innate_types.include?(@type)
207
- # These types get innate magical abilities
208
- innate_skills = ["Flying", "Camouflage", "Shape Shifting"]
209
- innate_skills.sample(rand(1..2)).each do |skill|
210
- @tiers["SPIRIT"]["Innate"]["skills"][skill] = rand(1..3) + (@level / 2)
211
- end
212
- end
213
-
214
- # For magic users, ensure they have appropriate Casting and Attunement
215
- if has_template_magic?(template)
216
- # Ensure minimum Casting level for spell use
217
- min_casting = (@level / 2) + 1
218
- if @tiers["SPIRIT"]["Casting"]["level"] < min_casting
219
- @tiers["SPIRIT"]["Casting"]["level"] = min_casting
220
- end
221
-
222
- # Add specific casting skills
223
- ["Range", "Duration", "Area of Effect"].each do |skill|
224
- @tiers["SPIRIT"]["Casting"]["skills"][skill] = rand(1..3) + (@level / 2)
225
- end
226
-
227
- # Set attunement domains based on type
228
- domains = case @type
229
- when /Wizard \((.*?)\)/
230
- [$1.capitalize]
231
- when "Priest"
232
- ["Life", "Body"]
233
- when "Mage"
234
- ["Fire", "Air", "Mind"]
235
- else
236
- ["Fire", "Water", "Air", "Earth"].sample(2)
237
- end
238
-
239
- domains.each do |domain|
240
- @tiers["SPIRIT"]["Attunement"]["skills"][domain] = rand(2..4) + (@level / 2)
241
- end
242
- end
243
-
244
- # Add experience-based skills for higher level NPCs
245
- if @level >= 4
246
- add_experience_skills()
247
- end
248
-
249
- # Apply predetermined stats if provided (for encounter consistency)
250
- apply_predetermined_stats() if @predetermined_stats
251
-
252
- # Ensure essential skills are always present
253
- ensure_essential_skills()
254
- end
255
-
256
- def add_experience_skills
257
- # Add additional skills for experienced characters
258
- experience_bonus = @level - 3 # 1 for level 4, 2 for level 5, etc.
259
-
260
- # Significantly expand skill generation based on level
261
- skill_count = case @level
262
- when 4 then rand(8..12)
263
- when 5 then rand(12..18)
264
- when 6 then rand(18..25)
265
- else rand(20..30)
266
- end
267
-
268
- skills_added = 0
269
-
270
- # Build skill pool based on actual tier system
271
- skill_pool = []
272
-
273
- # BODY skills - more varied distribution
274
- skill_pool += [
275
- ["BODY", "Athletics", "Hide", rand(1..2) + (experience_bonus / 2)],
276
- ["BODY", "Athletics", "Move Quietly", rand(1..2) + (experience_bonus / 2)],
277
- ["BODY", "Athletics", "Climb", rand(0..2) + (experience_bonus / 2)],
278
- ["BODY", "Athletics", "Swim", rand(0..2) + (experience_bonus / 2)],
279
- ["BODY", "Athletics", "Ride", rand(0..1) + (experience_bonus / 3)],
280
- ["BODY", "Athletics", "Jump", rand(0..1) + (experience_bonus / 3)],
281
- ["BODY", "Athletics", "Balance", rand(1..2) + (experience_bonus / 2)],
282
- ["BODY", "Endurance", "Running", rand(1..2) + (experience_bonus / 2)],
283
- ["BODY", "Endurance", "Combat Tenacity", rand(0..2) + (experience_bonus / 2)],
284
- ["BODY", "Sleight", "Pick pockets", rand(0..1) + (experience_bonus / 3)],
285
- ["BODY", "Sleight", "Disarm Traps", rand(0..1) + (experience_bonus / 3)]
286
- ]
287
-
288
- # MIND skills - more varied distribution
289
- skill_pool += [
290
- ["MIND", "Awareness", "Tracking", rand(1..2) + (experience_bonus / 2)],
291
- ["MIND", "Awareness", "Detect Traps", rand(0..1) + (experience_bonus / 3)],
292
- ["MIND", "Awareness", "Sense Emotions", rand(0..2) + (experience_bonus / 2)],
293
- ["MIND", "Awareness", "Sense of Direction", rand(1..2) + (experience_bonus / 2)],
294
- ["MIND", "Awareness", "Listening", rand(1..2) + (experience_bonus / 2)],
295
- ["MIND", "Social Knowledge", "Social lore", rand(1..2) + (experience_bonus / 2)],
296
- ["MIND", "Social Knowledge", "Spoken Language", rand(0..2) + (experience_bonus / 2)],
297
- ["MIND", "Social Knowledge", "Literacy", rand(0..2) + (experience_bonus / 2)],
298
- ["MIND", "Nature Knowledge", "Medical lore", rand(0..1) + (experience_bonus / 3)],
299
- ["MIND", "Nature Knowledge", "Plant Lore", rand(0..1) + (experience_bonus / 3)],
300
- ["MIND", "Nature Knowledge", "Animal Lore", rand(0..1) + (experience_bonus / 3)],
301
- ["MIND", "Practical Knowledge", "Survival Lore", rand(1..2) + (experience_bonus / 2)],
302
- ["MIND", "Practical Knowledge", "Set traps", rand(0..1) + (experience_bonus / 3)],
303
- ["MIND", "Willpower", "Mental Fortitude", rand(1..2) + (experience_bonus / 2)],
304
- ["MIND", "Willpower", "Courage", rand(0..2) + (experience_bonus / 2)]
305
- ]
306
-
307
- # Type-specific skills
308
- if has_magic?
309
- skill_pool += [
310
- ["MIND", "Awareness", "Sense Magick", rand(3..4) + experience_bonus],
311
- ["MIND", "Nature Knowledge", "Magick Rituals", rand(3..5) + experience_bonus],
312
- ["MIND", "Social Knowledge", "Mythology", rand(2..3) + experience_bonus],
313
- ["MIND", "Social Knowledge", "Legend Lore", rand(2..3) + experience_bonus],
314
- ["SPIRIT", "Casting", "Range", rand(2..4) + experience_bonus],
315
- ["SPIRIT", "Casting", "Duration", rand(2..4) + experience_bonus],
316
- ["SPIRIT", "Casting", "Area of Effect", rand(1..3) + experience_bonus]
317
- ]
318
- end
319
-
320
- # Physical combat types
321
- type_str = @type.to_s
322
- if ["Warrior", "Guard", "Soldier", "Gladiator", "Body guard", "Ranger", "Hunter"].any? { |t| type_str.include?(t) }
323
- skill_pool += [
324
- ["BODY", "Endurance", "Fortitude", rand(3..4) + experience_bonus],
325
- ["BODY", "Endurance", "Combat Tenacity", rand(3..4) + experience_bonus],
326
- ["MIND", "Practical Knowledge", "Ambush", rand(2..3) + experience_bonus],
327
- ["MIND", "Awareness", "Sense Ambush", rand(2..3) + experience_bonus]
328
- ]
329
- end
330
-
331
- # Scholarly types
332
- if ["Scholar", "Sage", "Scribe", "Wizard", "Mage"].any? { |t| type_str.include?(t) }
333
- skill_pool += [
334
- ["MIND", "Intelligence", "Innovation", rand(3..4) + experience_bonus],
335
- ["MIND", "Intelligence", "Problem Solving", rand(3..4) + experience_bonus],
336
- ["MIND", "Social Knowledge", "Literacy", rand(4..5) + experience_bonus],
337
- ["MIND", "Nature Knowledge", "Alchemy", rand(2..3) + experience_bonus]
338
- ]
339
- end
340
-
341
- # Rogueish types
342
- if ["Thief", "Rogue", "Assassin", "Scout"].any? { |t| type_str.include?(t) }
343
- skill_pool += [
344
- ["BODY", "Athletics", "Hide", rand(4..5) + experience_bonus],
345
- ["BODY", "Athletics", "Move Quietly", rand(4..5) + experience_bonus],
346
- ["BODY", "Sleight", "Pick pockets", rand(3..5) + experience_bonus],
347
- ["BODY", "Sleight", "Disarm Traps", rand(3..4) + experience_bonus],
348
- ["MIND", "Awareness", "Detect Traps", rand(3..4) + experience_bonus]
349
- ]
350
- end
351
-
352
- # Now randomly select and add skills
353
- skill_pool.shuffle!
354
-
355
- skill_pool.each do |char_name, attr_name, skill_name, value|
356
- break if skills_added >= skill_count
357
-
358
- # Ensure the tier structure exists
359
- next unless @tiers[char_name] && @tiers[char_name][attr_name]
360
-
361
- # Initialize skills hash if needed
362
- @tiers[char_name][attr_name]["skills"] ||= {}
363
-
364
- # Add skill if it doesn't exist or is 0
365
- if !@tiers[char_name][attr_name]["skills"][skill_name] ||
366
- @tiers[char_name][attr_name]["skills"][skill_name] == 0
367
- @tiers[char_name][attr_name]["skills"][skill_name] = value
368
- skills_added += 1
369
- end
370
- end
371
- end
372
-
373
- def calculate_tier_level(base, npc_level, tier_modifier)
374
- # Natural caps based on training difficulty
375
- # The harder to train, the lower the natural cap
376
-
377
- # Determine tier type and apply realistic caps
378
- # Adjusted to create proper population distribution
379
- tier_caps = case tier_modifier
380
- when 1.0 # Characteristic (hardest to train)
381
- { normal: 2, experienced: 3, master: 4, hero: 5 }
382
- when 0.8 # Attribute (moderate training)
383
- { normal: 3, experienced: 5, master: 6, hero: 7 }
384
- when 0.6 # Skill (easiest to train)
385
- { normal: 5, experienced: 7, master: 9, hero: 11 }
386
- else
387
- { normal: 2, experienced: 3, master: 4, hero: 5 }
388
- end
389
-
390
- # Determine NPC experience level - matches population distribution
391
- experience = case npc_level
392
- when 1..2 then :normal # Common folk
393
- when 3..4 then :experienced # Town champions
394
- when 5..6 then :master # Regional masters
395
- else :hero # National/legendary
396
- end
397
-
398
- max_value = tier_caps[experience]
399
-
400
- # Calculate level with diminishing returns
401
- # Characteristics grow very slowly, skills grow faster
402
- growth_rate = case tier_modifier
403
- when 1.0 then 0.4 # Very slow for characteristics
404
- when 0.8 then 0.6 # Moderate for attributes
405
- when 0.6 then 0.8 # Faster for skills
406
- else 0.5
407
- end
408
-
409
- # Use scaling that produces desired total skill progression
410
- # Target totals: L1: 4-5, L2: 6-7, L3: 8-9, L4: 10-11, L5: 12-13, L6: 14+
411
- # Typical warrior has BODY 1-2, Melee Combat 2-3, Weapon skill 2-3 at mid-levels
412
- level_multiplier = case npc_level
413
- when 1 then 0.7
414
- when 2 then 0.95
415
- when 3 then 1.2
416
- when 4 then 1.45
417
- when 5 then 1.7
418
- when 6 then 1.95
419
- else 2.2
420
- end
421
-
422
- level = (base * level_multiplier * growth_rate).to_i
423
-
424
- # Add variation ONLY if base > 0
425
- if base > 0
426
- variation = rand(3) - 1 # -1, 0, or 1
427
- level += variation
428
- end
429
-
430
- # Ensure minimum competence for trained individuals
431
- # Skills should rarely be below 3 for anyone with training
432
- if tier_modifier == 0.6 # Skills
433
- min_skill = case npc_level
434
- when 1..2 then 2
435
- when 3..4 then 3
436
- else 4
437
- end
438
- level = min_skill if level < min_skill && base > 0
439
- elsif tier_modifier == 0.8 # Attributes
440
- min_attr = case npc_level
441
- when 1..2 then 1
442
- when 3..4 then 2
443
- else 3
444
- end
445
- level = min_attr if level < min_attr && base > 0
446
- end
447
-
448
- # Apply training reality - most people plateau
449
- # Only exceptional individuals (high level + good base) reach max
450
- if tier_modifier == 1.0 # Characteristics rarely exceed 3
451
- level = 3 if level > 3 && rand(100) > 20 # 80% plateau at 3
452
- elsif tier_modifier == 0.8 # Attributes occasionally reach 6
453
- level = 5 if level > 5 && rand(100) > 40 # 60% plateau at 5
454
- end
455
-
456
- # Ensure within bounds
457
- level = 0 if level < 0
458
- level = max_value if level > max_value
459
-
460
- level
461
- end
462
-
463
- def add_weapon_skills(template)
464
- # Add melee weapon skills with primary weapon specialization
465
- if template["melee_weapons"]
466
- @tiers["BODY"]["Melee Combat"]["skills"] ||= {}
467
-
468
- # Find primary weapon (highest base value)
469
- primary_weapon = template["melee_weapons"].max_by { |_, v| v }
470
-
471
- template["melee_weapons"].each_with_index do |(weapon, skill_level), index|
472
- base_level = calculate_tier_level(skill_level, @level, 0.6)
473
-
474
- # Boost primary weapon by 1-2 points for specialization
475
- if weapon == primary_weapon[0] && skill_level >= 4
476
- boost = rand(2) + 1 # +1 or +2
477
- base_level += boost
478
- end
479
-
480
- @tiers["BODY"]["Melee Combat"]["skills"][weapon] = base_level
481
- end
482
- end
483
-
484
- # Add missile weapon skills
485
- if template["missile_weapons"]
486
- @tiers["BODY"]["Missile Combat"]["skills"] ||= {}
487
-
488
- # Find primary missile weapon
489
- primary_missile = template["missile_weapons"].max_by { |_, v| v }
490
-
491
- template["missile_weapons"].each do |weapon, skill_level|
492
- base_level = calculate_tier_level(skill_level, @level, 0.6)
493
-
494
- # Boost primary missile weapon
495
- if weapon == primary_missile[0] && skill_level >= 3
496
- boost = rand(2) + 1 # +1 or +2
497
- base_level += boost
498
- end
499
-
500
- @tiers["BODY"]["Missile Combat"]["skills"][weapon] = base_level
501
- end
502
- end
503
-
504
- # Add Unarmed combat for all NPCs (everyone can fight with fists)
505
- @tiers["BODY"]["Melee Combat"]["skills"] ||= {}
506
- if !@tiers["BODY"]["Melee Combat"]["skills"]["Unarmed"]
507
- # Base unarmed skill based on type and level
508
- unarmed_bonus = case @type
509
- when /Monk|Martial|Barbarian/
510
- 2 # Better at unarmed
511
- when /Warrior|Guard|Soldier|Gladiator/
512
- 1 # Decent at unarmed
513
- when /Wizard|Sage|Scholar|Scribe/
514
- -1 # Poor at unarmed
515
- else
516
- 0 # Average
517
- end
518
- unarmed_skill = calculate_tier_level(1 + unarmed_bonus, @level, 0.5)
519
- @tiers["BODY"]["Melee Combat"]["skills"]["Unarmed"] = [unarmed_skill, 0].max
520
- end
521
- end
522
-
523
- def select_equipment
524
- @weapons = []
525
- @ENC = 0
526
-
527
- # Get melee combat skills
528
- melee_skills = @tiers["BODY"]["Melee Combat"]["skills"] || {}
529
- missile_skills = @tiers["BODY"]["Missile Combat"]["skills"] || {}
530
-
531
- # Check if has shield skill
532
- has_shield = melee_skills["Shield"] && melee_skills["Shield"] > 0
533
-
534
- # Determine weapon loadout based on character type and skills
535
- case @type
536
- when /Warrior|Guard|Soldier|Knight/
537
- # Warriors typically have weapon + shield or two-handed weapon
538
- if has_shield && rand(100) < 70
539
- # Weapon + shield combo (70% chance if has shield skill)
540
- primary = select_best_weapon(melee_skills, ["Sword", "Axe", "Mace", "Spear"])
541
- @weapons << primary if primary
542
- @weapons << "Shield"
543
- elsif rand(100) < 40
544
- # Two-handed weapon (40% chance)
545
- primary = select_best_weapon(melee_skills, ["2H Sword", "2H Axe", "Polearm", "Spear"])
546
- @weapons << (primary || "Spear")
547
- else
548
- # Dual wield
549
- primary = select_best_weapon(melee_skills, ["Sword", "Axe", "Mace"])
550
- secondary = select_best_weapon(melee_skills, ["Short sword", "Dagger", "Hatchet"])
551
- @weapons << (primary || "Sword")
552
- @weapons << (secondary || "Dagger")
553
- end
554
- when /Thief|Assassin|Rogue/
555
- # Thieves prefer light weapons, often dual wield
556
- primary = select_best_weapon(melee_skills, ["Short sword", "Dagger", "Rapier"])
557
- @weapons << (primary || "Dagger")
558
- if rand(100) < 60
559
- # Often carry a second weapon
560
- @weapons << "Dagger"
561
- end
562
- when /Ranger|Hunter|Scout/
563
- # Rangers typically have melee + ranged
564
- primary = select_best_weapon(melee_skills, ["Sword", "Axe", "Spear"])
565
- @weapons << (primary || "Sword")
566
- if rand(100) < 30
567
- @weapons << "Dagger" # Backup weapon
568
- end
569
- when /Priest|Cleric|Monk/
570
- # Religious types often use blunt weapons
571
- primary = select_best_weapon(melee_skills, ["Mace", "Staff", "Hammer"])
572
- @weapons << (primary || "Staff")
573
- if has_shield && rand(100) < 40
574
- @weapons << "Shield"
575
- end
576
- when /Wizard|Mage|Sorcerer/
577
- # Mages usually just have a staff or dagger
578
- primary = select_best_weapon(melee_skills, ["Staff", "Dagger"])
579
- @weapons << (primary || "Staff")
580
- when /Noble/
581
- # Nobles have fancy weapons
582
- primary = select_best_weapon(melee_skills, ["Sword", "Rapier"])
583
- @weapons << (primary || "Sword")
584
- if rand(100) < 50
585
- @weapons << "Dagger" # Ornamental backup
586
- end
587
- when /Barbarian/
588
- # Barbarians use heavy weapons
589
- if rand(100) < 60
590
- primary = select_best_weapon(melee_skills, ["2H Axe", "2H Sword", "Spear"])
591
- @weapons << (primary || "2H Axe")
592
- else
593
- # Dual wield
594
- @weapons << select_best_weapon(melee_skills, ["Axe", "Sword"]) || "Axe"
595
- @weapons << select_best_weapon(melee_skills, ["Axe", "Mace"]) || "Hatchet"
596
- end
597
- when /Gladiator/
598
- # Gladiators have varied weapon combos
599
- combo = rand(100)
600
- if combo < 33
601
- # Sword and shield
602
- @weapons << select_best_weapon(melee_skills, ["Sword", "Spear"]) || "Sword"
603
- @weapons << "Shield"
604
- elsif combo < 66
605
- # Dual wield
606
- @weapons << "Sword"
607
- @weapons << "Short sword"
608
- else
609
- # Exotic weapon
610
- @weapons << select_best_weapon(melee_skills, ["Trident", "Net", "Spear"]) || "Spear"
611
- if has_shield
612
- @weapons << "Shield"
613
- end
614
- end
615
- when /Smith/
616
- # Smiths have varied hammers and tools - add randomization
617
- weapon_choice = rand(100)
618
- if weapon_choice < 40
619
- @weapons << select_best_weapon(melee_skills, ["Hammer", "War hammer"]) || "Hammer"
620
- elsif weapon_choice < 70
621
- @weapons << select_best_weapon(melee_skills, ["Mace", "Hammer"]) || "Mace"
622
- elsif weapon_choice < 85
623
- # Smith with axe
624
- @weapons << select_best_weapon(melee_skills, ["Axe", "Hatchet"]) || "Axe"
625
- else
626
- # Smith with sword (forged their own)
627
- @weapons << select_best_weapon(melee_skills, ["Sword", "Short sword"]) || "Sword"
628
- end
629
- # Sometimes add a shield
630
- if has_shield && rand(100) < 30
631
- @weapons << "Shield"
632
- end
633
- when /Sailor|Seaman|Mariner/
634
- # Sailors typically have cutlass or short sword
635
- primary = select_best_weapon(melee_skills, ["Cutlass", "Short sword", "Hatchet"])
636
- @weapons << (primary || "Cutlass")
637
- if rand(100) < 40
638
- @weapons << "Dagger" # Utility knife
639
- end
640
- when /Farmer|Commoner/
641
- # Common folk have simple weapons
642
- @weapons << select_best_weapon(melee_skills, ["Pitchfork", "Staff", "Hatchet"]) || "Staff"
643
- else
644
- # Default: pick best available weapon with more variety
645
- if melee_skills.any?
646
- # Get all weapons with decent skill
647
- good_weapons = melee_skills.select { |k, v| v > 0 && k != "Shield" }
648
- if good_weapons.any?
649
- # Sort weapons by skill level and pick from the better ones
650
- sorted_weapons = good_weapons.sort_by { |_, v| -v }
651
- # Take top weapons (those within 2 skill points of best)
652
- best_skill = sorted_weapons.first[1]
653
- top_weapons = sorted_weapons.select { |_, v| v >= best_skill - 2 }
654
-
655
- # Prefer weapons other than Dagger if available
656
- preferred = top_weapons.reject { |k, _| k =~ /Dagger/ }
657
- if preferred.any?
658
- @weapons << preferred.sample[0]
659
- else
660
- @weapons << top_weapons.sample[0]
661
- end
662
-
663
- # Sometimes add a secondary weapon
664
- if rand(100) < 30 && good_weapons.size > 1
665
- secondary = good_weapons.keys - [@weapons.first]
666
- @weapons << secondary.sample
667
- end
668
- end
669
- else
670
- # No skills, give more varied basic weapons
671
- basic_weapons = ["Staff", "Short sword", "Hatchet", "Mace", "Spear", "Knife"]
672
- @weapons << basic_weapons.sample
673
- end
674
- end
675
-
676
- # Add missile weapon if applicable
677
- if missile_skills.any?
678
- best_missile = missile_skills.max_by { |_, v| v }
679
- if best_missile && rand(100) < 70 # 70% chance to actually carry it
680
- @missile_weapon = best_missile[0]
681
- @ENC += 1
682
- end
683
- end
684
-
685
- # Set primary melee weapon for compatibility
686
- @melee_weapon = @weapons.first if @weapons.any?
687
-
688
- # Calculate encumbrance
689
- @ENC += @weapons.size
690
-
691
- # Use ORIGINAL armor selection logic from class_npc.rb
692
- # Determine armor level based on strength (exact original logic)
693
- strng = get_characteristic("BODY") || 3
694
- arm_level = case strng
695
- when 1..2 then 1
696
- when 3 then 2
697
- when 4 then 3
698
- when 5 then 5
699
- when 6 then 6
700
- when 7 then 7
701
- else 8
702
- end
703
-
704
- # Pick armor from ORIGINAL $Armour table
705
- armor_index = rand(arm_level) + 1
706
- armor_data = $Armour[armor_index]
707
-
708
- @armour = armor_data[0] # Armor name from original table
709
- @ap = armor_data[1] # Armor points from original table
710
-
711
- # Set armor in new format for compatibility with output
712
- @armor = {
713
- name: @armour,
714
- ap: @ap,
715
- enc: armor_data[4] || 0 # Weight as encumbrance
716
- }
717
-
718
- # Add armor encumbrance
719
- @ENC += @armor[:enc] if @armor
720
-
721
- # Add ORIGINAL weapon selection using $Melee and $Missile tables
722
- generate_original_weapons
723
- end
724
-
725
- def generate_original_weapons
726
- # Use EXACT original CLI weapon selection logic
727
- # Determine weapon level based on strength (like original)
728
- strng = get_characteristic("BODY") || 3
729
- wpn_level = case strng
730
- when 1 then 2
731
- when 2 then 4
732
- when 3 then 11
733
- when 4 then 18
734
- when 5 then 22
735
- when 7..8 then 28
736
- else 30
737
- end
738
-
739
- # Initialize weapon variables
740
- @melee1 = @melee2 = @melee3 = ""
741
- @melee1s = @melee2s = @melee3s = 0
742
- @melee1i = @melee1o = @melee1d = @melee1dam = @melee1hp = 0
743
- @melee2i = @melee2o = @melee2d = @melee2dam = @melee2hp = 0
744
- @melee3i = @melee3o = @melee3d = @melee3dam = @melee3hp = 0
745
- @missile = ""
746
- @missiles = @missileo = @missiledam = @missilerange = 0
747
-
748
- # Get reaction speed for initiative calculations
749
- reaction_speed = get_skill_total("MIND", "Awareness", "Reaction speed") || 0
750
- dodge_total = get_skill_total("BODY", "Athletics", "Dodge") || 0
751
-
752
- # Select melee weapon 1 from ORIGINAL $Melee table
753
- melee1_idx = rand(wpn_level) + 1
754
- melee1_data = $Melee[melee1_idx]
755
- @melee1 = melee1_data[0] # Weapon name like "Longsword/Buc"
756
- @melee1s = calculate_weapon_skill_for_name(@melee1)
757
- @melee1i = melee1_data[4] + reaction_speed # Init = weapon init + reaction
758
- @melee1o = melee1_data[5] + @melee1s # Off = weapon off + skill
759
- @melee1d = melee1_data[6] + @melee1s + (dodge_total / 5) # Def = weapon def + skill + dodge/5
760
- @melee1dam = melee1_data[3] + self.DB # Damage = weapon dam + DB
761
- @melee1hp = melee1_data[7] # Weapon hit points
762
-
763
- # Select melee weapon 2 (if different)
764
- melee2_idx = rand(wpn_level) + 1
765
- if melee2_idx != melee1_idx
766
- melee2_data = $Melee[melee2_idx]
767
- @melee2 = melee2_data[0]
768
- @melee2s = calculate_weapon_skill_for_name(@melee2)
769
- @melee2i = melee2_data[4] + reaction_speed
770
- @melee2o = melee2_data[5] + @melee2s
771
- @melee2d = melee2_data[6] + @melee2s + (dodge_total / 5)
772
- @melee2dam = melee2_data[3] + self.DB
773
- @melee2hp = melee2_data[7]
774
- end
775
-
776
- # Select missile weapon from ORIGINAL $Missile table
777
- msl_level = wpn_level
778
- missile_idx = rand(msl_level) + 1
779
- missile_data = $Missile[missile_idx]
780
- @missile = missile_data[0] # Weapon name like "Bow(H) [1]"
781
- @missiles = calculate_missile_skill_for_name(@missile)
782
- @missileo = missile_data[4] + @missiles # Off = weapon off + skill
783
- @missiledam = missile_data[3] + self.DB # Damage = weapon dam + DB
784
- @missilerange = missile_data[5] # Range from table
785
-
786
- # Apply strength bonus for throwing weapons (original logic)
787
- if @missile && missile_data[1] != "Crossbow" && missile_data[1] != "Bow"
788
- @missiledam += (strng / 5)
789
- end
790
- end
791
-
792
- def calculate_weapon_skill_for_name(weapon_name)
793
- # Map weapon names to 3-tier skills
794
- case weapon_name.to_s.downcase
795
- when /sword/
796
- get_skill_total("BODY", "Melee Combat", "Sword")
797
- when /axe/
798
- get_skill_total("BODY", "Melee Combat", "Axe")
799
- when /mace|club|hammer/
800
- get_skill_total("BODY", "Melee Combat", "Club")
801
- when /spear|polearm/
802
- get_skill_total("BODY", "Melee Combat", "Spear")
803
- when /staff/
804
- get_skill_total("BODY", "Melee Combat", "Staff")
805
- when /dagger|knife/
806
- get_skill_total("BODY", "Melee Combat", "Dagger")
807
- when /unarmed/
808
- get_skill_total("BODY", "Melee Combat", "Unarmed")
809
- else
810
- get_skill_total("BODY", "Melee Combat", "Sword") || 0
811
- end
812
- end
813
-
814
- def calculate_missile_skill_for_name(weapon_name)
815
- case weapon_name.to_s.downcase
816
- when /bow/
817
- get_skill_total("BODY", "Missile Combat", "Bow")
818
- when /crossbow|x-bow/
819
- get_skill_total("BODY", "Missile Combat", "Crossbow")
820
- when /sling/
821
- get_skill_total("BODY", "Missile Combat", "Sling")
822
- when /javelin/
823
- get_skill_total("BODY", "Missile Combat", "Javelin")
824
- when /rock|stone|throwing|th\s|knife/
825
- get_skill_total("BODY", "Missile Combat", "Throwing")
826
- else
827
- get_skill_total("BODY", "Missile Combat", "Bow") || 0
828
- end
829
- end
830
-
831
- def select_best_weapon(skills, preferred_weapons)
832
- # Find the best weapon from preferred list that character has skill in
833
- best_weapon = nil
834
- best_skill = 0
835
-
836
- preferred_weapons.each do |weapon|
837
- if skills[weapon] && skills[weapon] > best_skill
838
- best_weapon = weapon
839
- best_skill = skills[weapon]
840
- end
841
- end
842
-
843
- # If no preferred weapon found, check for any weapon skill
844
- if !best_weapon && skills.any?
845
- # Filter out Shield as it's not a primary weapon
846
- weapon_skills = skills.reject { |k, _| k == "Shield" }
847
- if weapon_skills.any?
848
- best = weapon_skills.max_by { |_, v| v }
849
- best_weapon = best[0]
850
- end
851
- end
852
-
853
- best_weapon
854
- end
855
-
856
- def has_template_magic?(template)
857
- # Check if template indicates magical ability
858
- return false unless template
859
-
860
- # Check if template has Casting attribute > 0
861
- casting_attr = template["attributes"]["SPIRIT/Casting"] || 0
862
- return true if casting_attr > 0
863
-
864
- # Check for specific magic-using types
865
- magic_types = ["Mage", "Wizard", "Witch (white)", "Witch (black)", "Sorcerer",
866
- "Summoner", "Priest", "Sage", "Seer"]
867
- type_str = @type.to_s
868
- magic_types.include?(type_str) || type_str.include?("Wizard")
869
- end
870
-
871
- # Moved to public section below
872
-
873
- def generate_spells
874
- # Generate spell cards based on character type and level
875
- spirit = get_characteristic("SPIRIT")
876
- casting_attr = get_attribute("SPIRIT", "Casting")
877
- casting_total = spirit + casting_attr
878
-
879
- # Restrict spells to appropriate creatures
880
- # Require SPIRIT >= 2 AND total Casting >= 5
881
- # Exclude specific races and creature types
882
- excluded_types = ["Araxi", "Troll", "Dwarf", "Ogre", "Lizard", "Animal", "Zombie", "Skeleton"]
883
- type_lower = @type.to_s.downcase
884
-
885
- is_excluded = excluded_types.any? { |ex| type_lower.include?(ex.downcase) }
886
-
887
- # Only generate spells for intelligent magical beings
888
- if spirit >= 2 && casting_total >= 5 && !is_excluded
889
- # Load spell database if not already loaded
890
- unless defined?($SpellDatabase)
891
- load File.join($pgmdir, "includes/tables/spells_new.rb")
892
- end
893
-
894
- @spells = generate_spell_cards(@type, @level, casting_total)
895
- end
896
- end
897
-
898
- # Dice rolling methods (matching old system)
899
- def d6
900
- rand(1..6)
901
- end
902
-
903
- def oD6
904
- # Open-ended D6 roll
905
- result = d6
906
- return result if (2..5).include?(result)
907
-
908
- if result == 1
909
- down = d6
910
- while down <= 3
911
- result -= 1
912
- down = d6
913
- end
914
- elsif result == 6
915
- up = d6
916
- while up >= 4
917
- result += 1
918
- up = d6
919
- end
920
- end
921
- result
922
- end
923
-
924
- def aD6
925
- # Average D6 roll (average of regular d6 and open-ended d6)
926
- ((d6 + oD6) / 2.0).to_i
927
- end
928
-
929
- # Public methods for accessing tier data
930
- public
931
-
932
- def has_magic?
933
- # Check if character has any casting ability
934
- casting_level = @tiers["SPIRIT"]["Casting"]["level"] || 0
935
- casting_level > 0
936
- end
937
-
938
- # Calculate derived stats
939
- def SIZE
940
- # SIZE system based on weight with half-sizes
941
- case @weight
942
- when 0...10 then 0.5
943
- when 10...15 then 1
944
- when 15...20 then 1.5
945
- when 20...35 then 2
946
- when 35...50 then 2.5
947
- when 50...75 then 3
948
- when 75...100 then 3.5
949
- when 100...125 then 4
950
- when 125...150 then 4.5
951
- when 150...188 then 5
952
- when 188...225 then 5.5
953
- when 225...263 then 6
954
- when 263...300 then 6.5
955
- when 300...350 then 7
956
- when 350...400 then 7.5
957
- when 400...450 then 8
958
- when 450...500 then 8.5
959
- when 500...550 then 9
960
- when 550...600 then 9.5
961
- when 600...663 then 10
962
- when 663...725 then 10.5
963
- when 725...788 then 11
964
- when 788...850 then 11.5
965
- when 850...925 then 12
966
- when 925...1000 then 12.5
967
- when 1000...1075 then 13
968
- when 1075...1150 then 13.5
969
- when 1150...1225 then 14
970
- when 1225...1300 then 14.5
971
- when 1300...1375 then 15
972
- when 1375...1450 then 15.5
973
- when 1450...1525 then 16
974
- when 1525...1600 then 16.5
975
- else
976
- # For very large creatures, add 0.5 per 100kg
977
- 16.5 + ((@weight - 1600) / 100.0).floor * 0.5
978
- end
979
- end
980
-
981
- def BP
982
- # Body Points: SIZE * 2 + Fortitude / 3
983
- # Fortitude is under Endurance
984
- fortitude = get_skill("BODY", "Endurance", "Fortitude")
985
- endurance = get_attribute("BODY", "Endurance")
986
- body = get_characteristic("BODY")
987
- total_fortitude = body + endurance + fortitude
988
- (self.SIZE * 2 + total_fortitude / 3.0).round
989
- end
990
-
991
- def DB
992
- # Damage Bonus: (SIZE + Strength total) / 3
993
- strength = get_attribute("BODY", "Strength")
994
- body = get_characteristic("BODY")
995
- total_strength = body + strength
996
- ((self.SIZE + total_strength) / 3.0).round
997
- end
998
-
999
- def MD
1000
- # Magic Defense: (Mental Fortitude + Attunement Self) / 3
1001
- mental_fortitude = get_skill("MIND", "Willpower", "Mental Fortitude")
1002
- willpower = get_attribute("MIND", "Willpower")
1003
- mind = get_characteristic("MIND")
1004
- total_mental_fortitude = mind + willpower + mental_fortitude
1005
-
1006
- attunement_self = get_skill("SPIRIT", "Attunement", "Self")
1007
- attunement = get_attribute("SPIRIT", "Attunement")
1008
- spirit = get_characteristic("SPIRIT")
1009
- total_attunement_self = spirit + attunement + attunement_self
1010
-
1011
- ((total_mental_fortitude + total_attunement_self) / 3.0).round
1012
- end
1013
-
1014
- def get_characteristic(name)
1015
- # Calculate characteristic as weighted average of its attributes
1016
- # Reflects years of broad training across all aspects
1017
- total = 0
1018
- count = 0
1019
- @tiers[name].each do |attr_name, attr_data|
1020
- if attr_data["level"] && attr_data["level"] > 0
1021
- total += attr_data["level"]
1022
- count += 1
1023
- end
1024
- end
1025
-
1026
- return 0 if count == 0
1027
-
1028
- # Characteristics are very hard to improve - most people stay at 2-3
1029
- char_level = (total.to_f / count / 1.5).round # Scaled down to reflect training difficulty
1030
-
1031
- # Apply realistic caps based on NPC level
1032
- max_char = case @level
1033
- when 1..2 then 2 # Novices rarely exceed 2
1034
- when 3..4 then 3 # Experienced typically max at 3
1035
- when 5..6 then 4 # Veterans might reach 4
1036
- else 5 # Only masters achieve 5
1037
- end
1038
-
1039
- char_level = max_char if char_level > max_char
1040
- char_level
1041
- end
1042
-
1043
- def get_attribute(char_name, attr_name)
1044
- @tiers[char_name][attr_name]["level"] || 0
1045
- end
1046
-
1047
- def get_skill(char_name, attr_name, skill_name)
1048
- @tiers[char_name][attr_name]["skills"][skill_name] || 0
1049
- end
1050
-
1051
- def get_skill_total(char_name, attr_name, skill_name)
1052
- # Calculate total: Characteristic + Attribute + Skill
1053
- char_level = get_characteristic(char_name)
1054
- attr_level = get_attribute(char_name, attr_name)
1055
- skill_level = get_skill(char_name, attr_name, skill_name)
1056
-
1057
- # Natural progression based on training difficulty:
1058
- # - Getting to 10 is achievable with focused skill training
1059
- # - Getting to 15 requires years of dedicated work
1060
- # - Getting to 18+ is legendary, requiring lifetime mastery
1061
- total = char_level + attr_level + skill_level
1062
-
1063
- # Apply soft cap at 18 for game balance (only true masters exceed)
1064
- if total > 18 && @level < 7
1065
- # Small chance for exceptional individuals to exceed 18
1066
- total = 18 unless rand(100) < 5
1067
- end
1068
-
1069
- total
1070
- end
1071
-
1072
- # Mark system for progression
1073
-
1074
- def add_mark(char_name, attr_name, skill_name = nil)
1075
- if skill_name
1076
- # Add mark to skill
1077
- key = "#{attr_name}/#{skill_name}"
1078
- @marks[char_name][key] ||= 0
1079
- @marks[char_name][key] += 1
1080
-
1081
- # Check if ready to advance
1082
- check_advancement(char_name, attr_name, skill_name)
1083
- else
1084
- # Add mark to attribute
1085
- @marks[char_name][attr_name] ||= 0
1086
- @marks[char_name][attr_name] += 1
1087
- end
1088
- end
1089
-
1090
- def check_advancement(char_name, attr_name, skill_name)
1091
- current_level = get_skill(char_name, attr_name, skill_name)
1092
- required_marks = (current_level + 1) * 5
1093
- key = "#{attr_name}/#{skill_name}"
1094
-
1095
- if @marks[char_name][key] >= required_marks
1096
- # Roll for advancement (all but a 1)
1097
- if oD6 > 1
1098
- @tiers[char_name][attr_name]["skills"][skill_name] += 1
1099
- @marks[char_name][key] = 0
1100
-
1101
- # Add mark to attribute above
1102
- add_mark(char_name, attr_name)
1103
-
1104
- return true
1105
- end
1106
- end
1107
- false
1108
- end
1109
-
1110
- private
1111
-
1112
- def generate_random_name(sex)
1113
- # Use the proper name generator based on race
1114
- race = @type.to_s.sub(/(:| ).*/, '').capitalize
1115
-
1116
- # Use naming function from functions.rb which uses the actual name generator
1117
- name = naming(race, sex)
1118
-
1119
- # Fallback to basic names if name generator fails
1120
- if name.nil? || name.empty?
1121
- if sex == "F" || (sex.empty? && rand(2) == 0)
1122
- female_names = ["Aria", "Luna", "Sera", "Mira", "Lyra", "Nova", "Kira", "Zara", "Elara", "Thalia"]
1123
- name = female_names.sample + " " + ["Starweaver", "Moonwhisper", "Brightblade", "Swiftwind", "Ironheart"].sample
1124
- else
1125
- male_names = ["Gareth", "Marcus", "Aldric", "Kael", "Doran", "Lucian", "Theron", "Cassius", "Orion", "Zephyr"]
1126
- name = male_names.sample + " " + ["Ironforge", "Stormcaller", "Darkbane", "Goldhand", "Steelclaw"].sample
1127
- end
1128
- end
1129
-
1130
- name
1131
- end
1132
-
1133
- def ensure_essential_skills
1134
- # Ensure all NPCs have essential combat/stealth awareness skills (even if at 0)
1135
- # These skills are frequently used in encounters
1136
-
1137
- # Ensure Athletics attribute exists with essential skills
1138
- @tiers["BODY"]["Athletics"] ||= {"level" => 0, "skills" => {}}
1139
- @tiers["BODY"]["Athletics"]["skills"] ||= {}
1140
-
1141
- # Add Move Quietly if missing
1142
- unless @tiers["BODY"]["Athletics"]["skills"].key?("Move Quietly")
1143
- @tiers["BODY"]["Athletics"]["skills"]["Move Quietly"] = 0
1144
- end
1145
-
1146
- # Add Hide if missing
1147
- unless @tiers["BODY"]["Athletics"]["skills"].key?("Hide")
1148
- @tiers["BODY"]["Athletics"]["skills"]["Hide"] = 0
1149
- end
1150
-
1151
- # Add Dodge if missing (often used in combat)
1152
- unless @tiers["BODY"]["Athletics"]["skills"].key?("Dodge")
1153
- @tiers["BODY"]["Athletics"]["skills"]["Dodge"] = 0
1154
- end
1155
-
1156
- # Ensure Awareness attribute exists with essential skills
1157
- @tiers["MIND"]["Awareness"] ||= {"level" => 0, "skills" => {}}
1158
- @tiers["MIND"]["Awareness"]["skills"] ||= {}
1159
-
1160
- # Add Alertness if missing (this is the general awareness skill)
1161
- unless @tiers["MIND"]["Awareness"]["skills"].key?("Alertness")
1162
- @tiers["MIND"]["Awareness"]["skills"]["Alertness"] = 0
1163
- end
1164
-
1165
- # Reaction speed should always be present for initiative calculations
1166
- unless @tiers["MIND"]["Awareness"]["skills"].key?("Reaction speed")
1167
- @tiers["MIND"]["Awareness"]["skills"]["Reaction speed"] = 0
1168
- end
1169
- end
1170
-
1171
- def apply_predetermined_stats
1172
- # Apply predetermined stats to preserve encounter consistency
1173
- # This allows encounters to pass specific stats that should not be randomized
1174
-
1175
- return unless @predetermined_stats.is_a?(Hash)
1176
-
1177
- # Override characteristics if provided
1178
- if @predetermined_stats["characteristics"]
1179
- @predetermined_stats["characteristics"].each do |char_name, value|
1180
- # This would require modifying the SIZE calculation system
1181
- # For now, we'll focus on weapons/armor/skills
1182
- end
1183
- end
1184
-
1185
- # Override specific weapon skills if provided
1186
- if @predetermined_stats["weapon_skills"]
1187
- @predetermined_stats["weapon_skills"].each do |weapon, skill_level|
1188
- # Add to melee combat skills
1189
- @tiers["BODY"]["Melee Combat"]["skills"] ||= {}
1190
- @tiers["BODY"]["Melee Combat"]["skills"][weapon] = skill_level.to_i
1191
- end
1192
- end
1193
-
1194
- # Override missile skills if provided
1195
- if @predetermined_stats["missile_skills"]
1196
- @predetermined_stats["missile_skills"].each do |weapon, skill_level|
1197
- @tiers["BODY"]["Missile Combat"]["skills"] ||= {}
1198
- @tiers["BODY"]["Missile Combat"]["skills"][weapon] = skill_level.to_i
1199
- end
1200
- end
1201
-
1202
- # Override armor if provided
1203
- if @predetermined_stats["armor"]
1204
- @armor = @predetermined_stats["armor"]
1205
- end
1206
-
1207
- # Override other skills if provided
1208
- if @predetermined_stats["skills"]
1209
- @predetermined_stats["skills"].each do |skill_path, value|
1210
- parts = skill_path.split("/")
1211
- next unless parts.length == 3
1212
-
1213
- char_name, attr_name, skill_name = parts
1214
- next unless @tiers[char_name] && @tiers[char_name][attr_name]
1215
-
1216
- @tiers[char_name][attr_name]["skills"] ||= {}
1217
- @tiers[char_name][attr_name]["skills"][skill_name] = value.to_i
1218
- end
1219
- end
1220
- end
1221
- end