@kevin5251984/guild 0.2.17 → 0.2.19

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.
@@ -51,24 +51,32 @@
51
51
  </div>
52
52
  <div class="rpg-scene is-inn" id="scene-inn" hidden>
53
53
  <div class="inn-map">
54
- <div class="inn-wall"></div>
55
- <div class="inn-rail"></div>
56
- <div class="inn-bar"></div>
57
- <div class="inn-bottles" id="inn-bottles"></div>
58
- <div class="inn-table t1"></div>
59
- <div class="inn-table t2"></div>
60
- <div class="inn-barrel b1"></div>
61
- <div class="inn-barrel b2"></div>
62
- <div class="inn-hearth"></div>
54
+ <div class="inn-wall">
55
+ <span class="inn-window"></span>
56
+ <span class="inn-window"></span>
57
+ <span class="inn-window"></span>
58
+ <span class="inn-window"></span>
59
+ </div>
60
+ <div class="inn-nook inn-nook-l">
61
+ <span class="inn-prop plant"></span>
62
+ <span class="inn-prop plant"></span>
63
+ </div>
64
+ <div class="inn-nook inn-nook-r">
65
+ <div class="inn-bar"></div>
66
+ <div class="inn-bottles" id="inn-bottles"></div>
67
+ </div>
63
68
  <div class="inn-actors" id="inn-actors"></div>
64
69
  </div>
65
70
  <button type="button" class="rpg-exit" id="leave-inn" data-i18n="studio.leave">出門</button>
66
71
  </div>
67
72
  <div class="rpg-dialog" id="rpg-dialog">
68
73
  <div class="rpg-dialog-inner">
69
- <div class="rpg-name" id="rpg-name">酒館</div>
70
- <p class="rpg-line" id="rpg-line">夜晚的編制還開著。點那扇門進去。</p>
71
- <div class="rpg-acts" id="rpg-acts"></div>
74
+ <img class="rpg-face" id="rpg-face" alt="" hidden />
75
+ <div class="rpg-copy">
76
+ <div class="rpg-name" id="rpg-name">酒館</div>
77
+ <p class="rpg-line" id="rpg-line">夜晚的編制還開著。點那扇門進去。</p>
78
+ <div class="rpg-acts" id="rpg-acts"></div>
79
+ </div>
72
80
  </div>
73
81
  </div>
74
82
  </div>
@@ -88,6 +96,7 @@
88
96
  <input name="oneLiner" placeholder="這名冒險者在編制裡做什麼" data-i18n-placeholder="studio.oneLinerPh" />
89
97
  <div class="card-foot">
90
98
  <button type="button" class="ghost danger" id="delete-bot" hidden data-i18n="bot.delete">請這名冒險者離席</button>
99
+ <button type="button" class="ghost" id="gen-look" hidden data-i18n="studio.genLook">生成形象</button>
91
100
  <div class="save-row">
92
101
  <p class="block-flash" hidden></p>
93
102
  <button type="button" class="save-md" data-save="identity" hidden>儲存名字</button>
@@ -183,7 +192,7 @@
183
192
  let openId = "";
184
193
  let activeTags = new Set();
185
194
  let showAllSkills = false;
186
- let rpgTalk = { name: "", line: "", acts: "" };
195
+ let rpgTalk = { name: "", line: "", acts: "", face: "" };
187
196
  const PICKED = "__picked__";
188
197
  const REST_CAP = 12;
189
198
 
@@ -197,6 +206,8 @@
197
206
  document.title = t("studio.editDocTitle");
198
207
  submit.hidden = true;
199
208
  document.getElementById("delete-bot").hidden = false;
209
+ document.getElementById("gen-look").hidden = false;
210
+ document.getElementById("gen-look").textContent = t("studio.genLook");
200
211
  mdSaves.forEach((btn) => {
201
212
  btn.hidden = false;
202
213
  const kind = btn.getAttribute("data-save");
@@ -212,6 +223,7 @@
212
223
  submit.hidden = false;
213
224
  submit.textContent = t("studio.seat");
214
225
  document.getElementById("delete-bot").hidden = true;
226
+ document.getElementById("gen-look").hidden = true;
215
227
  document.title = t("title.studio");
216
228
  mdSaves.forEach((btn) => {
217
229
  btn.hidden = true;
@@ -238,6 +250,24 @@
238
250
  function setBlockFlash(block, kind, text) {
239
251
  setNodeFlash(block && block.querySelector(".gen-row .block-flash"), kind, text);
240
252
  }
253
+ function beginBusy(button, label) {
254
+ if (!button || button.disabled || button.getAttribute("aria-busy") === "true") {
255
+ return false;
256
+ }
257
+ button.dataset.idleLabel = button.textContent;
258
+ button.disabled = true;
259
+ button.setAttribute("aria-busy", "true");
260
+ button.classList.add("is-busy");
261
+ if (label) button.textContent = label;
262
+ return true;
263
+ }
264
+ function endBusy(button, label) {
265
+ if (!button) return;
266
+ button.disabled = false;
267
+ button.removeAttribute("aria-busy");
268
+ button.classList.remove("is-busy");
269
+ button.textContent = label || button.dataset.idleLabel || button.textContent;
270
+ }
241
271
  function saveFlashEl(kind) {
242
272
  const btn = document.querySelector('.save-md[data-save="' + kind + '"]');
243
273
  return btn && btn.parentElement ? btn.parentElement.querySelector(".block-flash") : null;
@@ -547,8 +577,7 @@
547
577
  setNodeFlash(flashEl, "error", t("studio.emptyLib"));
548
578
  return;
549
579
  }
550
- button.disabled = true;
551
- button.textContent = t("studio.picking");
580
+ if (!beginBusy(button, t("studio.picking"))) return;
552
581
  try {
553
582
  const response = await fetch("/generate/skills", {
554
583
  method: "POST",
@@ -597,8 +626,7 @@
597
626
  } catch (err) {
598
627
  setNodeFlash(flashEl, "error", err.message);
599
628
  } finally {
600
- button.disabled = false;
601
- button.textContent = t("studio.pickSkills");
629
+ endBusy(button, t("studio.pickSkills"));
602
630
  }
603
631
  });
604
632
  document.getElementById("skill-search").addEventListener("input", () => {
@@ -688,8 +716,7 @@
688
716
  setBlockFlash(block, "error", t("studio.needPrompt", { kind: kind }));
689
717
  return;
690
718
  }
691
- button.disabled = true;
692
- button.textContent = t("studio.generating");
719
+ if (!beginBusy(button, t("studio.generating"))) return;
693
720
  try {
694
721
  const response = await fetch("/generate", {
695
722
  method: "POST",
@@ -729,8 +756,7 @@
729
756
  } catch (err) {
730
757
  setBlockFlash(block, "error", err.message);
731
758
  } finally {
732
- button.disabled = false;
733
- button.textContent = t("studio.genMd");
759
+ endBusy(button, t("studio.genMd"));
734
760
  }
735
761
  });
736
762
  });
@@ -887,80 +913,29 @@
887
913
  const roster = await fetch("/bots").then((r) => r.json());
888
914
  renderBar(Array.isArray(roster) ? roster : []);
889
915
  setScene("inn");
890
- talk(body.name, t("studio.hireToast"), npcActs(body));
916
+ talk(body.name, t("studio.hireToast"), npcActs(body), body.portrait);
891
917
  });
892
918
 
893
- function seedOf(key) {
894
- let n = 0;
895
- for (const ch of key) n = (n + ch.charCodeAt(0) * 17) % 997;
896
- return n;
897
- }
898
- function shirtColor(key) {
899
- return COLORS[seedOf(key) % COLORS.length];
900
- }
901
- function px(ctx, x, y, w, h, color) {
902
- ctx.fillStyle = color;
903
- ctx.fillRect(x, y, w, h);
904
- }
905
- function drawBuddy(canvas, shirt, seed) {
906
- const ctx = canvas.getContext("2d");
907
- canvas.width = 16;
908
- canvas.height = 24;
909
- ctx.imageSmoothingEnabled = false;
910
- const skin = ["#f0c8a0", "#e2b184", "#c68642", "#8d5524"][seed % 4];
911
- const hair = ["#2a1c12", "#c4a35a", "#1a1a1a", "#6b2d3c", "#3c4a6e"][seed % 5];
912
- const outline = "#1b1510";
913
- const pants = "#2a3140";
914
- const style = seed % 3;
915
- px(ctx, 4, 12, 8, 7, outline);
916
- px(ctx, 5, 13, 6, 5, shirt);
917
- px(ctx, 3, 13, 2, 5, shirt);
918
- px(ctx, 11, 13, 2, 5, shirt);
919
- px(ctx, 3, 18, 2, 1, skin);
920
- px(ctx, 11, 18, 2, 1, skin);
921
- px(ctx, 4, 4, 8, 8, outline);
922
- px(ctx, 5, 5, 6, 6, skin);
923
- px(ctx, 6, 7, 1, 1, outline);
924
- px(ctx, 9, 7, 1, 1, outline);
925
- if (style === 0) {
926
- px(ctx, 4, 3, 8, 3, hair);
927
- px(ctx, 4, 5, 2, 3, hair);
928
- px(ctx, 10, 5, 2, 3, hair);
929
- } else if (style === 1) {
930
- px(ctx, 4, 2, 8, 4, hair);
931
- px(ctx, 3, 5, 2, 4, hair);
932
- px(ctx, 11, 5, 2, 4, hair);
933
- } else {
934
- px(ctx, 5, 2, 6, 2, hair);
935
- px(ctx, 4, 4, 8, 2, hair);
936
- px(ctx, 7, 1, 2, 2, hair);
937
- }
938
- px(ctx, 5, 19, 3, 4, pants);
939
- px(ctx, 8, 19, 3, 4, pants);
940
- px(ctx, 5, 23, 3, 1, outline);
941
- px(ctx, 8, 23, 3, 1, outline);
942
- }
943
- const SPOTS = [
944
- [22, 38],
945
- [36, 34],
946
- [50, 40],
947
- [64, 36],
948
- [26, 54],
949
- [42, 58],
950
- [58, 52],
951
- [74, 56],
952
- ];
953
919
  let barBots = [];
954
920
  function setScene(name) {
955
921
  document.getElementById("scene-street").hidden = name !== "street";
956
922
  document.getElementById("scene-inn").hidden = name !== "inn";
957
923
  document.getElementById("rpg-screen").setAttribute("data-scene", name);
958
924
  }
959
- function talk(name, line, acts) {
960
- rpgTalk = { name: name, line: line, acts: acts || "" };
925
+ function talk(name, line, acts, face) {
926
+ rpgTalk = { name: name, line: line, acts: acts || "", face: face || "" };
961
927
  document.getElementById("rpg-name").textContent = name;
962
928
  document.getElementById("rpg-line").textContent = line;
963
929
  document.getElementById("rpg-acts").innerHTML = acts || "";
930
+ const img = document.getElementById("rpg-face");
931
+ const url = String(face || "").trim();
932
+ if (url) {
933
+ img.hidden = false;
934
+ img.src = url;
935
+ } else {
936
+ img.removeAttribute("src");
937
+ img.hidden = true;
938
+ }
964
939
  }
965
940
  function npcActs(bot) {
966
941
  return (
@@ -975,6 +950,69 @@
975
950
  "</a>"
976
951
  );
977
952
  }
953
+ function npcSprite(bot) {
954
+ if (bot.portrait) {
955
+ return (
956
+ '<img class="npc-bust" alt="" src="' +
957
+ esc(bot.portrait) +
958
+ '">'
959
+ );
960
+ }
961
+ return (
962
+ '<span class="npc-blank" aria-hidden="true">' +
963
+ esc(initials(bot.name || bot.handle)) +
964
+ "</span>"
965
+ );
966
+ }
967
+ const lookBusy = new Set();
968
+ async function requestLook(id) {
969
+ const bot = barBots.find((item) => item.id === id);
970
+ if (!bot || lookBusy.has(id)) return;
971
+ const formBtn = document.getElementById("gen-look");
972
+ if (formBtn && EDIT_ID === id && !beginBusy(formBtn, t("studio.genLookBusy"))) {
973
+ return;
974
+ }
975
+ lookBusy.add(id);
976
+ const npc = document.querySelector('.rpg-npc[data-id="' + id + '"]');
977
+ if (npc) npc.classList.add("drawing");
978
+ talk(bot.name, t("studio.genLookBusy"), npcActs(bot), bot.portrait);
979
+ const idFlash = document.querySelector("#create-bot > .card .block-flash");
980
+ if (idFlash && EDIT_ID === id) {
981
+ setNodeFlash(idFlash, "muted", t("studio.genLookBusy"));
982
+ }
983
+ try {
984
+ const response = await fetch("/bots/" + encodeURIComponent(id) + "/look", {
985
+ method: "POST",
986
+ });
987
+ const body = await response.json();
988
+ if (!response.ok) {
989
+ talk(bot.name, body.error || t("studio.genLookFail"), npcActs(bot), bot.portrait);
990
+ if (idFlash && EDIT_ID === id) {
991
+ setNodeFlash(idFlash, "error", body.error || t("studio.genLookFail"));
992
+ }
993
+ return;
994
+ }
995
+ const i = barBots.findIndex((item) => item.id === id);
996
+ if (i >= 0) barBots[i] = Object.assign({}, barBots[i], body);
997
+ await renderBar(barBots);
998
+ document.querySelectorAll(".rpg-npc").forEach((el) => {
999
+ el.classList.toggle("on", el.getAttribute("data-id") === id);
1000
+ });
1001
+ talk(body.name || bot.name, t("studio.genLookDone"), npcActs(body), body.portrait);
1002
+ if (idFlash && EDIT_ID === id) {
1003
+ setNodeFlash(idFlash, "health", t("studio.genLookDone"));
1004
+ }
1005
+ } catch (err) {
1006
+ talk(bot.name, t("studio.genLookFail"), npcActs(bot), bot.portrait);
1007
+ if (idFlash && EDIT_ID === id) {
1008
+ setNodeFlash(idFlash, "error", t("studio.genLookFail"));
1009
+ }
1010
+ } finally {
1011
+ lookBusy.delete(id);
1012
+ if (npc) npc.classList.remove("drawing");
1013
+ if (formBtn && EDIT_ID === id) endBusy(formBtn, t("studio.genLook"));
1014
+ }
1015
+ }
978
1016
  function renderSideRoster(bots) {
979
1017
  const hire = document.getElementById("side-hire");
980
1018
  const box = document.getElementById("side-roster");
@@ -997,48 +1035,55 @@
997
1035
  })
998
1036
  .join("");
999
1037
  }
1000
- function renderBar(bots) {
1038
+ async function renderBar(bots) {
1001
1039
  barBots = bots || [];
1002
1040
  renderSideRoster(barBots);
1003
1041
  const bottles = ["#c44", "#4a8", "#c84", "#48c", "#a4c", "#cc8", "#844", "#4cc"];
1004
1042
  document.getElementById("inn-bottles").innerHTML = bottles
1005
1043
  .map((c) => '<span style="background:' + c + '"></span>')
1006
1044
  .join("");
1007
- const roster = barBots.slice(0, SPOTS.length - 1);
1008
- const nodes = roster.map((bot, i) => {
1009
- const spot = SPOTS[i];
1045
+ const diving = {};
1046
+ try {
1047
+ const space = await fetch("/workspace").then((r) => r.json());
1048
+ (space.live || []).forEach((row) => {
1049
+ if (row && row.botId) diving[row.botId] = true;
1050
+ });
1051
+ } catch {
1052
+ /* roster still renders if live is down */
1053
+ }
1054
+ const station = (inner) =>
1055
+ '<div class="inn-station"><span class="inn-desk" aria-hidden="true"><span class="inn-monitor"></span></span>' +
1056
+ inner +
1057
+ "</div>";
1058
+ const nodes = barBots.map((bot) => {
1010
1059
  const on = EDIT_ID === bot.id ? " on" : "";
1011
- return (
1060
+ const live = Boolean(diving[bot.id]);
1061
+ return station(
1012
1062
  '<button type="button" class="rpg-npc' +
1013
- on +
1014
- '" style="left:' +
1015
- spot[0] +
1016
- "%;top:" +
1017
- spot[1] +
1018
- '%" data-id="' +
1019
- bot.id +
1020
- '"><canvas class="buddy" data-handle="' +
1021
- bot.handle +
1022
- '"></canvas><span class="tag">' +
1023
- esc(bot.name) +
1024
- "</span></button>"
1063
+ on +
1064
+ '" data-id="' +
1065
+ bot.id +
1066
+ '"><span class="npc-state' +
1067
+ (live ? " live" : "") +
1068
+ '">' +
1069
+ esc(live ? t("studio.diving") : t("studio.idle")) +
1070
+ "</span>" +
1071
+ npcSprite(bot) +
1072
+ '<span class="tag">' +
1073
+ esc(bot.name) +
1074
+ "</span></button>",
1025
1075
  );
1026
1076
  });
1027
- const hireSpot = SPOTS[Math.min(roster.length, SPOTS.length - 1)];
1028
1077
  nodes.push(
1029
- '<button type="button" class="rpg-npc vacant" style="left:' +
1030
- hireSpot[0] +
1031
- "%;top:" +
1032
- hireSpot[1] +
1033
- '%" data-id="hire"><canvas class="buddy" data-handle="recruit"></canvas><span class="tag">' +
1034
- t("studio.hireChip") +
1035
- "</span></button>",
1078
+ station(
1079
+ '<button type="button" class="rpg-npc vacant" data-id="hire"><span class="npc-state">' +
1080
+ esc(t("studio.idle")) +
1081
+ '</span><span class="npc-blank" aria-hidden="true">?</span><span class="tag">' +
1082
+ t("studio.hireChip") +
1083
+ "</span></button>",
1084
+ ),
1036
1085
  );
1037
1086
  document.getElementById("inn-actors").innerHTML = nodes.join("");
1038
- document.querySelectorAll("#inn-actors canvas.buddy").forEach((canvas) => {
1039
- const handle = canvas.getAttribute("data-handle") || "";
1040
- drawBuddy(canvas, shirtColor(handle), seedOf(handle));
1041
- });
1042
1087
  }
1043
1088
  document.getElementById("enter-inn").addEventListener("click", () => {
1044
1089
  setScene("inn");
@@ -1062,11 +1107,16 @@
1062
1107
  }
1063
1108
  const bot = barBots.find((item) => item.id === id);
1064
1109
  if (!bot) return;
1065
- talk(bot.name, bot.oneLiner || t("studio.drink"), npcActs(bot));
1110
+ talk(bot.name, bot.oneLiner || t("studio.drink"), npcActs(bot), bot.portrait);
1066
1111
  });
1067
1112
  document.getElementById("rpg-acts").addEventListener("click", (event) => {
1068
1113
  const link = event.target.closest('a[href="#hire"]');
1069
1114
  if (link) document.getElementById("hire").hidden = false;
1115
+ const look = event.target.closest("[data-look]");
1116
+ if (look) requestLook(look.getAttribute("data-look"));
1117
+ });
1118
+ document.getElementById("gen-look").addEventListener("click", () => {
1119
+ if (EDIT_ID) requestLook(EDIT_ID);
1070
1120
  });
1071
1121
 
1072
1122
  async function boot() {
@@ -1087,7 +1137,11 @@
1087
1137
  }
1088
1138
  allSkills = mergeSkills(guild, host);
1089
1139
  const bots = await fetch("/bots").then((r) => r.json());
1090
- renderBar(Array.isArray(bots) ? bots : []);
1140
+ await renderBar(Array.isArray(bots) ? bots : []);
1141
+ if (location.hash === "#inn") {
1142
+ setScene("inn");
1143
+ talk(t("studio.keeper"), t("studio.keeperLine"), "");
1144
+ }
1091
1145
  if (EDIT_ID) {
1092
1146
  const response = await fetch("/bots/" + encodeURIComponent(EDIT_ID));
1093
1147
  const bot = await response.json();
@@ -1107,7 +1161,7 @@
1107
1161
  selected = new Set(bot.skillIds || []);
1108
1162
  setFlash("muted", t("studio.editing", { handle: bot.handle }));
1109
1163
  setScene("inn");
1110
- talk(bot.name, bot.oneLiner || t("studio.drink"), npcActs(bot));
1164
+ talk(bot.name, bot.oneLiner || t("studio.drink"), npcActs(bot), bot.portrait);
1111
1165
  } else {
1112
1166
  setScene("street");
1113
1167
  document.getElementById("hire").hidden = true;
@@ -1119,7 +1173,7 @@
1119
1173
  applyStudioCopy();
1120
1174
  render();
1121
1175
  renderBar(barBots);
1122
- if (rpgTalk.name) talk(rpgTalk.name, rpgTalk.line, rpgTalk.acts);
1176
+ if (rpgTalk.name) talk(rpgTalk.name, rpgTalk.line, rpgTalk.acts, rpgTalk.face);
1123
1177
  });
1124
1178
  boot().catch((err) => setFlash("error", err.message));
1125
1179
  </script>