@kevin5251984/guild 0.2.12 → 0.2.14

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.
@@ -95,12 +95,7 @@
95
95
  return;
96
96
  }
97
97
  const button = document.getElementById("ai-generate");
98
- if (button.disabled || button.getAttribute("aria-busy") === "true") return;
99
98
  button.disabled = true;
100
- button.setAttribute("aria-busy", "true");
101
- button.classList.add("is-busy");
102
- const idle = button.textContent;
103
- button.textContent = t("studio.generating");
104
99
  try {
105
100
  const response = await fetch("/generate", {
106
101
  method: "POST",
@@ -120,9 +115,6 @@
120
115
  );
121
116
  } finally {
122
117
  button.disabled = false;
123
- button.removeAttribute("aria-busy");
124
- button.classList.remove("is-busy");
125
- button.textContent = idle;
126
118
  }
127
119
  });
128
120
 
@@ -51,32 +51,24 @@
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">
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>
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>
68
63
  <div class="inn-actors" id="inn-actors"></div>
69
64
  </div>
70
65
  <button type="button" class="rpg-exit" id="leave-inn" data-i18n="studio.leave">出門</button>
71
66
  </div>
72
67
  <div class="rpg-dialog" id="rpg-dialog">
73
68
  <div class="rpg-dialog-inner">
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>
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>
80
72
  </div>
81
73
  </div>
82
74
  </div>
@@ -96,7 +88,6 @@
96
88
  <input name="oneLiner" placeholder="這名冒險者在編制裡做什麼" data-i18n-placeholder="studio.oneLinerPh" />
97
89
  <div class="card-foot">
98
90
  <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>
100
91
  <div class="save-row">
101
92
  <p class="block-flash" hidden></p>
102
93
  <button type="button" class="save-md" data-save="identity" hidden>儲存名字</button>
@@ -192,7 +183,7 @@
192
183
  let openId = "";
193
184
  let activeTags = new Set();
194
185
  let showAllSkills = false;
195
- let rpgTalk = { name: "", line: "", acts: "", face: "" };
186
+ let rpgTalk = { name: "", line: "", acts: "" };
196
187
  const PICKED = "__picked__";
197
188
  const REST_CAP = 12;
198
189
 
@@ -206,8 +197,6 @@
206
197
  document.title = t("studio.editDocTitle");
207
198
  submit.hidden = true;
208
199
  document.getElementById("delete-bot").hidden = false;
209
- document.getElementById("gen-look").hidden = false;
210
- document.getElementById("gen-look").textContent = t("studio.genLook");
211
200
  mdSaves.forEach((btn) => {
212
201
  btn.hidden = false;
213
202
  const kind = btn.getAttribute("data-save");
@@ -223,7 +212,6 @@
223
212
  submit.hidden = false;
224
213
  submit.textContent = t("studio.seat");
225
214
  document.getElementById("delete-bot").hidden = true;
226
- document.getElementById("gen-look").hidden = true;
227
215
  document.title = t("title.studio");
228
216
  mdSaves.forEach((btn) => {
229
217
  btn.hidden = true;
@@ -250,24 +238,6 @@
250
238
  function setBlockFlash(block, kind, text) {
251
239
  setNodeFlash(block && block.querySelector(".gen-row .block-flash"), kind, text);
252
240
  }
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
- }
271
241
  function saveFlashEl(kind) {
272
242
  const btn = document.querySelector('.save-md[data-save="' + kind + '"]');
273
243
  return btn && btn.parentElement ? btn.parentElement.querySelector(".block-flash") : null;
@@ -577,7 +547,8 @@
577
547
  setNodeFlash(flashEl, "error", t("studio.emptyLib"));
578
548
  return;
579
549
  }
580
- if (!beginBusy(button, t("studio.picking"))) return;
550
+ button.disabled = true;
551
+ button.textContent = t("studio.picking");
581
552
  try {
582
553
  const response = await fetch("/generate/skills", {
583
554
  method: "POST",
@@ -626,7 +597,8 @@
626
597
  } catch (err) {
627
598
  setNodeFlash(flashEl, "error", err.message);
628
599
  } finally {
629
- endBusy(button, t("studio.pickSkills"));
600
+ button.disabled = false;
601
+ button.textContent = t("studio.pickSkills");
630
602
  }
631
603
  });
632
604
  document.getElementById("skill-search").addEventListener("input", () => {
@@ -716,7 +688,8 @@
716
688
  setBlockFlash(block, "error", t("studio.needPrompt", { kind: kind }));
717
689
  return;
718
690
  }
719
- if (!beginBusy(button, t("studio.generating"))) return;
691
+ button.disabled = true;
692
+ button.textContent = t("studio.generating");
720
693
  try {
721
694
  const response = await fetch("/generate", {
722
695
  method: "POST",
@@ -756,7 +729,8 @@
756
729
  } catch (err) {
757
730
  setBlockFlash(block, "error", err.message);
758
731
  } finally {
759
- endBusy(button, t("studio.genMd"));
732
+ button.disabled = false;
733
+ button.textContent = t("studio.genMd");
760
734
  }
761
735
  });
762
736
  });
@@ -913,29 +887,80 @@
913
887
  const roster = await fetch("/bots").then((r) => r.json());
914
888
  renderBar(Array.isArray(roster) ? roster : []);
915
889
  setScene("inn");
916
- talk(body.name, t("studio.hireToast"), npcActs(body), body.portrait);
890
+ talk(body.name, t("studio.hireToast"), npcActs(body));
917
891
  });
918
892
 
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
+ ];
919
953
  let barBots = [];
920
954
  function setScene(name) {
921
955
  document.getElementById("scene-street").hidden = name !== "street";
922
956
  document.getElementById("scene-inn").hidden = name !== "inn";
923
957
  document.getElementById("rpg-screen").setAttribute("data-scene", name);
924
958
  }
925
- function talk(name, line, acts, face) {
926
- rpgTalk = { name: name, line: line, acts: acts || "", face: face || "" };
959
+ function talk(name, line, acts) {
960
+ rpgTalk = { name: name, line: line, acts: acts || "" };
927
961
  document.getElementById("rpg-name").textContent = name;
928
962
  document.getElementById("rpg-line").textContent = line;
929
963
  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
- }
939
964
  }
940
965
  function npcActs(bot) {
941
966
  return (
@@ -950,69 +975,6 @@
950
975
  "</a>"
951
976
  );
952
977
  }
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
- }
1016
978
  function renderSideRoster(bots) {
1017
979
  const hire = document.getElementById("side-hire");
1018
980
  const box = document.getElementById("side-roster");
@@ -1035,55 +997,48 @@
1035
997
  })
1036
998
  .join("");
1037
999
  }
1038
- async function renderBar(bots) {
1000
+ function renderBar(bots) {
1039
1001
  barBots = bots || [];
1040
1002
  renderSideRoster(barBots);
1041
1003
  const bottles = ["#c44", "#4a8", "#c84", "#48c", "#a4c", "#cc8", "#844", "#4cc"];
1042
1004
  document.getElementById("inn-bottles").innerHTML = bottles
1043
1005
  .map((c) => '<span style="background:' + c + '"></span>')
1044
1006
  .join("");
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) => {
1007
+ const roster = barBots.slice(0, SPOTS.length - 1);
1008
+ const nodes = roster.map((bot, i) => {
1009
+ const spot = SPOTS[i];
1059
1010
  const on = EDIT_ID === bot.id ? " on" : "";
1060
- const live = Boolean(diving[bot.id]);
1061
- return station(
1011
+ return (
1062
1012
  '<button type="button" class="rpg-npc' +
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>",
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>"
1075
1025
  );
1076
1026
  });
1027
+ const hireSpot = SPOTS[Math.min(roster.length, SPOTS.length - 1)];
1077
1028
  nodes.push(
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
- ),
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>",
1085
1036
  );
1086
1037
  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
+ });
1087
1042
  }
1088
1043
  document.getElementById("enter-inn").addEventListener("click", () => {
1089
1044
  setScene("inn");
@@ -1107,16 +1062,11 @@
1107
1062
  }
1108
1063
  const bot = barBots.find((item) => item.id === id);
1109
1064
  if (!bot) return;
1110
- talk(bot.name, bot.oneLiner || t("studio.drink"), npcActs(bot), bot.portrait);
1065
+ talk(bot.name, bot.oneLiner || t("studio.drink"), npcActs(bot));
1111
1066
  });
1112
1067
  document.getElementById("rpg-acts").addEventListener("click", (event) => {
1113
1068
  const link = event.target.closest('a[href="#hire"]');
1114
1069
  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);
1120
1070
  });
1121
1071
 
1122
1072
  async function boot() {
@@ -1137,11 +1087,7 @@
1137
1087
  }
1138
1088
  allSkills = mergeSkills(guild, host);
1139
1089
  const bots = await fetch("/bots").then((r) => r.json());
1140
- await renderBar(Array.isArray(bots) ? bots : []);
1141
- if (location.hash === "#inn") {
1142
- setScene("inn");
1143
- talk(t("studio.keeper"), t("studio.keeperLine"), "");
1144
- }
1090
+ renderBar(Array.isArray(bots) ? bots : []);
1145
1091
  if (EDIT_ID) {
1146
1092
  const response = await fetch("/bots/" + encodeURIComponent(EDIT_ID));
1147
1093
  const bot = await response.json();
@@ -1161,7 +1107,7 @@
1161
1107
  selected = new Set(bot.skillIds || []);
1162
1108
  setFlash("muted", t("studio.editing", { handle: bot.handle }));
1163
1109
  setScene("inn");
1164
- talk(bot.name, bot.oneLiner || t("studio.drink"), npcActs(bot), bot.portrait);
1110
+ talk(bot.name, bot.oneLiner || t("studio.drink"), npcActs(bot));
1165
1111
  } else {
1166
1112
  setScene("street");
1167
1113
  document.getElementById("hire").hidden = true;
@@ -1173,7 +1119,7 @@
1173
1119
  applyStudioCopy();
1174
1120
  render();
1175
1121
  renderBar(barBots);
1176
- if (rpgTalk.name) talk(rpgTalk.name, rpgTalk.line, rpgTalk.acts, rpgTalk.face);
1122
+ if (rpgTalk.name) talk(rpgTalk.name, rpgTalk.line, rpgTalk.acts);
1177
1123
  });
1178
1124
  boot().catch((err) => setFlash("error", err.message));
1179
1125
  </script>