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,551 @@
1
+ # Monster/Animal NPC class for the new 3-tier system
2
+ # This creates simplified NPCs for non-humanoid encounters
3
+
4
+ class MonsterNew
5
+ attr_accessor :name, :type, :level, :sex, :description, :weight
6
+ attr_accessor :SIZE, :BP, :DB, :MD, :ENC
7
+ attr_accessor :tiers, :armor, :special_abilities, :spells
8
+
9
+ def initialize(monster_type, level = 1)
10
+ @type = monster_type
11
+ @level = level
12
+ @sex = ["M", "F"].sample # Even monsters have gender sometimes
13
+
14
+ # Load monster stats if not loaded
15
+ unless defined?($MonsterStats)
16
+ load File.join($pgmdir, "includes/tables/monster_stats_new.rb")
17
+ end
18
+
19
+ # Get stats template
20
+ stats = get_monster_stats(@type)
21
+
22
+ # Set name based on type
23
+ @name = @type.gsub(/Monster:|Animal:|Small |Large /, "").strip.capitalize
24
+ @description = stats["description"]
25
+ @special_abilities = stats["special"]
26
+
27
+ # Generate weight and SIZE
28
+ if stats["weight_range"]
29
+ # New system: use weight range and calculate SIZE
30
+ @weight = rand(stats["weight_range"][0]..stats["weight_range"][1])
31
+ @SIZE = calculate_size_from_weight(@weight)
32
+ elsif stats["size_range"]
33
+ # Legacy system: convert old SIZE to approximate weight
34
+ old_size = rand(stats["size_range"][0]..stats["size_range"][1])
35
+ # Convert old SIZE to weight (rough approximation)
36
+ @weight = size_to_weight(old_size)
37
+ @SIZE = calculate_size_from_weight(@weight)
38
+ else
39
+ # Default human-like range centered around 70kg
40
+ # Using a more realistic distribution
41
+ base_weight = 60
42
+ variation = rand(-10..20) # Slightly skewed toward heavier
43
+ @weight = base_weight + variation + rand(10)
44
+ @weight = [@weight, 40].max # Minimum weight
45
+ @SIZE = calculate_size_from_weight(@weight)
46
+ end
47
+
48
+ # Initialize tiers with simplified structure for monsters
49
+ @tiers = {
50
+ "BODY" => {
51
+ "level" => stats["base_body"] + (@level / 2),
52
+ "Strength" => { "level" => stats["base_body"] + rand(0..1) },
53
+ "Endurance" => { "level" => stats["base_body"] + rand(0..1) },
54
+ "Melee Combat" => {
55
+ "level" => stats["base_body"],
56
+ "skills" => generate_combat_skills(stats["skills"])
57
+ },
58
+ "Missile Combat" => {
59
+ "level" => 0,
60
+ "skills" => {}
61
+ },
62
+ "Athletics" => {
63
+ "level" => stats["base_body"] + rand(-1..1),
64
+ "skills" => generate_athletics_skills(stats)
65
+ }
66
+ },
67
+ "MIND" => {
68
+ "level" => stats["base_mind"] + (@level / 3),
69
+ "Intelligence" => { "level" => stats["base_mind"] },
70
+ "Awareness" => {
71
+ "level" => stats["base_mind"] + rand(0..1),
72
+ "skills" => generate_awareness_skills(stats)
73
+ }
74
+ },
75
+ "SPIRIT" => {
76
+ "level" => stats["base_spirit"],
77
+ "Casting" => { "level" => stats["base_spirit"] > 0 ? rand(1..3) : 0 },
78
+ "Attunement" => { "level" => stats["base_spirit"] > 0 ? rand(1..2) : 0, "skills" => {} }
79
+ }
80
+ }
81
+
82
+ # Add magical abilities ONLY for truly magical creatures (not animals)
83
+ if stats["base_spirit"] > 0 && !@type.match?(/animal:/i)
84
+ # Dragons get Earth magic, other magical creatures get varied
85
+ if @type =~ /dragon/i
86
+ @tiers["SPIRIT"]["Attunement"]["skills"]["Earth"] = @level + rand(2..4)
87
+ elsif @type =~ /drake/i
88
+ @tiers["SPIRIT"]["Attunement"]["skills"]["Fire"] = @level + rand(1..3)
89
+ elsif @type =~ /elemental/i
90
+ element = ["Fire", "Water", "Air", "Earth"].sample
91
+ @tiers["SPIRIT"]["Attunement"]["skills"][element] = @level + rand(2..4)
92
+ elsif @type =~ /undead|zombie|skeleton|lich/i
93
+ @tiers["SPIRIT"]["Attunement"]["skills"]["Death"] = @level + rand(1..3)
94
+ elsif @type =~ /vampire|werewolf|faerie|wyvern/i
95
+ # Specific magical monsters
96
+ domain = ["Fire", "Water", "Air", "Earth", "Life", "Death", "Mind"].sample
97
+ @tiers["SPIRIT"]["Attunement"]["skills"][domain] = @level + rand(1..2)
98
+ end
99
+ # Do NOT give magic to generic creatures or animals
100
+ end
101
+
102
+ # Calculate derived stats
103
+ calculate_derived_stats
104
+
105
+ # Generate spells for magical creatures (not animals)
106
+ generate_spells if stats["base_spirit"] > 0 && !@type.match?(/animal:/i)
107
+
108
+ # Set armor
109
+ set_armor(stats["armor"])
110
+ end
111
+
112
+ private
113
+
114
+ def size_to_weight(old_size)
115
+ # Convert old SIZE system to approximate weight for backward compatibility
116
+ # Old system: SIZE 8-10 for humans, SIZE 15-20 for dragons
117
+ case old_size
118
+ when 0..1 then rand(5..15)
119
+ when 2 then rand(15..30)
120
+ when 3 then rand(30..60)
121
+ when 4 then rand(60..90)
122
+ when 5 then rand(90..135)
123
+ when 6 then rand(135..180)
124
+ when 7 then rand(180..240)
125
+ when 8 then rand(240..300)
126
+ when 9 then rand(300..375)
127
+ when 10 then rand(375..450)
128
+ when 11 then rand(450..525)
129
+ when 12 then rand(525..600)
130
+ when 13 then rand(600..700)
131
+ when 14 then rand(700..800)
132
+ when 15 then rand(800..900)
133
+ when 16 then rand(900..1000)
134
+ when 17 then rand(1000..1200)
135
+ when 18 then rand(1200..1400)
136
+ when 19 then rand(1400..1600)
137
+ when 20 then rand(1600..1800)
138
+ else old_size * 90 # Rough approximation for very large creatures
139
+ end
140
+ end
141
+
142
+ def calculate_size_from_weight(weight)
143
+ # SIZE system based on weight with half-sizes - matches NPC class
144
+ case weight
145
+ when 0...10 then 0.5
146
+ when 10...15 then 1
147
+ when 15...20 then 1.5
148
+ when 20...35 then 2
149
+ when 35...50 then 2.5
150
+ when 50...75 then 3
151
+ when 75...100 then 3.5
152
+ when 100...125 then 4
153
+ when 125...150 then 4.5
154
+ when 150...188 then 5
155
+ when 188...225 then 5.5
156
+ when 225...263 then 6
157
+ when 263...300 then 6.5
158
+ when 300...350 then 7
159
+ when 350...400 then 7.5
160
+ when 400...450 then 8
161
+ when 450...500 then 8.5
162
+ when 500...550 then 9
163
+ when 550...600 then 9.5
164
+ when 600...663 then 10
165
+ when 663...725 then 10.5
166
+ when 725...788 then 11
167
+ when 788...850 then 11.5
168
+ when 850...925 then 12
169
+ when 925...1000 then 12.5
170
+ when 1000...1075 then 13
171
+ when 1075...1150 then 13.5
172
+ when 1150...1225 then 14
173
+ when 1225...1300 then 14.5
174
+ when 1300...1375 then 15
175
+ when 1375...1450 then 15.5
176
+ when 1450...1525 then 16
177
+ when 1525...1600 then 16.5
178
+ else
179
+ # For very large creatures, add 0.5 per 100kg
180
+ 16.5 + ((weight - 1600) / 100.0).floor * 0.5
181
+ end
182
+ end
183
+
184
+ def generate_combat_skills(skill_list)
185
+ skills = {}
186
+
187
+ skill_list.each do |skill_name|
188
+ case skill_name.downcase
189
+ when /bite/
190
+ skills["Bite"] = @level + rand(2..4) # Natural attacks are effective
191
+ when /claw/
192
+ skills["Claw"] = @level + rand(2..4)
193
+ when /tusk/
194
+ skills["Tusk"] = @level + rand(1..3)
195
+ when /tail/
196
+ skills["Tail"] = @level + rand(1..3)
197
+ when /sword/
198
+ skills["Sword"] = @level + rand(1..3)
199
+ when /spear/
200
+ skills["Spear"] = @level + rand(1..3)
201
+ when /dagger/
202
+ skills["Dagger"] = @level + rand(0..2)
203
+ when /club/
204
+ skills["Club"] = @level + rand(0..2)
205
+ when /unarmed/
206
+ skills["Unarmed"] = @level + rand(2..4)
207
+ when /grappl/
208
+ skills["Grappling"] = @level + rand(1..3)
209
+ end
210
+ end
211
+
212
+ # Ensure at least one combat skill with a reasonable level
213
+ if skills.empty?
214
+ skills["Unarmed"] = @level + rand(2..3)
215
+ end
216
+
217
+ skills
218
+ end
219
+
220
+ def generate_athletics_skills(stats)
221
+ skills = {}
222
+
223
+ # Base athletics skills appropriate for all creatures
224
+ skills["Dodge"] = @level + rand(0..2)
225
+
226
+ # Movement skills based on creature type
227
+ case stats["type"]
228
+ when "Undead"
229
+ # Undead don't swim or climb well, but can move quietly
230
+ skills["Move quietly"] = @level + rand(0..2)
231
+ skills["Hide"] = @level + rand(-1..1)
232
+ # Skeletons and zombies are slow
233
+ if @name.downcase.include?("skeleton") || @name.downcase.include?("zombie")
234
+ skills["Running"] = rand(0..1) # Very slow
235
+ else
236
+ skills["Running"] = @level + rand(-1..1)
237
+ end
238
+ when "Animal"
239
+ # Animals have natural athletics
240
+ skills["Running"] = @level + rand(0..2)
241
+ skills["Jumping"] = @level + rand(0..1)
242
+ # Aquatic animals can swim
243
+ if @name.downcase.include?("fish") || @name.downcase.include?("shark")
244
+ skills["Swimming"] = @level + rand(2..4)
245
+ elsif @name.downcase.include?("bear") || @name.downcase.include?("dog")
246
+ skills["Swimming"] = rand(1..2)
247
+ end
248
+ # Climbing for appropriate animals
249
+ if @name.downcase.include?("bear") || @name.downcase.include?("monkey")
250
+ skills["Climbing"] = @level + rand(1..3)
251
+ end
252
+ # Stealth for predators
253
+ if stats["skills"].any? { |s| s.downcase.include?("stealth") || s.downcase.include?("ambush") }
254
+ skills["Move quietly"] = @level + rand(1..3)
255
+ skills["Hide"] = @level + rand(0..2)
256
+ end
257
+ when "Monster"
258
+ # Most monsters have standard athletics
259
+ skills["Running"] = @level + rand(-1..2)
260
+ # Flying creatures
261
+ if stats["skills"].any? { |s| s.downcase.include?("flight") || s.downcase.include?("fly") }
262
+ skills["Flying"] = @level + rand(2..4)
263
+ end
264
+ # Stealthy monsters
265
+ if @name.downcase.include?("goblin") || stats["skills"].include?("Stealth")
266
+ skills["Move quietly"] = @level + rand(1..3)
267
+ skills["Hide"] = @level + rand(1..3)
268
+ end
269
+ # Large monsters don't hide well
270
+ if @weight > 500
271
+ skills["Hide"] = rand(0..1) if skills["Hide"]
272
+ skills["Move quietly"] = rand(0..1) if skills["Move quietly"]
273
+ end
274
+ else
275
+ # Default fallback
276
+ skills["Running"] = @level + rand(-1..1)
277
+ end
278
+
279
+ # Remove any skills with 0 or negative values
280
+ skills.delete_if { |_, v| v <= 0 }
281
+
282
+ skills
283
+ end
284
+
285
+ def calculate_derived_stats
286
+ body = @tiers["BODY"]["level"]
287
+ mind = @tiers["MIND"]["level"]
288
+ spirit = @tiers["SPIRIT"]["level"]
289
+
290
+ # Body Points: SIZE * 2 + Fortitude / 3 (same as NPC class)
291
+ fortitude = get_skill_total("BODY", "Endurance", "Fortitude")
292
+ @BP = (@SIZE * 2 + fortitude / 3.0).round
293
+
294
+ # Damage Bonus: (SIZE + Wield weapon) / 3 (same as NPC class)
295
+ wield_weapon = get_skill_total("BODY", "Strength", "Wield weapon")
296
+ @DB = ((@SIZE + wield_weapon) / 3.0).round
297
+
298
+ # Magic Defense: (Mental fortitude + Attunement/Self) / 3 (same as NPC class)
299
+ mental_fortitude = get_skill_total("MIND", "Learning", "Mental fortitude")
300
+ attunement_self = get_skill_total("SPIRIT", "Magic", "Attunement/Self")
301
+ @MD = ((mental_fortitude + attunement_self) / 3.0).round
302
+
303
+ # Encumbrance (monsters don't carry much)
304
+ @ENC = 0
305
+ end
306
+
307
+ def set_armor(armor_desc)
308
+ return unless armor_desc
309
+
310
+ if armor_desc =~ /Natural \((\d+) AP\)/
311
+ @armor = {
312
+ name: "Natural armor",
313
+ ap: $1.to_i,
314
+ enc: 0
315
+ }
316
+ else
317
+ @armor = {
318
+ name: armor_desc,
319
+ ap: 1,
320
+ enc: 0
321
+ }
322
+ end
323
+ end
324
+
325
+ def generate_spells
326
+ # Load spell tables if not loaded
327
+ unless defined?($SpellDatabase)
328
+ load File.join($pgmdir, "includes/tables/spells_new.rb")
329
+ end
330
+
331
+ @spells = []
332
+
333
+ # Determine primary domain
334
+ domain = nil
335
+ highest_skill = 0
336
+
337
+ if @tiers["SPIRIT"]["Attunement"]["skills"]
338
+ @tiers["SPIRIT"]["Attunement"]["skills"].each do |dom, skill|
339
+ if skill > highest_skill
340
+ domain = dom
341
+ highest_skill = skill
342
+ end
343
+ end
344
+ end
345
+
346
+ return unless domain
347
+
348
+ # Calculate spell count based on level and domain skill
349
+ spell_count = (@level + highest_skill) / 2
350
+ spell_count = [spell_count, 1].max
351
+ spell_count = [spell_count, 20].min # Cap at 20 spells
352
+
353
+ # Dragons get more spells
354
+ spell_count = (@level * 2) if @type =~ /dragon/i
355
+ spell_count = [spell_count, 30].min if @type =~ /dragon/i
356
+
357
+ # Get spells from the appropriate domain
358
+ available_spells = []
359
+
360
+ # Filter spells by domain from $SpellDatabase
361
+ $SpellDatabase.each do |spell_name, spell_data|
362
+ if spell_data["domain"] == domain
363
+ available_spells << {
364
+ 'name' => spell_name,
365
+ 'duration' => spell_data["duration"],
366
+ 'range' => spell_data["distance"],
367
+ 'area' => spell_data["aoe"]
368
+ }
369
+ end
370
+ end
371
+
372
+ # If no spells found for domain, use some generic ones
373
+ if available_spells.empty?
374
+ # Create some basic spells for the domain
375
+ case domain
376
+ when "Earth"
377
+ available_spells = [
378
+ {'name' => "Stone Skin", 'duration' => "1 hour", 'range' => "Touch", 'area' => "Single"},
379
+ {'name' => "Earth Spike", 'duration' => "Instant", 'range' => "20m", 'area' => "Single"},
380
+ {'name' => "Tremor", 'duration' => "Instant", 'range' => "50m", 'area' => "10m radius"},
381
+ {'name' => "Stone Wall", 'duration' => "Permanent", 'range' => "10m", 'area' => "Wall 5m"},
382
+ {'name' => "Earth Bind", 'duration' => "1 minute", 'range' => "30m", 'area' => "Single"},
383
+ {'name' => "Quake", 'duration' => "Instant", 'range' => "100m", 'area' => "20m radius"}
384
+ ]
385
+ when "Fire"
386
+ available_spells = [
387
+ {'name' => "Flame Bolt", 'duration' => "Instant", 'range' => "30m", 'area' => "Single"},
388
+ {'name' => "Fire Shield", 'duration' => "10 minutes", 'range' => "Self", 'area' => "Self"},
389
+ {'name' => "Fireball", 'duration' => "Instant", 'range' => "50m", 'area' => "5m radius"}
390
+ ]
391
+ when "Water"
392
+ available_spells = [
393
+ {'name' => "Ice Bolt", 'duration' => "Instant", 'range' => "30m", 'area' => "Single"},
394
+ {'name' => "Water Walk", 'duration' => "1 hour", 'range' => "Touch", 'area' => "Single"},
395
+ {'name' => "Freeze", 'duration' => "1 minute", 'range' => "20m", 'area' => "Single"}
396
+ ]
397
+ when "Air"
398
+ available_spells = [
399
+ {'name' => "Lightning", 'duration' => "Instant", 'range' => "50m", 'area' => "Single"},
400
+ {'name' => "Wind Walk", 'duration' => "10 minutes", 'range' => "Touch", 'area' => "Single"},
401
+ {'name' => "Storm", 'duration' => "10 minutes", 'range' => "100m", 'area' => "50m radius"}
402
+ ]
403
+ when "Death"
404
+ available_spells = [
405
+ {'name' => "Drain Life", 'duration' => "Instant", 'range' => "Touch", 'area' => "Single"},
406
+ {'name' => "Animate Dead", 'duration' => "1 hour", 'range' => "10m", 'area' => "Corpse"},
407
+ {'name' => "Death Touch", 'duration' => "Instant", 'range' => "Touch", 'area' => "Single"}
408
+ ]
409
+ else
410
+ available_spells = [
411
+ {'name' => "Magic Bolt", 'duration' => "Instant", 'range' => "30m", 'area' => "Single"}
412
+ ]
413
+ end
414
+ end
415
+
416
+ # Select random spells
417
+ selected_spells = available_spells.sample([spell_count, available_spells.length].min)
418
+
419
+ selected_spells.each do |spell|
420
+ @spells << {
421
+ 'name' => spell['name'],
422
+ 'domain' => domain,
423
+ 'duration' => spell['duration'] || "Instant",
424
+ 'range' => spell['range'] || "Touch",
425
+ 'area' => spell['area'] || "Target"
426
+ }
427
+ end
428
+ end
429
+
430
+ public
431
+
432
+ # Compatibility methods for encounter system
433
+ def get_characteristic(char_name)
434
+ @tiers[char_name]["level"] || 0
435
+ end
436
+
437
+ def get_attribute(char_name, attr_name)
438
+ return 0 unless @tiers[char_name] && @tiers[char_name][attr_name]
439
+ @tiers[char_name][attr_name]["level"] || 0
440
+ end
441
+
442
+ def get_skill(char_name, attr_name, skill_name)
443
+ return 0 unless @tiers[char_name] && @tiers[char_name][attr_name]
444
+ return 0 unless @tiers[char_name][attr_name]["skills"]
445
+ @tiers[char_name][attr_name]["skills"][skill_name] || 0
446
+ end
447
+
448
+
449
+ def age
450
+ "Unknown"
451
+ end
452
+
453
+ def height
454
+ @SIZE * 20 # Rough estimate
455
+ end
456
+
457
+ def weight
458
+ @SIZE * 10 # Rough estimate
459
+ end
460
+
461
+ def area
462
+ "Wild"
463
+ end
464
+
465
+ def has_magic?
466
+ # Check if monster has any casting ability
467
+ return false unless @tiers["SPIRIT"] && @tiers["SPIRIT"]["Casting"]
468
+ casting_level = @tiers["SPIRIT"]["Casting"]["level"] || 0
469
+ casting_level > 0
470
+ end
471
+
472
+ def get_skill_total(char_name, attr_name, skill_name)
473
+ # Calculate total: Characteristic + Attribute + Skill
474
+ char_level = get_characteristic(char_name)
475
+ attr_level = get_attribute(char_name, attr_name)
476
+ skill_level = get_skill(char_name, attr_name, skill_name)
477
+
478
+ char_level + attr_level + skill_level
479
+ end
480
+
481
+ # Additional methods for compatibility with detailed views
482
+ def profession
483
+ @type.to_s.gsub(/Monster:|Animal:/, "")
484
+ end
485
+
486
+ def cult
487
+ nil # Monsters don't have religions
488
+ end
489
+
490
+ def social_status
491
+ "Wild" # Monsters are outside society
492
+ end
493
+
494
+ def money
495
+ 0 # Monsters don't carry money
496
+ end
497
+
498
+ def equipment
499
+ [] # Monsters don't have equipment
500
+ end
501
+
502
+ def generate_awareness_skills(stats)
503
+ skills = {}
504
+
505
+ # Base awareness skills
506
+ skills["Reaction speed"] = @level + rand(0..2)
507
+ skills["Alertness"] = @level + rand(0..2)
508
+
509
+ case stats["type"]
510
+ when "Undead"
511
+ # Undead have poor awareness but don't get surprised
512
+ skills["Alertness"] = rand(1..2)
513
+ skills["Search"] = rand(0..1)
514
+ # No tracking or sense ambush for mindless undead
515
+ unless @name.downcase.include?("vampire") || @name.downcase.include?("lich")
516
+ skills.delete("Reaction speed")
517
+ end
518
+ when "Animal"
519
+ # Animals have good natural senses
520
+ skills["Alertness"] = @level + rand(1..3)
521
+ # Predators track
522
+ if stats["skills"].any? { |s| s.downcase.include?("track") || s.downcase.include?("hunt") }
523
+ skills["Tracking"] = @level + rand(2..4)
524
+ end
525
+ # Pack animals sense ambush
526
+ if stats["skills"].include?("Pack tactics")
527
+ skills["Sense ambush"] = @level + rand(1..3)
528
+ end
529
+ when "Monster"
530
+ # Standard monster awareness
531
+ skills["Search"] = @level + rand(0..1)
532
+ # Intelligent monsters have better awareness
533
+ if stats["base_mind"] >= 3
534
+ skills["Sense ambush"] = @level + rand(0..2)
535
+ skills["Search"] = @level + rand(1..2)
536
+ end
537
+ # Tracking for appropriate monsters
538
+ if stats["skills"].include?("Tracking")
539
+ skills["Tracking"] = @level + rand(1..3)
540
+ end
541
+ else
542
+ # Default fallback
543
+ skills["Search"] = rand(0..1)
544
+ end
545
+
546
+ # Remove any skills with 0 or negative values
547
+ skills.delete_if { |_, v| v <= 0 }
548
+
549
+ skills
550
+ end
551
+ end