Class: Esp::Mw::DialogueDsl

Inherits:
Object
  • Object
show all
Defined in:
lib/esp/mw/dialogue_dsl.rb

Overview

Block-style DSL for emitting Dialogue (DIAL) and DialogueInfo (INFO) records without the bookkeeping. Used inside Ruby mod sources via the loader-exposed ‘dialogue { … }` helper.

The contract: a ‘dialogue` block returns a flat Array of records that the author splats into their mod’s records array. The DSL handles:

  • One DIAL record per ‘topic`, with `dialogue_type` carried through.

  • INFO records under each topic, chained via prev_id/next_id in author order — Morrowind evaluates filters top-down, so author order is filter precedence.

  • Speaker scoping via ‘speaker “Name” do … end`. Nested blocks inherit; an explicit `speaker:` on an info wins.

  • i18n: a ‘t(“key”)` helper is available inside the block when the loader passes an Esp::Mw::I18n instance.

Example:

dialogue do
  speaker "Hrisskar Flat-Foot" do
    topic "Flat-Foot" do
      info t("hrisskar.flat_foot.intro")
      info t("hrisskar.flat_foot.legion"), pc_faction: "Imperial Legion"
    end
  end

  topic "AFSN_Tracker", type: :journal do
    info "Stage 10 text", journal_index: 10
    info "Stage 20 text", journal_index: 20
  end
end

Supported info kwargs:

speaker:           speaker_id filter (overrides surrounding scope)
race:              speaker_race filter
class:             speaker_class filter (note the keyword clash —
                   use the string key or the `class_:` alias)
faction:           speaker_faction filter
cell:              speaker_cell filter
sex:               :any | :female | :male
pc_faction:        player_faction filter
pc_rank:           player_rank filter
speaker_rank:      NPC rank filter
disposition:       minimum disposition
sound:             sound_path
result_script:     inline MWScript text to run on activation
result_script_source: path to .mwscript file (resolved relative to
                      the mod source dir at preflight time)
journal_index:     for Journal-type topics — maps to data.disposition

Constant Summary collapse

VALID_TYPES =
%i[topic journal persuasion greeting voice].freeze
VALID_SEX =
%i[any female male].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(i18n: nil) ⇒ DialogueDsl

Returns a new instance of DialogueDsl.



83
84
85
86
87
88
89
90
# File 'lib/esp/mw/dialogue_dsl.rb', line 83

def initialize(i18n: nil)
  @records = []
  @speaker_stack = []
  @current_topic = nil
  return unless i18n

  define_singleton_method(:t) { |key| i18n.t(key) }
end

Instance Attribute Details

#recordsObject (readonly)

Returns the value of attribute records.



81
82
83
# File 'lib/esp/mw/dialogue_dsl.rb', line 81

def records
  @records
end

Class Method Details

.build(i18n: nil, &block) ⇒ Object

Raises:

  • (ArgumentError)


57
58
59
60
61
62
63
# File 'lib/esp/mw/dialogue_dsl.rb', line 57

def self.build(i18n: nil, &block)
  raise ArgumentError, Esp.t('errors.dialogue.needs_block') unless block

  dsl = new(i18n: i18n)
  dsl.instance_eval(&block)
  dsl.records
end

.from_spec(spec) ⇒ Object

Data-driven equivalent of ‘build`, for callers that can’t pass a Ruby block (MCP/HTTP). Spec is JSON-shaped:

{ "topics" => [ { "name"=>, "type"=>, "speaker"=>,
                  "infos"=>[ { "text"=>, <filter keys>... } ] } ] }

Filter keys mirror the block DSL’s info kwargs. ‘text` carries literal strings or `@t:` sentinels (resolved at build), so there’s no t() helper and no i18n instance here — unlike the block form.

Raises:

  • (ArgumentError)


72
73
74
75
76
77
78
79
# File 'lib/esp/mw/dialogue_dsl.rb', line 72

def self.from_spec(spec)
  topics = spec.is_a?(Hash) ? spec['topics'] : spec
  raise ArgumentError, Esp.t('errors.dialogue.needs_topics') unless topics.is_a?(Array)

  dsl = new
  topics.each { |t| dsl.send(:apply_topic_spec, t) }
  dsl.records
end

Instance Method Details

#info(text, **filters) ⇒ Object



114
115
116
117
118
# File 'lib/esp/mw/dialogue_dsl.rb', line 114

def info(text, **filters)
  raise Esp.t('errors.dialogue.info_outside_topic') unless @current_topic

  @current_topic[:infos] << { text: text, filters: filters }
end

#speaker(name, &block) ⇒ Object

Raises:

  • (ArgumentError)


92
93
94
95
96
97
98
# File 'lib/esp/mw/dialogue_dsl.rb', line 92

def speaker(name, &block)
  raise ArgumentError, Esp.t('errors.dialogue.speaker_needs_block') unless block

  @speaker_stack.push(name)
  instance_eval(&block)
  @speaker_stack.pop
end

#topic(name, type: :topic, &block) ⇒ Object

Raises:

  • (ArgumentError)


100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/esp/mw/dialogue_dsl.rb', line 100

def topic(name, type: :topic, &block)
  unless VALID_TYPES.include?(type)
    raise ArgumentError, Esp.t('errors.dialogue.bad_topic_type', types: VALID_TYPES)
  end
  raise ArgumentError, Esp.t('errors.dialogue.topic_needs_block') unless block
  raise Esp.t('errors.dialogue.nested_topics') if @current_topic

  @records << { 'type' => 'Dialogue', 'flags' => '', 'id' => name,
                'dialogue_type' => normalize_type(type) }
  @current_topic = { name: name, type: type, infos: [] }
  instance_eval(&block)
  flush_topic
end