amar-rpg 2.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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +675 -0
  3. data/README.md +155 -0
  4. data/amar-tui.rb +8195 -0
  5. data/cli_enc_output.rb +87 -0
  6. data/cli_enc_output_new.rb +433 -0
  7. data/cli_enc_output_new_3tier.rb +198 -0
  8. data/cli_enc_output_new_compact.rb +238 -0
  9. data/cli_name_gen.rb +21 -0
  10. data/cli_npc_output.rb +279 -0
  11. data/cli_npc_output_new.rb +700 -0
  12. data/cli_town_output.rb +39 -0
  13. data/cli_weather_output.rb +36 -0
  14. data/includes/class_enc.rb +341 -0
  15. data/includes/class_enc_new.rb +512 -0
  16. data/includes/class_monster_new.rb +551 -0
  17. data/includes/class_npc.rb +1378 -0
  18. data/includes/class_npc_new.rb +1187 -0
  19. data/includes/class_npc_new.rb.backup +706 -0
  20. data/includes/class_npc_new_skills.rb +153 -0
  21. data/includes/class_town.rb +237 -0
  22. data/includes/d6s.rb +40 -0
  23. data/includes/equipment_tables.rb +120 -0
  24. data/includes/functions.rb +67 -0
  25. data/includes/includes.rb +30 -0
  26. data/includes/randomizer.rb +15 -0
  27. data/includes/spell_catalog.rb +441 -0
  28. data/includes/tables/armour.rb +13 -0
  29. data/includes/tables/chartype.rb +4412 -0
  30. data/includes/tables/chartype_new.rb +765 -0
  31. data/includes/tables/chartype_new_full.rb +2713 -0
  32. data/includes/tables/enc_specific.rb +168 -0
  33. data/includes/tables/enc_type.rb +17 -0
  34. data/includes/tables/encounters.rb +99 -0
  35. data/includes/tables/magick.rb +169 -0
  36. data/includes/tables/melee.rb +36 -0
  37. data/includes/tables/missile.rb +17 -0
  38. data/includes/tables/monster_stats_new.rb +264 -0
  39. data/includes/tables/month.rb +18 -0
  40. data/includes/tables/names.rb +21 -0
  41. data/includes/tables/personality.rb +12 -0
  42. data/includes/tables/race_templates.rb +318 -0
  43. data/includes/tables/religions.rb +266 -0
  44. data/includes/tables/spells_new.rb +496 -0
  45. data/includes/tables/tier_system.rb +104 -0
  46. data/includes/tables/town.rb +71 -0
  47. data/includes/tables/weather.rb +41 -0
  48. data/includes/town_relations.rb +127 -0
  49. data/includes/weather.rb +108 -0
  50. data/includes/weather2latex.rb +114 -0
  51. data/lib/rcurses.rb +33 -0
  52. metadata +157 -0
@@ -0,0 +1,1187 @@
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 square root for more realistic progression
410
+ level = (base * Math.sqrt(npc_level + 1) * growth_rate).to_i
411
+
412
+ # Add minimal variation ONLY if base > 0
413
+ if base > 0
414
+ variation = rand(3) - 1 # -1, 0, or 1
415
+ level += variation
416
+ end
417
+
418
+ # Ensure minimum competence for trained individuals
419
+ # Skills should rarely be below 3 for anyone with training
420
+ if tier_modifier == 0.6 # Skills
421
+ min_skill = case npc_level
422
+ when 1..2 then 2
423
+ when 3..4 then 3
424
+ else 4
425
+ end
426
+ level = min_skill if level < min_skill && base > 0
427
+ elsif tier_modifier == 0.8 # Attributes
428
+ min_attr = case npc_level
429
+ when 1..2 then 1
430
+ when 3..4 then 2
431
+ else 3
432
+ end
433
+ level = min_attr if level < min_attr && base > 0
434
+ end
435
+
436
+ # Apply training reality - most people plateau
437
+ # Only exceptional individuals (high level + good base) reach max
438
+ if tier_modifier == 1.0 # Characteristics rarely exceed 3
439
+ level = 3 if level > 3 && rand(100) > 20 # 80% plateau at 3
440
+ elsif tier_modifier == 0.8 # Attributes occasionally reach 6
441
+ level = 5 if level > 5 && rand(100) > 40 # 60% plateau at 5
442
+ end
443
+
444
+ # Ensure within bounds
445
+ level = 0 if level < 0
446
+ level = max_value if level > max_value
447
+
448
+ level
449
+ end
450
+
451
+ def add_weapon_skills(template)
452
+ # Add melee weapon skills
453
+ if template["melee_weapons"]
454
+ @tiers["BODY"]["Melee Combat"]["skills"] ||= {}
455
+ template["melee_weapons"].each do |weapon, skill_level|
456
+ base_level = calculate_tier_level(skill_level, @level, 0.6)
457
+ @tiers["BODY"]["Melee Combat"]["skills"][weapon] = base_level
458
+ end
459
+ end
460
+
461
+ # Add missile weapon skills
462
+ if template["missile_weapons"]
463
+ @tiers["BODY"]["Missile Combat"]["skills"] ||= {}
464
+ template["missile_weapons"].each do |weapon, skill_level|
465
+ base_level = calculate_tier_level(skill_level, @level, 0.6)
466
+ @tiers["BODY"]["Missile Combat"]["skills"][weapon] = base_level
467
+ end
468
+ end
469
+
470
+ # Add Unarmed combat for all NPCs (everyone can fight with fists)
471
+ @tiers["BODY"]["Melee Combat"]["skills"] ||= {}
472
+ if !@tiers["BODY"]["Melee Combat"]["skills"]["Unarmed"]
473
+ # Base unarmed skill based on type and level
474
+ unarmed_bonus = case @type
475
+ when /Monk|Martial|Barbarian/
476
+ 2 # Better at unarmed
477
+ when /Warrior|Guard|Soldier|Gladiator/
478
+ 1 # Decent at unarmed
479
+ when /Wizard|Sage|Scholar|Scribe/
480
+ -1 # Poor at unarmed
481
+ else
482
+ 0 # Average
483
+ end
484
+ unarmed_skill = calculate_tier_level(1 + unarmed_bonus, @level, 0.5)
485
+ @tiers["BODY"]["Melee Combat"]["skills"]["Unarmed"] = [unarmed_skill, 0].max
486
+ end
487
+ end
488
+
489
+ def select_equipment
490
+ @weapons = []
491
+ @ENC = 0
492
+
493
+ # Get melee combat skills
494
+ melee_skills = @tiers["BODY"]["Melee Combat"]["skills"] || {}
495
+ missile_skills = @tiers["BODY"]["Missile Combat"]["skills"] || {}
496
+
497
+ # Check if has shield skill
498
+ has_shield = melee_skills["Shield"] && melee_skills["Shield"] > 0
499
+
500
+ # Determine weapon loadout based on character type and skills
501
+ case @type
502
+ when /Warrior|Guard|Soldier|Knight/
503
+ # Warriors typically have weapon + shield or two-handed weapon
504
+ if has_shield && rand(100) < 70
505
+ # Weapon + shield combo (70% chance if has shield skill)
506
+ primary = select_best_weapon(melee_skills, ["Sword", "Axe", "Mace", "Spear"])
507
+ @weapons << primary if primary
508
+ @weapons << "Shield"
509
+ elsif rand(100) < 40
510
+ # Two-handed weapon (40% chance)
511
+ primary = select_best_weapon(melee_skills, ["2H Sword", "2H Axe", "Polearm", "Spear"])
512
+ @weapons << (primary || "Spear")
513
+ else
514
+ # Dual wield
515
+ primary = select_best_weapon(melee_skills, ["Sword", "Axe", "Mace"])
516
+ secondary = select_best_weapon(melee_skills, ["Short sword", "Dagger", "Hatchet"])
517
+ @weapons << (primary || "Sword")
518
+ @weapons << (secondary || "Dagger")
519
+ end
520
+ when /Thief|Assassin|Rogue/
521
+ # Thieves prefer light weapons, often dual wield
522
+ primary = select_best_weapon(melee_skills, ["Short sword", "Dagger", "Rapier"])
523
+ @weapons << (primary || "Dagger")
524
+ if rand(100) < 60
525
+ # Often carry a second weapon
526
+ @weapons << "Dagger"
527
+ end
528
+ when /Ranger|Hunter|Scout/
529
+ # Rangers typically have melee + ranged
530
+ primary = select_best_weapon(melee_skills, ["Sword", "Axe", "Spear"])
531
+ @weapons << (primary || "Sword")
532
+ if rand(100) < 30
533
+ @weapons << "Dagger" # Backup weapon
534
+ end
535
+ when /Priest|Cleric|Monk/
536
+ # Religious types often use blunt weapons
537
+ primary = select_best_weapon(melee_skills, ["Mace", "Staff", "Hammer"])
538
+ @weapons << (primary || "Staff")
539
+ if has_shield && rand(100) < 40
540
+ @weapons << "Shield"
541
+ end
542
+ when /Wizard|Mage|Sorcerer/
543
+ # Mages usually just have a staff or dagger
544
+ primary = select_best_weapon(melee_skills, ["Staff", "Dagger"])
545
+ @weapons << (primary || "Staff")
546
+ when /Noble/
547
+ # Nobles have fancy weapons
548
+ primary = select_best_weapon(melee_skills, ["Sword", "Rapier"])
549
+ @weapons << (primary || "Sword")
550
+ if rand(100) < 50
551
+ @weapons << "Dagger" # Ornamental backup
552
+ end
553
+ when /Barbarian/
554
+ # Barbarians use heavy weapons
555
+ if rand(100) < 60
556
+ primary = select_best_weapon(melee_skills, ["2H Axe", "2H Sword", "Spear"])
557
+ @weapons << (primary || "2H Axe")
558
+ else
559
+ # Dual wield
560
+ @weapons << select_best_weapon(melee_skills, ["Axe", "Sword"]) || "Axe"
561
+ @weapons << select_best_weapon(melee_skills, ["Axe", "Mace"]) || "Hatchet"
562
+ end
563
+ when /Gladiator/
564
+ # Gladiators have varied weapon combos
565
+ combo = rand(100)
566
+ if combo < 33
567
+ # Sword and shield
568
+ @weapons << select_best_weapon(melee_skills, ["Sword", "Spear"]) || "Sword"
569
+ @weapons << "Shield"
570
+ elsif combo < 66
571
+ # Dual wield
572
+ @weapons << "Sword"
573
+ @weapons << "Short sword"
574
+ else
575
+ # Exotic weapon
576
+ @weapons << select_best_weapon(melee_skills, ["Trident", "Net", "Spear"]) || "Spear"
577
+ if has_shield
578
+ @weapons << "Shield"
579
+ end
580
+ end
581
+ when /Smith/
582
+ # Smiths have varied hammers and tools - add randomization
583
+ weapon_choice = rand(100)
584
+ if weapon_choice < 40
585
+ @weapons << select_best_weapon(melee_skills, ["Hammer", "War hammer"]) || "Hammer"
586
+ elsif weapon_choice < 70
587
+ @weapons << select_best_weapon(melee_skills, ["Mace", "Hammer"]) || "Mace"
588
+ elsif weapon_choice < 85
589
+ # Smith with axe
590
+ @weapons << select_best_weapon(melee_skills, ["Axe", "Hatchet"]) || "Axe"
591
+ else
592
+ # Smith with sword (forged their own)
593
+ @weapons << select_best_weapon(melee_skills, ["Sword", "Short sword"]) || "Sword"
594
+ end
595
+ # Sometimes add a shield
596
+ if has_shield && rand(100) < 30
597
+ @weapons << "Shield"
598
+ end
599
+ when /Sailor|Seaman|Mariner/
600
+ # Sailors typically have cutlass or short sword
601
+ primary = select_best_weapon(melee_skills, ["Cutlass", "Short sword", "Hatchet"])
602
+ @weapons << (primary || "Cutlass")
603
+ if rand(100) < 40
604
+ @weapons << "Dagger" # Utility knife
605
+ end
606
+ when /Farmer|Commoner/
607
+ # Common folk have simple weapons
608
+ @weapons << select_best_weapon(melee_skills, ["Pitchfork", "Staff", "Hatchet"]) || "Staff"
609
+ else
610
+ # Default: pick best available weapon with more variety
611
+ if melee_skills.any?
612
+ # Get all weapons with decent skill
613
+ good_weapons = melee_skills.select { |k, v| v > 0 && k != "Shield" }
614
+ if good_weapons.any?
615
+ # Sort weapons by skill level and pick from the better ones
616
+ sorted_weapons = good_weapons.sort_by { |_, v| -v }
617
+ # Take top weapons (those within 2 skill points of best)
618
+ best_skill = sorted_weapons.first[1]
619
+ top_weapons = sorted_weapons.select { |_, v| v >= best_skill - 2 }
620
+
621
+ # Prefer weapons other than Dagger if available
622
+ preferred = top_weapons.reject { |k, _| k =~ /Dagger/ }
623
+ if preferred.any?
624
+ @weapons << preferred.sample[0]
625
+ else
626
+ @weapons << top_weapons.sample[0]
627
+ end
628
+
629
+ # Sometimes add a secondary weapon
630
+ if rand(100) < 30 && good_weapons.size > 1
631
+ secondary = good_weapons.keys - [@weapons.first]
632
+ @weapons << secondary.sample
633
+ end
634
+ end
635
+ else
636
+ # No skills, give more varied basic weapons
637
+ basic_weapons = ["Staff", "Short sword", "Hatchet", "Mace", "Spear", "Knife"]
638
+ @weapons << basic_weapons.sample
639
+ end
640
+ end
641
+
642
+ # Add missile weapon if applicable
643
+ if missile_skills.any?
644
+ best_missile = missile_skills.max_by { |_, v| v }
645
+ if best_missile && rand(100) < 70 # 70% chance to actually carry it
646
+ @missile_weapon = best_missile[0]
647
+ @ENC += 1
648
+ end
649
+ end
650
+
651
+ # Set primary melee weapon for compatibility
652
+ @melee_weapon = @weapons.first if @weapons.any?
653
+
654
+ # Calculate encumbrance
655
+ @ENC += @weapons.size
656
+
657
+ # Use ORIGINAL armor selection logic from class_npc.rb
658
+ # Determine armor level based on strength (exact original logic)
659
+ strng = get_characteristic("BODY") || 3
660
+ arm_level = case strng
661
+ when 1..2 then 1
662
+ when 3 then 2
663
+ when 4 then 3
664
+ when 5 then 5
665
+ when 6 then 6
666
+ when 7 then 7
667
+ else 8
668
+ end
669
+
670
+ # Pick armor from ORIGINAL $Armour table
671
+ armor_index = rand(arm_level) + 1
672
+ armor_data = $Armour[armor_index]
673
+
674
+ @armour = armor_data[0] # Armor name from original table
675
+ @ap = armor_data[1] # Armor points from original table
676
+
677
+ # Set armor in new format for compatibility with output
678
+ @armor = {
679
+ name: @armour,
680
+ ap: @ap,
681
+ enc: armor_data[4] || 0 # Weight as encumbrance
682
+ }
683
+
684
+ # Add armor encumbrance
685
+ @ENC += @armor[:enc] if @armor
686
+
687
+ # Add ORIGINAL weapon selection using $Melee and $Missile tables
688
+ generate_original_weapons
689
+ end
690
+
691
+ def generate_original_weapons
692
+ # Use EXACT original CLI weapon selection logic
693
+ # Determine weapon level based on strength (like original)
694
+ strng = get_characteristic("BODY") || 3
695
+ wpn_level = case strng
696
+ when 1 then 2
697
+ when 2 then 4
698
+ when 3 then 11
699
+ when 4 then 18
700
+ when 5 then 22
701
+ when 7..8 then 28
702
+ else 30
703
+ end
704
+
705
+ # Initialize weapon variables
706
+ @melee1 = @melee2 = @melee3 = ""
707
+ @melee1s = @melee2s = @melee3s = 0
708
+ @melee1i = @melee1o = @melee1d = @melee1dam = @melee1hp = 0
709
+ @melee2i = @melee2o = @melee2d = @melee2dam = @melee2hp = 0
710
+ @melee3i = @melee3o = @melee3d = @melee3dam = @melee3hp = 0
711
+ @missile = ""
712
+ @missiles = @missileo = @missiledam = @missilerange = 0
713
+
714
+ # Get reaction speed for initiative calculations
715
+ reaction_speed = get_skill_total("MIND", "Awareness", "Reaction speed") || 0
716
+ dodge_total = get_skill_total("BODY", "Athletics", "Dodge") || 0
717
+
718
+ # Select melee weapon 1 from ORIGINAL $Melee table
719
+ melee1_idx = rand(wpn_level) + 1
720
+ melee1_data = $Melee[melee1_idx]
721
+ @melee1 = melee1_data[0] # Weapon name like "Longsword/Buc"
722
+ @melee1s = calculate_weapon_skill_for_name(@melee1)
723
+ @melee1i = melee1_data[4] + reaction_speed # Init = weapon init + reaction
724
+ @melee1o = melee1_data[5] + @melee1s # Off = weapon off + skill
725
+ @melee1d = melee1_data[6] + @melee1s + (dodge_total / 5) # Def = weapon def + skill + dodge/5
726
+ @melee1dam = melee1_data[3] + self.DB # Damage = weapon dam + DB
727
+ @melee1hp = melee1_data[7] # Weapon hit points
728
+
729
+ # Select melee weapon 2 (if different)
730
+ melee2_idx = rand(wpn_level) + 1
731
+ if melee2_idx != melee1_idx
732
+ melee2_data = $Melee[melee2_idx]
733
+ @melee2 = melee2_data[0]
734
+ @melee2s = calculate_weapon_skill_for_name(@melee2)
735
+ @melee2i = melee2_data[4] + reaction_speed
736
+ @melee2o = melee2_data[5] + @melee2s
737
+ @melee2d = melee2_data[6] + @melee2s + (dodge_total / 5)
738
+ @melee2dam = melee2_data[3] + self.DB
739
+ @melee2hp = melee2_data[7]
740
+ end
741
+
742
+ # Select missile weapon from ORIGINAL $Missile table
743
+ msl_level = wpn_level
744
+ missile_idx = rand(msl_level) + 1
745
+ missile_data = $Missile[missile_idx]
746
+ @missile = missile_data[0] # Weapon name like "Bow(H) [1]"
747
+ @missiles = calculate_missile_skill_for_name(@missile)
748
+ @missileo = missile_data[4] + @missiles # Off = weapon off + skill
749
+ @missiledam = missile_data[3] + self.DB # Damage = weapon dam + DB
750
+ @missilerange = missile_data[5] # Range from table
751
+
752
+ # Apply strength bonus for throwing weapons (original logic)
753
+ if @missile && missile_data[1] != "Crossbow" && missile_data[1] != "Bow"
754
+ @missiledam += (strng / 5)
755
+ end
756
+ end
757
+
758
+ def calculate_weapon_skill_for_name(weapon_name)
759
+ # Map weapon names to 3-tier skills
760
+ case weapon_name.to_s.downcase
761
+ when /sword/
762
+ get_skill_total("BODY", "Melee Combat", "Sword")
763
+ when /axe/
764
+ get_skill_total("BODY", "Melee Combat", "Axe")
765
+ when /mace|club|hammer/
766
+ get_skill_total("BODY", "Melee Combat", "Club")
767
+ when /spear|polearm/
768
+ get_skill_total("BODY", "Melee Combat", "Spear")
769
+ when /staff/
770
+ get_skill_total("BODY", "Melee Combat", "Staff")
771
+ when /dagger|knife/
772
+ get_skill_total("BODY", "Melee Combat", "Dagger")
773
+ when /unarmed/
774
+ get_skill_total("BODY", "Melee Combat", "Unarmed")
775
+ else
776
+ get_skill_total("BODY", "Melee Combat", "Sword") || 0
777
+ end
778
+ end
779
+
780
+ def calculate_missile_skill_for_name(weapon_name)
781
+ case weapon_name.to_s.downcase
782
+ when /bow/
783
+ get_skill_total("BODY", "Missile Combat", "Bow")
784
+ when /crossbow|x-bow/
785
+ get_skill_total("BODY", "Missile Combat", "Crossbow")
786
+ when /sling/
787
+ get_skill_total("BODY", "Missile Combat", "Sling")
788
+ when /javelin/
789
+ get_skill_total("BODY", "Missile Combat", "Javelin")
790
+ when /rock|stone|throwing|th\s|knife/
791
+ get_skill_total("BODY", "Missile Combat", "Throwing")
792
+ else
793
+ get_skill_total("BODY", "Missile Combat", "Bow") || 0
794
+ end
795
+ end
796
+
797
+ def select_best_weapon(skills, preferred_weapons)
798
+ # Find the best weapon from preferred list that character has skill in
799
+ best_weapon = nil
800
+ best_skill = 0
801
+
802
+ preferred_weapons.each do |weapon|
803
+ if skills[weapon] && skills[weapon] > best_skill
804
+ best_weapon = weapon
805
+ best_skill = skills[weapon]
806
+ end
807
+ end
808
+
809
+ # If no preferred weapon found, check for any weapon skill
810
+ if !best_weapon && skills.any?
811
+ # Filter out Shield as it's not a primary weapon
812
+ weapon_skills = skills.reject { |k, _| k == "Shield" }
813
+ if weapon_skills.any?
814
+ best = weapon_skills.max_by { |_, v| v }
815
+ best_weapon = best[0]
816
+ end
817
+ end
818
+
819
+ best_weapon
820
+ end
821
+
822
+ def has_template_magic?(template)
823
+ # Check if template indicates magical ability
824
+ return false unless template
825
+
826
+ # Check if template has Casting attribute > 0
827
+ casting_attr = template["attributes"]["SPIRIT/Casting"] || 0
828
+ return true if casting_attr > 0
829
+
830
+ # Check for specific magic-using types
831
+ magic_types = ["Mage", "Wizard", "Witch (white)", "Witch (black)", "Sorcerer",
832
+ "Summoner", "Priest", "Sage", "Seer"]
833
+ type_str = @type.to_s
834
+ magic_types.include?(type_str) || type_str.include?("Wizard")
835
+ end
836
+
837
+ # Moved to public section below
838
+
839
+ def generate_spells
840
+ # Generate spell cards based on character type and level
841
+ spirit = get_characteristic("SPIRIT")
842
+ casting_attr = get_attribute("SPIRIT", "Casting")
843
+ casting_total = spirit + casting_attr
844
+
845
+ # Restrict spells to appropriate creatures
846
+ # Require SPIRIT >= 2 AND total Casting >= 5
847
+ # Exclude specific races and creature types
848
+ excluded_types = ["Araxi", "Troll", "Dwarf", "Ogre", "Lizard", "Animal", "Zombie", "Skeleton"]
849
+ type_lower = @type.to_s.downcase
850
+
851
+ is_excluded = excluded_types.any? { |ex| type_lower.include?(ex.downcase) }
852
+
853
+ # Only generate spells for intelligent magical beings
854
+ if spirit >= 2 && casting_total >= 5 && !is_excluded
855
+ # Load spell database if not already loaded
856
+ unless defined?($SpellDatabase)
857
+ load File.join($pgmdir, "includes/tables/spells_new.rb")
858
+ end
859
+
860
+ @spells = generate_spell_cards(@type, @level, casting_total)
861
+ end
862
+ end
863
+
864
+ # Dice rolling methods (matching old system)
865
+ def d6
866
+ rand(1..6)
867
+ end
868
+
869
+ def oD6
870
+ # Open-ended D6 roll
871
+ result = d6
872
+ return result if (2..5).include?(result)
873
+
874
+ if result == 1
875
+ down = d6
876
+ while down <= 3
877
+ result -= 1
878
+ down = d6
879
+ end
880
+ elsif result == 6
881
+ up = d6
882
+ while up >= 4
883
+ result += 1
884
+ up = d6
885
+ end
886
+ end
887
+ result
888
+ end
889
+
890
+ def aD6
891
+ # Average D6 roll (average of regular d6 and open-ended d6)
892
+ ((d6 + oD6) / 2.0).to_i
893
+ end
894
+
895
+ # Public methods for accessing tier data
896
+ public
897
+
898
+ def has_magic?
899
+ # Check if character has any casting ability
900
+ casting_level = @tiers["SPIRIT"]["Casting"]["level"] || 0
901
+ casting_level > 0
902
+ end
903
+
904
+ # Calculate derived stats
905
+ def SIZE
906
+ # SIZE system based on weight with half-sizes
907
+ case @weight
908
+ when 0...10 then 0.5
909
+ when 10...15 then 1
910
+ when 15...20 then 1.5
911
+ when 20...35 then 2
912
+ when 35...50 then 2.5
913
+ when 50...75 then 3
914
+ when 75...100 then 3.5
915
+ when 100...125 then 4
916
+ when 125...150 then 4.5
917
+ when 150...188 then 5
918
+ when 188...225 then 5.5
919
+ when 225...263 then 6
920
+ when 263...300 then 6.5
921
+ when 300...350 then 7
922
+ when 350...400 then 7.5
923
+ when 400...450 then 8
924
+ when 450...500 then 8.5
925
+ when 500...550 then 9
926
+ when 550...600 then 9.5
927
+ when 600...663 then 10
928
+ when 663...725 then 10.5
929
+ when 725...788 then 11
930
+ when 788...850 then 11.5
931
+ when 850...925 then 12
932
+ when 925...1000 then 12.5
933
+ when 1000...1075 then 13
934
+ when 1075...1150 then 13.5
935
+ when 1150...1225 then 14
936
+ when 1225...1300 then 14.5
937
+ when 1300...1375 then 15
938
+ when 1375...1450 then 15.5
939
+ when 1450...1525 then 16
940
+ when 1525...1600 then 16.5
941
+ else
942
+ # For very large creatures, add 0.5 per 100kg
943
+ 16.5 + ((@weight - 1600) / 100.0).floor * 0.5
944
+ end
945
+ end
946
+
947
+ def BP
948
+ # Body Points: SIZE * 2 + Fortitude / 3
949
+ # Fortitude is under Endurance
950
+ fortitude = get_skill("BODY", "Endurance", "Fortitude")
951
+ endurance = get_attribute("BODY", "Endurance")
952
+ body = get_characteristic("BODY")
953
+ total_fortitude = body + endurance + fortitude
954
+ (self.SIZE * 2 + total_fortitude / 3.0).round
955
+ end
956
+
957
+ def DB
958
+ # Damage Bonus: (SIZE + Strength total) / 3
959
+ strength = get_attribute("BODY", "Strength")
960
+ body = get_characteristic("BODY")
961
+ total_strength = body + strength
962
+ ((self.SIZE + total_strength) / 3.0).round
963
+ end
964
+
965
+ def MD
966
+ # Magic Defense: (Mental Fortitude + Attunement Self) / 3
967
+ mental_fortitude = get_skill("MIND", "Willpower", "Mental Fortitude")
968
+ willpower = get_attribute("MIND", "Willpower")
969
+ mind = get_characteristic("MIND")
970
+ total_mental_fortitude = mind + willpower + mental_fortitude
971
+
972
+ attunement_self = get_skill("SPIRIT", "Attunement", "Self")
973
+ attunement = get_attribute("SPIRIT", "Attunement")
974
+ spirit = get_characteristic("SPIRIT")
975
+ total_attunement_self = spirit + attunement + attunement_self
976
+
977
+ ((total_mental_fortitude + total_attunement_self) / 3.0).round
978
+ end
979
+
980
+ def get_characteristic(name)
981
+ # Calculate characteristic as weighted average of its attributes
982
+ # Reflects years of broad training across all aspects
983
+ total = 0
984
+ count = 0
985
+ @tiers[name].each do |attr_name, attr_data|
986
+ if attr_data["level"] && attr_data["level"] > 0
987
+ total += attr_data["level"]
988
+ count += 1
989
+ end
990
+ end
991
+
992
+ return 0 if count == 0
993
+
994
+ # Characteristics are very hard to improve - most people stay at 2-3
995
+ char_level = (total.to_f / count / 1.5).round # Scaled down to reflect training difficulty
996
+
997
+ # Apply realistic caps based on NPC level
998
+ max_char = case @level
999
+ when 1..2 then 2 # Novices rarely exceed 2
1000
+ when 3..4 then 3 # Experienced typically max at 3
1001
+ when 5..6 then 4 # Veterans might reach 4
1002
+ else 5 # Only masters achieve 5
1003
+ end
1004
+
1005
+ char_level = max_char if char_level > max_char
1006
+ char_level
1007
+ end
1008
+
1009
+ def get_attribute(char_name, attr_name)
1010
+ @tiers[char_name][attr_name]["level"] || 0
1011
+ end
1012
+
1013
+ def get_skill(char_name, attr_name, skill_name)
1014
+ @tiers[char_name][attr_name]["skills"][skill_name] || 0
1015
+ end
1016
+
1017
+ def get_skill_total(char_name, attr_name, skill_name)
1018
+ # Calculate total: Characteristic + Attribute + Skill
1019
+ char_level = get_characteristic(char_name)
1020
+ attr_level = get_attribute(char_name, attr_name)
1021
+ skill_level = get_skill(char_name, attr_name, skill_name)
1022
+
1023
+ # Natural progression based on training difficulty:
1024
+ # - Getting to 10 is achievable with focused skill training
1025
+ # - Getting to 15 requires years of dedicated work
1026
+ # - Getting to 18+ is legendary, requiring lifetime mastery
1027
+ total = char_level + attr_level + skill_level
1028
+
1029
+ # Apply soft cap at 18 for game balance (only true masters exceed)
1030
+ if total > 18 && @level < 7
1031
+ # Small chance for exceptional individuals to exceed 18
1032
+ total = 18 unless rand(100) < 5
1033
+ end
1034
+
1035
+ total
1036
+ end
1037
+
1038
+ # Mark system for progression
1039
+
1040
+ def add_mark(char_name, attr_name, skill_name = nil)
1041
+ if skill_name
1042
+ # Add mark to skill
1043
+ key = "#{attr_name}/#{skill_name}"
1044
+ @marks[char_name][key] ||= 0
1045
+ @marks[char_name][key] += 1
1046
+
1047
+ # Check if ready to advance
1048
+ check_advancement(char_name, attr_name, skill_name)
1049
+ else
1050
+ # Add mark to attribute
1051
+ @marks[char_name][attr_name] ||= 0
1052
+ @marks[char_name][attr_name] += 1
1053
+ end
1054
+ end
1055
+
1056
+ def check_advancement(char_name, attr_name, skill_name)
1057
+ current_level = get_skill(char_name, attr_name, skill_name)
1058
+ required_marks = (current_level + 1) * 5
1059
+ key = "#{attr_name}/#{skill_name}"
1060
+
1061
+ if @marks[char_name][key] >= required_marks
1062
+ # Roll for advancement (all but a 1)
1063
+ if oD6 > 1
1064
+ @tiers[char_name][attr_name]["skills"][skill_name] += 1
1065
+ @marks[char_name][key] = 0
1066
+
1067
+ # Add mark to attribute above
1068
+ add_mark(char_name, attr_name)
1069
+
1070
+ return true
1071
+ end
1072
+ end
1073
+ false
1074
+ end
1075
+
1076
+ private
1077
+
1078
+ def generate_random_name(sex)
1079
+ # Use the proper name generator based on race
1080
+ race = @type.to_s.sub(/(:| ).*/, '').capitalize
1081
+
1082
+ # Use naming function from functions.rb which uses the actual name generator
1083
+ name = naming(race, sex)
1084
+
1085
+ # Fallback to basic names if name generator fails
1086
+ if name.nil? || name.empty?
1087
+ if sex == "F" || (sex.empty? && rand(2) == 0)
1088
+ female_names = ["Aria", "Luna", "Sera", "Mira", "Lyra", "Nova", "Kira", "Zara", "Elara", "Thalia"]
1089
+ name = female_names.sample + " " + ["Starweaver", "Moonwhisper", "Brightblade", "Swiftwind", "Ironheart"].sample
1090
+ else
1091
+ male_names = ["Gareth", "Marcus", "Aldric", "Kael", "Doran", "Lucian", "Theron", "Cassius", "Orion", "Zephyr"]
1092
+ name = male_names.sample + " " + ["Ironforge", "Stormcaller", "Darkbane", "Goldhand", "Steelclaw"].sample
1093
+ end
1094
+ end
1095
+
1096
+ name
1097
+ end
1098
+
1099
+ def ensure_essential_skills
1100
+ # Ensure all NPCs have essential combat/stealth awareness skills (even if at 0)
1101
+ # These skills are frequently used in encounters
1102
+
1103
+ # Ensure Athletics attribute exists with essential skills
1104
+ @tiers["BODY"]["Athletics"] ||= {"level" => 0, "skills" => {}}
1105
+ @tiers["BODY"]["Athletics"]["skills"] ||= {}
1106
+
1107
+ # Add Move Quietly if missing
1108
+ unless @tiers["BODY"]["Athletics"]["skills"].key?("Move Quietly")
1109
+ @tiers["BODY"]["Athletics"]["skills"]["Move Quietly"] = 0
1110
+ end
1111
+
1112
+ # Add Hide if missing
1113
+ unless @tiers["BODY"]["Athletics"]["skills"].key?("Hide")
1114
+ @tiers["BODY"]["Athletics"]["skills"]["Hide"] = 0
1115
+ end
1116
+
1117
+ # Add Dodge if missing (often used in combat)
1118
+ unless @tiers["BODY"]["Athletics"]["skills"].key?("Dodge")
1119
+ @tiers["BODY"]["Athletics"]["skills"]["Dodge"] = 0
1120
+ end
1121
+
1122
+ # Ensure Awareness attribute exists with essential skills
1123
+ @tiers["MIND"]["Awareness"] ||= {"level" => 0, "skills" => {}}
1124
+ @tiers["MIND"]["Awareness"]["skills"] ||= {}
1125
+
1126
+ # Add Alertness if missing (this is the general awareness skill)
1127
+ unless @tiers["MIND"]["Awareness"]["skills"].key?("Alertness")
1128
+ @tiers["MIND"]["Awareness"]["skills"]["Alertness"] = 0
1129
+ end
1130
+
1131
+ # Reaction speed should always be present for initiative calculations
1132
+ unless @tiers["MIND"]["Awareness"]["skills"].key?("Reaction speed")
1133
+ @tiers["MIND"]["Awareness"]["skills"]["Reaction speed"] = 0
1134
+ end
1135
+ end
1136
+
1137
+ def apply_predetermined_stats
1138
+ # Apply predetermined stats to preserve encounter consistency
1139
+ # This allows encounters to pass specific stats that should not be randomized
1140
+
1141
+ return unless @predetermined_stats.is_a?(Hash)
1142
+
1143
+ # Override characteristics if provided
1144
+ if @predetermined_stats["characteristics"]
1145
+ @predetermined_stats["characteristics"].each do |char_name, value|
1146
+ # This would require modifying the SIZE calculation system
1147
+ # For now, we'll focus on weapons/armor/skills
1148
+ end
1149
+ end
1150
+
1151
+ # Override specific weapon skills if provided
1152
+ if @predetermined_stats["weapon_skills"]
1153
+ @predetermined_stats["weapon_skills"].each do |weapon, skill_level|
1154
+ # Add to melee combat skills
1155
+ @tiers["BODY"]["Melee Combat"]["skills"] ||= {}
1156
+ @tiers["BODY"]["Melee Combat"]["skills"][weapon] = skill_level.to_i
1157
+ end
1158
+ end
1159
+
1160
+ # Override missile skills if provided
1161
+ if @predetermined_stats["missile_skills"]
1162
+ @predetermined_stats["missile_skills"].each do |weapon, skill_level|
1163
+ @tiers["BODY"]["Missile Combat"]["skills"] ||= {}
1164
+ @tiers["BODY"]["Missile Combat"]["skills"][weapon] = skill_level.to_i
1165
+ end
1166
+ end
1167
+
1168
+ # Override armor if provided
1169
+ if @predetermined_stats["armor"]
1170
+ @armor = @predetermined_stats["armor"]
1171
+ end
1172
+
1173
+ # Override other skills if provided
1174
+ if @predetermined_stats["skills"]
1175
+ @predetermined_stats["skills"].each do |skill_path, value|
1176
+ parts = skill_path.split("/")
1177
+ next unless parts.length == 3
1178
+
1179
+ char_name, attr_name, skill_name = parts
1180
+ next unless @tiers[char_name] && @tiers[char_name][attr_name]
1181
+
1182
+ @tiers[char_name][attr_name]["skills"] ||= {}
1183
+ @tiers[char_name][attr_name]["skills"][skill_name] = value.to_i
1184
+ end
1185
+ end
1186
+ end
1187
+ end