◆Cobblemon Wiki

애드온 제작

대화

Dialogues는 API 또는 데이터팩에서 사용해 GUI 안에서 대화를 재생할 수 있는 기능입니다. 엔티티를 표시하고, 여러 페이지의 텍스트를 보여 주며, 다양한 입력 방식을 처리합니다.

데이터팩을 통해 등록된 dialogue는 /opendialogue 명령어로 열 수 있습니다.

MoLang 표현식을 사용하는 모든 위치에는 단일 텍스트 속성을 넣거나, 여러 표현식을 실행해 결과를 만들 수 있도록 텍스트 배열을 넣을 수 있습니다.
Dialogue Intro.png

대화

Dialogue JSON 파일은 다음 주요 요소로 구성됩니다:

  • escapeAction: 대화에서 빠져나갈 때 수행할 동작을 정의하는 문자열 또는 배열입니다. MoLang 표현식 하나이거나 MoLang 표현식의 연속일 수 있습니다.

  • speakers: 대화의 화자를 정의하는 객체입니다. 각 화자는 키-값 쌍으로 표현되며, 키는 화자의 식별자이고 값은 화자의 이름과 모습을 설명하는 객체입니다.

  • pages: 대화의 각 페이지를 나타내는 객체 배열입니다.

화자

각 화자는 다음 속성을 가진 객체로 표현됩니다:

  • name: 화자의 이름을 정의하는 문자열 또는 객체입니다. 보통은 단순 텍스트입니다. 객체라면 type 속성이 "expression"으로 설정되어 있어야 하고, MoLang 표현식이 들어 있는 expression 속성이 필요합니다. 이름을 동적으로 만들고 싶을 때 표현식을 사용합니다.

  • face: 화자의 얼굴을 정의하는 객체 또는 MoLang 표현식입니다. 이는 GUI의 프레임 안에 렌더링됩니다. 객체인 경우 얼굴 타입을 나타내는 type 속성이 있어야 합니다. 현재 객체 타입으로 지원되는 것은 “standard”뿐이며, 다른 방식은 객체 대신 MoLang 표현식으로 처리합니다.

  • gibber: gibber 동작을 정의하는 객체입니다. 기본 설정을 사용하려면 단순히 \{\}로 정의할 수 있고, 원하는 속성을 포함한 전체 객체로도 정의할 수 있습니다. 사용할 수 있는 속성은 아래의 gibber 섹션을 참고하세요.

얼굴

화자의 얼굴은 여러 방식으로 정의할 수 있습니다. 객체로 정의하면 “standard” 얼굴 타입이 됩니다.

“standard”

standard 얼굴 타입은 표시되는 얼굴이 월드의 특정 엔티티와 연결되지는 않지만, Cobblemon의 식별자 및 aspect 목록 형식을 따르는 경우에 사용합니다. 객체에는 다음 속성이 있어야 합니다:

  • modelType: ‘pokemon’ 또는 ‘npc’ 중 하나입니다.

  • identifier: cobblemon:bulbasaur 같은 포켓몬 또는 NPC의 식별자입니다.

  • aspects: 모델에 적용할 외형 요소 배열입니다.

For example:

 "face": \{
   "type": "artificial",
   "modelType": "pokemon",
   "identifier": "cobblemon:pikachu",
   "aspects": [
     "shiny"
   ]
 \}

플레이어

q.player.face()를 사용하면 이 얼굴에 플레이어 모델을 렌더링할 수 있습니다.

For example:

 "face": "q.player.face();"

npc

NPC 엔티티로 시작된 대화라면 q.npc.face()를 사용해 대화 중인 NPC를 직접 참조해 표시할 수 있습니다.

For example:

 "face": "q.npc.face();"

횡설수설

dialogue의 추가 기능으로, 텍스트가 점진적으로 나타나고 말소리를 흉내 내는 단순한 음조가 함께 재생되도록 할 수 있습니다. 여기서는 이를 gibbering이라고 부릅니다. gibbering에는 여러 옵션이 있으며 모두 합리적인 기본값을 가집니다. 사용할 수 있는 속성은 다음과 같습니다:

  • graduallyShowText: true이면 GUI의 텍스트가 점진적으로 입력되는 것처럼 표시되는 불리언 값입니다. false이면 gibbering 사운드 효과는 재생되지만 모든 텍스트가 처음부터 보입니다. 기본값은 true입니다.

  • allowSkip: 대화 상자를 클릭했을 때 gibbering을 건너뛰고 모든 텍스트를 한 번에 표시할지 여부입니다. false이면 플레이어는 gibbering이 전부 끝날 때까지 기다린 뒤 다음 페이지로 넘어갈 수 있습니다.

step: 각 gibber 사이의 문자 수입니다. 현재 문자 위치에서 gibber가 재생된 뒤, 일정 시간 간격 후 이 step 값을 사용해 텍스트의 더 뒤쪽 문자로 이동하여 다음 gibber를 재생합니다. 텍스트를 점진적으로 표시할 때 step이 클수록 텍스트가 더 큰 덩어리로 나타납니다. 기본값은 4자입니다.

interval: 각 gibber 소리 사이의 시간(초)입니다. 값이 작을수록 소리가 더 빠르게 재생되며, 텍스트를 점진적으로 표시하는 경우 텍스트도 더 빠르게 나타납니다. 기본값은 0.1초입니다.

minPitch: 음을 무작위로 선택할 때 사용할 수 있는 최소 피치 배율입니다. 기본값은 0.8입니다.

maxPitch: 음을 무작위로 선택할 때 사용할 수 있는 최대 피치 배율입니다. 기본값은 1.2입니다.

minVolume: 음을 무작위로 선택할 때 사용할 수 있는 최소 음량 배율입니다. 기본값은 0.75입니다.

maxVolume: 음을 무작위로 선택할 때 사용할 수 있는 최대 음량 배율입니다. 기본값은 1.1입니다.

sounds: gibber를 재생할 때 선택할 사운드 이벤트를 나타내는 리소스 위치 목록입니다. 사운드는 원하는 개수만큼 넣을 수 있습니다. 기본값은 "cobblemon:entity.villager.npc1", "cobblemon:entity.villager.npc2", "cobblemon:entity.villager.npc1" 세 가지입니다.

문서

각 페이지는 다음 속성을 가진 객체로 표현됩니다:

  • id: 페이지를 고유하게 식별하는 문자열입니다. 특정 페이지로 이동하고 싶을 때 필요합니다. 페이지 번호를 사용할 수도 있지만, 페이지 ID를 사용하는 것이 좋습니다.

  • speaker: 화자의 식별자를 지정하는 문자열입니다. 이 식별자를 가진 화자가 speakers 목록에 존재해야 합니다.

  • lines: 각 텍스트 줄을 나타내는 문자열 또는 객체 배열입니다. 객체라면 type 속성이 "expression"으로 설정되어 있어야 하고, 문자열로 평가되는 MoLang 표현식이 들어 있는 expression 속성이 필요합니다. 변수를 포함해 동적 텍스트를 만들고 싶을 때 표현식을 사용합니다.

  • input: 입력 프롬프트를 정의하는 객체 또는 문자열입니다. 객체라면 type 속성이 "option", "text", "auto-continue", "none" 중 하나로 설정되어야 하며, 타입에 따라 다른 속성이 필요합니다. 기본 입력 값은 none입니다.

  • gibber: 페이지별 gibber 설정입니다. gibber 객체에 대한 자세한 내용은 위를 참고하세요. 기본값은 null이며, 따라서 이 페이지의 speaker에 gibber가 있다면 그것으로 대체됩니다.

  • clientActions: 페이지의 첫 프레임이 클라이언트에서 렌더링된 뒤 클라이언트에서 실행될 MoLang 표현식 목록입니다. "q.sound(...)"로 소리를 재생하거나, 렌더링 중인 얼굴이 Bedrock 애니메이션 엔티티라면 예를 들어 "q.face.play_animation('win');"으로 애니메이션을 재생하는 데 사용할 수 있습니다.

  • escapeAction: 페이지에서 빠져나갈 때 수행할 동작을 정의하는 문자열 또는 배열입니다. MoLang 표현식 하나이거나 MoLang 표현식의 연속일 수 있습니다.

  • background: 대화 상자의 배경 텍스처로 사용할 리소스 위치입니다. 기본값은 null이며, 이 경우 Cobblemon의 내부 텍스처가 사용됩니다.

입력 타입

페이지 객체의 input 속성은 type에 따라 서로 다른 구조를 가질 수 있습니다.

action 속성이 MoLang 표현식으로 설정된 모든 입력에서는 v.seconds_taken_to_input을 사용해 해당 입력을 하기까지 걸린 시간을 확인할 수 있습니다.

“옵션”

입력이 옵션 세트인 경우입니다. 객체에는 각 옵션을 나타내는 객체 배열인 options 속성이 있어야 합니다. 또한 vertical 속성이 true인지 false인지에 따라 옵션을 세로 또는 가로로 표시할지 지정할 수 있습니다.

각 옵션 객체는 다음 속성을 가질 수 있습니다:

  • value: dialogue 옵션의 고유 값입니다. 플레이어에게 표시되지 않으며, 스크립트에서 이 옵션을 참조하는 데 사용됩니다. 필수 속성입니다.

  • text옵션에 표시될 텍스트입니다. 단순 문자열이거나 객체일 수 있습니다. 필수 속성입니다.

  • action: 사용자가 옵션을 선택했을 때 실행할 MoLang 표현식입니다.

  • isVisible: true 또는 false를 반환해야 하는 MoLang 표현식입니다. false이면 해당 옵션은 플레이어에게 보이지 않습니다. 기본값은 true입니다.

  • isSelectable: true 또는 false를 반환해야 하는 MoLang 표현식입니다. false이면 해당 옵션은 회색으로 비활성화됩니다(보이는 경우에만). 기본값은 true입니다.

Dialogue Options.png

“text”

입력이 텍스트 필드인 경우입니다. `action` 속성은 플레이어가 텍스트를 입력하고 Enter를 눌렀을 때 실행되는 MoLang 표현식입니다. 앞서 설명했듯이 MoLang 표현식 안에서 `v.selected_option`을 사용해 입력한 텍스트를 검사할 수 있습니다.
Dialogue Input.png

“auto-continue”

대화가 일정 시간이 지난 뒤 자동으로 진행됩니다. 이를 위한 몇 가지 선택 속성이 있습니다.

  • delay: 진행되기까지의 지연 시간(초)입니다. 기본값은 5초입니다.

  • allowSkip: 사용자가 클릭으로 지연 시간을 건너뛸 수 있는지 여부입니다. 기본값은 true입니다. false이면 플레이어는 지연 시간이 끝날 때까지 기다려야 합니다.

  • showTimer: 자동 진행까지 플레이어에게 남은 시간을 나타내는 막대를 표시할지 여부입니다. 기본값은 false입니다.

  • action: 플레이어가 클릭하거나(allowSkip이 true일 때만) 지연 시간이 끝났을 때 실행되는 MoLang 표현식입니다. 기본적으로 다음 페이지로 이동합니다.

“none”

계속하려면 사용자가 클릭할 때까지 기다리는 것 외에 입력은 없습니다. 객체에는 플레이어가 클릭했을 때 실행할 MoLang 표현식을 정의하는 action 속성이 있을 수 있습니다. 기본적으로 다음 페이지로 이동합니다.

시간 제한

"none"과 "autocontinue"를 제외한 모든 입력 타입은 페이지를 보는 플레이어에게 시간 제한을 적용할 수 있는 timeout 객체를 가질 수 있습니다.

timeout 객체는 3가지 속성을 가질 수 있습니다:

  • duration: 플레이어가 입력을 완료해야 하는 시간(초)입니다. 기본값은 10초입니다.

  • showTimer: 플레이어에게 남은 시간을 보여 주는 막대를 표시할지 여부입니다. 기본값은 true입니다.

  • action: 제한 시간 안에 입력하지 않았을 때 실행할 MoLang 표현식입니다. 기본적으로 dialogue를 닫습니다.

Dialogue Timer.png

예시

다음 예시는 모드에 내장되어 있으며 “example”이라는 라벨이 붙어 있습니다.

 \{
   "escapeAction": "q.dialogue.set_page('quitter')",
   "speakers": \{
     "pikachu": \{
       "name": "The Voice of Hiro",
       "face": \{
         "type": "artificial",
         "modelType": "pokemon",
         "identifier": "cobblemon:pikachu",
         "aspects": [
           "shiny"
         ]
       \},
       "gibber": \{
         "step": 2,
         "interval": 0.07,
         "allowSkip": false
       \}
     \},
     "player": \{
       "face": "q.player.face();",
       "name": \{
         "type": "expression",
         "expression": "q.player.username"
       \}
     \}
   \},
   "pages": [
     \{
       "id": "player-chat",
       "speaker": "player",
       "lines": [
         \{
           "type": "expression",
           "expression": "'Hello, I\\'m ' + q.player.username + '!'"
         \}
       ]
     \},
     \{
       "speaker": "pikachu",
       "lines": [
         \{
           "type": "expression",
           "expression": "'Nice to meet you, ' + q.player.username + '! Welcome to the world of dialogues!'"
         \}
       ]
     \},
     \{
       "id": "intro",
       "speaker": "pikachu",
       "lines": [ "Do you want to learn more?"],
       "input": \{
         "type": "option",
         "vertical": false,
         "options": [
           \{
             "text": "Yes",
             "value": "yes",
             "action": [
               "v.player_response = 'That sounds great!';",
               "v.next_page = 'page1';",
               "q.dialogue.set_page('player-surrogate');"
             ]
           \},
           \{
             "text": "No",
             "value": "no",
             "action": [
               "v.player_response = 'No, I really don\\'t care.';",
               "v.next_page = 'quitter';",
               "q.dialogue.set_page('player-surrogate');"
             ]
           \},
           \{
             "text": "Sword!",
             "value": "sword",
             "isVisible": "q.player.main_held_item.is_of('minecraft:iron_sword')",
             "isSelectable": "q.player.data.scared_npc != true;",
             "action": [
               "v.player_response = 'How about you die! Muahahaha!';",
               "v.next_page = 'sword';",
               "q.dialogue.set_page('player-surrogate');"
             ]
           \},
           \{
             "text": "Sword again!",
             "value": "sword2",
             "isVisible": "q.player.main_held_item.is_of('minecraft:iron_sword')",
             "isSelectable": "q.player.data.scared_npc == true;",
             "action": [
               "v.player_response = 'How about you die... again! Muahahaha!';",
               "v.next_page = 'sword-again';",
               "q.dialogue.set_page('player-surrogate');"
             ]
           \}
         ]
       \}
     \},
     \{
       "id": "player-surrogate",
       "speaker": "player",
       "gibber": \{\},
       "lines": [
         \{
           "type": "expression",
           "expression": "v.player_response;"
         \}
       ],
       "input": "q.dialogue.set_page(v.next_page);"
     \},
     \{
       "id": "page1",
       "speaker": "pikachu",
       "lines": [
         "Well you see, this is a dialogue.",
         "You can have multiple pages, and each page can have multiple lines."
       ]
     \},
     \{
       "id": "page2",
       "speaker": "pikachu",
       "lines": ["On an unrelated note, did you know that I'm deeply afraid of iron swords? Anyway, cya!"],
       "input": \{
         "type": "auto-continue",
         "delay": 5,
         "allowSkip": true,
         "action": "q.dialogue.close();"
       \}
     \},
     \{
       "id": "quitter",
       "speaker": "pikachu",
       "lines": ["Fine. Be that way."],
       "input": "q.dialogue.close();",
       "escapeAction": "q.dialogue.close();"
     \},
     \{
       "id": "sword",
       "speaker": "pikachu",
       "lines": ["Oh god no, please don't hurt me!"],
       "escapeAction": "q.dialogue.input();",
       "input": [
         "t.data = q.player.data();",
         "t.data.scared_npc = true;",
         "q.player.save_data();",
         "q.dialogue.close();"
       ]
     \},
     \{
       "id": "sword-again",
       "speaker": "pikachu",
       "lines": ["I'm ready for you this time. YOU will die today!"],
       "escapeAction": "0",
       "input": \{
         "type": "option",
         "vertical": true,
         "timeout": \{
           "delay": 6,
           "action": "q.dialogue.input('stand');"
         \},
         "options": [
           \{
             "text": "Parry his attack.",
             "value": "parry",
             "action": [
               "v.reaction = 'parry';",
               "q.dialogue.set_page('sword-again-decided');"
             ]
           \},
           \{
             "text": "Stand perfectly still.",
             "value": "stand",
             "action": [
               "v.reaction = 'stand still';",
               "q.dialogue.set_page('sword-again-decided');"
             ]
           \},
           \{
             "text": "Drop your sword.",
             "value": "drop",
             "action": [
               "v.reaction = 'drop your sword';",
               "q.dialogue.set_page('sword-again-decided');"
             ]
           \}
         ]
       \}
     \},
     \{
       "id": "sword-again-decided",
       "lines": [
         \{
           "type": "expression",
           "expression": "v.reaction == 'parry' ? 'You parry his attack and kill him.' : (v.reaction == 'stand still' ? 'He kills you.' : 'You drop your sword and he kills you.')"
         \}
       ],
       "input": \{
         "type": "auto-continue",
         "delay": 3,
         "allowSkip": false,
         "showTimer": false,
         "action": "q.dialogue.set_page('sword-again-reaction');"
       \}
     \},
     \{
       "id": "sword-again-reaction",
       "speaker": "pikachu",
       "lines": [
         \{
           "type": "expression",
           "expression": "'Nah, I\\'m just kidding. Interesting that you chose to ' + v.reaction + ' though.'"
         \}
       ]
     \},
     \{
       "id": "name-question",
       "speaker": "pikachu",
       "lines": [ "Here's a puzzle though... What is my name? Lowercase only."],
       "input": \{
         "type": "text",
         "timeout": \{
           "delay": 10,
           "action": "q.dialogue.set_page('too-slow');"
         \},
         "action": [
           "q.dialogue.set_page('name-speak');",
           "t.data = q.player.data();"
         ]
       \}
     \},
     \{
       "id": "name-speak",
       "speaker": "player",
       "lines": [
         \{
           "type": "expression",
           "expression": "'Your name is ' + v.selected_option + ', right?'"
         \}
       ],
       "input": "q.dialogue.set_page('name-guess');"
     \},
     \{
       "id": "name-guess",
       "speaker": "pikachu",
       "lines": [
         \{
           "type": "expression",
           "expression": "v.selected_option == 'hiroku' ? 'Correct! You know things that you shouldn\\'t...' : ('Wrong! I\\'m not \"' + v.selected_option + '\". Whoever that is.')"
         \}
       ],
       "input": \{
         "type": "auto-continue",
         "delay": 5,
         "allowSkip": true,
         "action": "q.dialogue.set_page('farewell');"
       \}
     \},
     \{
       "id": "farewell",
       "speaker": "pikachu",
       "lines": ["Anyway, I'm done with this dialogue. Bye!"],
       "input": "q.dialogue.close();"
     \},
     \{
       "id": "too-slow",
       "speaker": "pikachu",
       "lines": ["Don't know? Sad. Well I don't know your name either, so I guess we're even."],
       "input": "q.dialogue.close()"
     \}
   ]
 \}