Class: Lich::Gemstone::CreatureInstance

Inherits:
Object
  • Object
show all
Includes:
Common::CreatureBase
Defined in:
documented/gemstone/creature.rb

Overview

Individual GemStone creature instance (runtime tracking with ID).

Shares its id-keyed registry, room roster and <crtrStatus> status/flag handling with DragonRealms via the Common::CreatureBase mixin; the GemStone-specific layer here adds bestiary templates, UCS (Unarmed Combat System) tracking, HP/injury modelling and the GemStone valid_target? exclusions.

Constant Summary collapse

BODY_PARTS =

Valid body locations for injury and amputation tracking.

Matches the 17 body parts exposed by limb-targeting messages and the Limb Disruption (708) spell.

See Also:

  • #amputate!
%w[abdomen back chest head leftArm leftEye leftFoot leftHand leftLeg neck nerves rightArm rightEye rightFoot rightHand rightLeg]
UCS_TTL =

UCS data expires after 2 minutes

120
UCS_SMITE_TTL =

Smite effect expires after 15 seconds

15
STUN_ROUND_SECONDS =

Seconds per stun round. Critical tables express stun in rounds (a rank-5 crit's stunned: 5 means five rounds), while their roundtime field is already in seconds - the two units live side by side in the same CritRanks hash, so never mix them.

5
COUP_INCAP_STATUSES =

Statuses that satisfy Coup de Grace's "incapacitated in some way" requirement, unlocking the (rank * 10)% threshold instead of (rank * 5)%. Positional states (prone/kneeling/sitting) are deliberately excluded.

%w[stunned immobilized webbed sleeping bound].freeze

Constants included from Common::CreatureBase

Common::CreatureBase::ALL_CRTR_FLAGS, Common::CreatureBase::CRTR_CLASSIFICATION_FLAGS, Common::CreatureBase::CRTR_STATUS_FLAGS, Common::CreatureBase::KNOWN_FLAG_NAMES, Common::CreatureBase::STATUS_DURATIONS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Common::CreatureBase

included

Constructor Details

#initialize(id, noun, name) ⇒ void

Initializes a tracked creature instance.

Sets up mutable runtime state: injury tracking per body part (initialized to 0), damage taken, UCS (Unarmed Combat System) position/tierup/smote tracking, and stun estimates from critical hits. Status tracking is delegated to the CreatureBase mixin.

Parameters:

  • id (Integer) —

    the creature's game ID (cast to integer)

  • noun (String) —

    the creature's lowercase noun form (e.g. "orc", "goblin")

  • name (String) —

    the creature's display name, possibly with boon adjectives



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'documented/gemstone/creature.rb', line 373

def initialize(id, noun, name)
  @id = id.to_i
  @noun = noun
  @name = name
  initialize_status_tracking
  @injuries = Hash.new(0)
  @health = nil
  @damage_taken = 0
  @created_at = Time.now
  @fatal_crit = false
  @ucs_position = nil
  @ucs_tierup = nil
  @ucs_smote = nil
  @ucs_updated = nil
  @amputated = []
  @stun_rounds = nil
  @stun_estimated_until = nil
end

Instance Attribute Details

#created_at ⇒ Object

Returns the value of attribute created_at.



341
342
343
# File 'documented/gemstone/creature.rb', line 341

def created_at
  @created_at
end

#damage_taken ⇒ Object

Returns the value of attribute damage_taken.



341
342
343
# File 'documented/gemstone/creature.rb', line 341

def damage_taken
  @damage_taken
end

#fatal_crit ⇒ Object

Returns the value of attribute fatal_crit.



341
342
343
# File 'documented/gemstone/creature.rb', line 341

def fatal_crit
  @fatal_crit
end

#health ⇒ Object

Returns the value of attribute health.



341
342
343
# File 'documented/gemstone/creature.rb', line 341

def health
  @health
end

#id ⇒ Object

Returns the value of attribute id.



341
342
343
# File 'documented/gemstone/creature.rb', line 341

def id
  @id
end

#injuries ⇒ Object

Returns the value of attribute injuries.



341
342
343
# File 'documented/gemstone/creature.rb', line 341

def injuries
  @injuries
end

#name ⇒ Object

Returns the value of attribute name.



341
342
343
# File 'documented/gemstone/creature.rb', line 341

def name
  @name
end

#noun ⇒ Object

Returns the value of attribute noun.



341
342
343
# File 'documented/gemstone/creature.rb', line 341

def noun
  @noun
end

#status ⇒ Object

Returns the value of attribute status.



341
342
343
# File 'documented/gemstone/creature.rb', line 341

def status
  @status
end

#status_timestamps ⇒ Object

Returns the value of attribute status_timestamps.



341
342
343
# File 'documented/gemstone/creature.rb', line 341

def status_timestamps
  @status_timestamps
end

#ucs_position ⇒ Object

Get UCS position tier (1-3, or nil if expired)



473
474
475
476
# File 'documented/gemstone/creature.rb', line 473

def ucs_position
  return nil if ucs_expired?
  @ucs_position
end

#ucs_smote ⇒ Object

Returns the value of attribute ucs_smote.



341
342
343
# File 'documented/gemstone/creature.rb', line 341

def ucs_smote
  @ucs_smote
end

#ucs_tierup ⇒ Object

Get UCS tierup vulnerability (or nil if expired)



479
480
481
482
# File 'documented/gemstone/creature.rb', line 479

def ucs_tierup
  return nil if ucs_expired?
  @ucs_tierup
end

#ucs_updated ⇒ Object

Returns the value of attribute ucs_updated.



341
342
343
# File 'documented/gemstone/creature.rb', line 341

def ucs_updated
  @ucs_updated
end

Instance Method Details

#add_damage(amount) ⇒ Object

Add damage to creature



602
603
604
# File 'documented/gemstone/creature.rb', line 602

def add_damage(amount)
  @damage_taken += amount.to_i
end

#add_injury(body_part, amount = 1) ⇒ Object

Add injury to body part



571
572
573
574
575
576
577
578
579
# File 'documented/gemstone/creature.rb', line 571

def add_injury(body_part, amount = 1)
  unless BODY_PARTS.include?(body_part.to_s)
    raise ArgumentError, "Invalid body part: #{body_part}"
  end
  # An amputated limb cannot accrue further wound rank - it is gone.
  return if amputated?(body_part)

  @injuries[body_part.to_sym] += amount
end

#add_stun_estimate(rounds, at: Time.now) ⇒ void

This method returns an undefined value.

Records a crit-table stun estimate, in rounds.

This is deliberately not stored as a timed entry in @status. The authoritative stun boolean is owned by and the combat message parser (see STATUS_DURATIONS, where 'stunned' is nil on purpose); expiring it on a table-derived timer would clear stun while a resisted-or-stacked creature is still stunned, and report muckled? false when acting is still unsafe. So the estimate lives alongside the boolean and is advisory only.

Stun does not cleanly stack, so a new estimate extends but never shortens an existing one.

Parameters:

  • rounds (Integer) —

    stun rounds from the critical table.

  • at (Time) (defaults to: Time.now) —

    when the game applied the crit (prompt time, not parse time - the async parser can lag the server).



501
502
503
504
505
506
507
508
509
510
511
# File 'documented/gemstone/creature.rb', line 501

def add_stun_estimate(rounds, at: Time.now)
  rounds = rounds.to_i
  return if rounds <= 0

  expires_at = at + (rounds * STUN_ROUND_SECONDS)
  return if @stun_estimated_until && @stun_estimated_until >= expires_at

  @stun_rounds = rounds
  @stun_estimated_until = expires_at
  debug_log("+stun estimate: #{rounds} round#{'s' unless rounds == 1} (~#{rounds * STUN_ROUND_SECONDS}s)")
end

#amputate!(body_part) ⇒ void

This method returns an undefined value.

Marks a body part as amputated.

Distinct from @injuries, which accumulates rank: an amputated limb is gone rather than wounded, cannot be wounded further, and stays gone.

Parameters:

  • body_part (String, Symbol) —

    one of BODY_PARTS.



550
551
552
553
554
555
556
557
558
# File 'documented/gemstone/creature.rb', line 550

def amputate!(body_part)
  unless BODY_PARTS.include?(body_part.to_s)
    raise ArgumentError, "Invalid body part: #{body_part}"
  end
  return if @amputated.include?(body_part.to_s)

  @amputated << body_part.to_s
  debug_log("+amputated: #{body_part}")
end

#amputated?(body_part) ⇒ Boolean

Returns whether a body part has been amputated.

Returns:

  • (Boolean) —

    whether a body part has been amputated.



561
562
563
# File 'documented/gemstone/creature.rb', line 561

def amputated?(body_part)
  @amputated.include?(body_part.to_s)
end

#amputated_parts ⇒ Array<String>

Returns every amputated body part.

Returns:

  • (Array<String>) —

    every amputated body part.



566
567
568
# File 'documented/gemstone/creature.rb', line 566

def amputated_parts
  @amputated.dup
end

#clear_smote ⇒ Object

Clear smote status



460
461
462
463
464
# File 'documented/gemstone/creature.rb', line 460

def clear_smote
  @ucs_smote = nil
  @ucs_updated = Time.now
  debug_log("UCS: smote cleared")
end

#clear_stun_estimate ⇒ void

This method returns an undefined value.

Clears any crit-derived stun estimate (creature shook off the stun).



538
539
540
541
# File 'documented/gemstone/creature.rb', line 538

def clear_stun_estimate
  @stun_rounds = nil
  @stun_estimated_until = nil
end

#coup_eligible?(rank) ⇒ Boolean

Check if creature currently qualifies for Coup de Grace at the given trained rank: at or below (rank * 10)% of max HP when incapacitated, (rank * 5)% otherwise, hard-capped at 200 HP either way. The cap is what binds on large creatures, so this compares raw HP, not percent.

Returns:

  • (Boolean)


655
656
657
658
659
660
661
# File 'documented/gemstone/creature.rb', line 655

def coup_eligible?(rank)
  return false unless rank.to_i > 0
  return false unless current_hp && max_hp && max_hp > 0
  incap = COUP_INCAP_STATUSES.any? { |s| has_status?(s) }
  threshold = [(max_hp * rank.to_i * (incap ? 10 : 5)) / 100.0, 200].min
  current_hp <= threshold
end

#current_hp ⇒ Object

Calculate current HP (max_hp - damage_taken)



628
629
630
631
# File 'documented/gemstone/creature.rb', line 628

def current_hp
  return nil unless max_hp
  [max_hp - @damage_taken, 0].max
end

#dead? ⇒ Boolean

Check if creature is dead (0 HP)

Returns:

  • (Boolean)


664
665
666
# File 'documented/gemstone/creature.rb', line 664

def dead?
  current_hp == 0
end

#essential_data ⇒ Object

Essential data for this instance



702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
# File 'documented/gemstone/creature.rb', line 702

def essential_data
  {
    id: @id,
    noun: @noun,
    name: @name,
    status: @status,
    injuries: @injuries,
    health: @health,
    damage_taken: @damage_taken,
    max_hp: max_hp,
    current_hp: current_hp,
    hp_percent: hp_percent,
    has_template: has_template?,
    created_at: @created_at,
    ucs_position: ucs_position,
    ucs_tierup: ucs_tierup,
    ucs_smote: smote?,
    amputated: amputated_parts,
    stun_rounds: stun_rounds,
    stunned_for: stunned_for
  }
end

#fatal_crit? ⇒ Boolean

Check if creature died from fatal crit

Returns:

  • (Boolean)


592
593
594
# File 'documented/gemstone/creature.rb', line 592

def fatal_crit?
  @fatal_crit
end

#has_template? ⇒ Boolean

Check if creature has template data

Returns:

  • (Boolean)


403
404
405
# File 'documented/gemstone/creature.rb', line 403

def has_template?
  !template.nil?
end

#hp_percent ⇒ Object

Calculate HP percentage (0-100)



634
635
636
637
# File 'documented/gemstone/creature.rb', line 634

def hp_percent
  return nil unless max_hp && max_hp > 0
  ((current_hp.to_f / max_hp) * 100).round(1)
end

#injured?(location, threshold = 1) ⇒ Boolean

Check if injured at location

Returns:

  • (Boolean)


582
583
584
# File 'documented/gemstone/creature.rb', line 582

def injured?(location, threshold = 1)
  @injuries[location.to_sym] >= threshold
end

#injured_locations(threshold = 1) ⇒ Object

Get all injured locations



597
598
599
# File 'documented/gemstone/creature.rb', line 597

def injured_locations(threshold = 1)
  @injuries.select { |_, value| value >= threshold }.keys
end

#low_hp?(threshold = 25) ⇒ Boolean

Check if creature is below HP threshold

Returns:

  • (Boolean)


640
641
642
643
# File 'documented/gemstone/creature.rb', line 640

def low_hp?(threshold = 25)
  return false unless hp_percent
  hp_percent <= threshold
end

#mark_fatal_crit! ⇒ Object

Mark creature as killed by fatal critical hit



587
588
589
# File 'documented/gemstone/creature.rb', line 587

def mark_fatal_crit!
  @fatal_crit = true
end

#max_hp ⇒ Object

Get maximum HP from template, with fallback



607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
# File 'documented/gemstone/creature.rb', line 607

def max_hp
  # Try template first
  hp = template&.max_hp
  return hp if hp && hp > 0

  # Fall back to combat tracker setting if available
  begin
    if defined?(Lich::Gemstone::Combat::Tracker) &&
       Lich::Gemstone::Combat::Tracker.respond_to?(:fallback_hp)
      fallback = Lich::Gemstone::Combat::Tracker.fallback_hp
      return fallback if fallback && fallback > 0
    end
  rescue
    # Ignore errors accessing tracker
  end

  # Last resort: hardcoded fallback
  400
end

#muckled? ⇒ Boolean

Creature-side analog of Lich::Gemstone::Status.muckled? (the player's own "can't act right now" check). Deliberately narrower than everything tracked in @status: excludes penalty-only conditions (disoriented), positional ones (prone/kneeling/sitting/flying/ hovering), and calm (the player version excludes that too, tracking it separately) - this is only the statuses that actually prevent acting, not ones that merely penalize or reposition.

Returns:

  • (Boolean)


691
692
693
694
# File 'documented/gemstone/creature.rb', line 691

def muckled?
  has_status?('webbed') || crtr_flag?(:dead) || dead? || has_status?('stunned') ||
    has_status?('sleeping') || has_status?('immobilized') || has_status?('rooted')
end

#position_to_tier(pos) ⇒ Object

Convert position string/number to tier (1-3)



410
411
412
413
414
415
416
417
# File 'documented/gemstone/creature.rb', line 410

def position_to_tier(pos)
  case pos
  when "decent", 1, "1" then 1
  when "good", 2, "2" then 2
  when "excellent", 3, "3" then 3
  else nil
  end
end

#reset_damage ⇒ Object

Reset damage (creature healed or respawned)



697
698
699
# File 'documented/gemstone/creature.rb', line 697

def reset_damage
  @damage_taken = 0
end

#set_ucs_position(position) ⇒ Object

Set UCS position tier



420
421
422
423
424
425
426
427
428
429
430
# File 'documented/gemstone/creature.rb', line 420

def set_ucs_position(position)
  new_tier = position_to_tier(position)
  return unless new_tier

  # Clear tierup if tier changed
  @ucs_tierup = nil if new_tier != @ucs_position

  @ucs_position = new_tier
  @ucs_updated = Time.now
  debug_log("UCS: position=#{new_tier}")
end

#set_ucs_tierup(attack_type) ⇒ Object

Set UCS tierup vulnerability



433
434
435
436
437
# File 'documented/gemstone/creature.rb', line 433

def set_ucs_tierup(attack_type)
  @ucs_tierup = attack_type
  @ucs_updated = Time.now
  debug_log("UCS: tierup=#{attack_type}")
end

#smite! ⇒ Object

Mark creature as smote (crimson mist applied)



440
441
442
443
444
# File 'documented/gemstone/creature.rb', line 440

def smite!
  @ucs_smote = Time.now
  @ucs_updated = Time.now
  debug_log("UCS: smote!")
end

#smote? ⇒ Boolean

Check if creature is currently smote

Returns:

  • (Boolean)


447
448
449
450
451
452
453
454
455
456
457
# File 'documented/gemstone/creature.rb', line 447

def smote?
  return false unless @ucs_smote

  # Check if smite effect has expired
  if Time.now - @ucs_smote > UCS_SMITE_TTL
    @ucs_smote = nil
    return false
  end

  true
end

#stun_rounds ⇒ Integer?

Estimated stun rounds from the last crit, or nil when the estimate has lapsed or stun is no longer active.

Returns:

  • (Integer, nil)


517
518
519
520
# File 'documented/gemstone/creature.rb', line 517

def stun_rounds
  return nil unless stun_estimate_active?
  @stun_rounds
end

#stunned_for ⇒ Float

Estimated seconds of stun remaining.

Advisory: derived from the critical table, not observed. Returns 0.0 when no estimate is active. Callers deciding whether it is safe to act should gate on muckled?/has_status?('stunned') and use this only to size the window.

Returns:

  • (Float)


530
531
532
533
# File 'documented/gemstone/creature.rb', line 530

def stunned_for
  return 0.0 unless stun_estimate_active?
  [@stun_estimated_until - Time.now, 0.0].max.round(1)
end

#template ⇒ Object

Get the template for this creature. Sentinel-cached so a creature with no template doesn't redo the name lookup (downcase + boon regex) on every call the way ||= would.



395
396
397
398
399
400
# File 'documented/gemstone/creature.rb', line 395

def template
  return @template if defined?(@template_looked_up)

  @template_looked_up = true
  @template = CreatureTemplate[@name]
end

#ucs_expired? ⇒ Boolean

Check if UCS data has expired

Returns:

  • (Boolean)


467
468
469
470
# File 'documented/gemstone/creature.rb', line 467

def ucs_expired?
  return true unless @ucs_updated
  (Time.now - @ucs_updated) > UCS_TTL
end

#valid_target? ⇒ Boolean

Checks whether this creature should be considered attackable.

Uses the same decoy and appendage exclusions as GameObj.targets, but uses structured death data from and HP tracking instead of regex-matching a status string.

Returns:

  • (Boolean)


675
676
677
678
679
680
681
682
# File 'documented/gemstone/creature.rb', line 675

def valid_target?
  return false if crtr_flag?(:dead) || dead?
  return false if @name =~ /^animated\b/i && @name !~ /^animated slush/i
  return false if @noun =~ /^(?:arm|appendage|claw|limb|pincer|tentacle)s?$|^(?:palpus|palpi)$/i &&
                  @name !~ /(?:amaranthine|ghostly|grizzled|ancient) kraken tentacle/i

  true
end