Skip to content

Instantly share code, notes, and snippets.

@sounisi5011
Last active May 22, 2020 06:47
Show Gist options
  • Save sounisi5011/946c96f18c1314bec70569cd7a82d3e6 to your computer and use it in GitHub Desktop.
Save sounisi5011/946c96f18c1314bec70569cd7a82d3e6 to your computer and use it in GitHub Desktop.
Starbound用JSONジェネレーター

Starbound用JSONジェネレーター

依頼されて作った雑アプリ。StarboundのMod制作で必要なJSONデータファイル(*.object)を生成する。

動作ページ

機能

  • 入力欄に入力してJSONファイルを作成できます。
  • 作成したJSONファイルをダウンロードできます。
  • 作成したJSONファイルをプレビューで確認できます。
  • 入力欄の内容はブラウザのsessionStorageに一時保存されます。保存された内容はウィンドウ・タブ毎に、タブを閉じるまで維持されます。リロードしても消えません。

現在の制限

  • 生成されるJSONの改行コードはLFです。改行コードLFに非対応のテキストエディタ(Windowsに付属している「メモ帳」など)では、JSONの改行が維持されません。
  • 複数行の文字列を入力できるのは次のフィールドのみです:
    • interactData.filter内の JSON 値の入力欄
    • description
    • 種族別のアイテムの説明文:
      • apexDescription
      • avianDescription
      • floranDescription
      • glitchDescription
      • humanDescription
      • hylotlDescription
      • novakidDescription
    • orientations[0]spaces内の JSON 値の入力欄
  • 数値の入力欄には次の制限を設けています:
    • priceに指定可能な値は0以上1刻みの数値です。例えば、03999999は指定できますが、0.1-1.75は指定できません。
    • orientations[0]imagePositionに指定可能な数値は1刻みの数値です。例えば、0-468738は指定できますが、0.121.64-3.7は指定できません。
    • orientations[0]framesに指定可能な値は0以上1刻みの数値です。例えば、0187は指定できますが、0.13-0.8などは指定できません。
    • orientations[0]animationCycleに指定可能な値は0以上0.01刻みの数値です。例えば、011.18.75は指定できますが、0.0061.013-6は指定できません。
/*
* [email protected]で<input type=number>もリサイズするように初期化
*/
((selector) => {
Stretchy.selectors.base += `, ${selector}`;
Stretchy.resizeAll(document.querySelectorAll(selector));
})("input[type=number]");
<!DOCTYPE html>
<html lang=ja>
<meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<meta name=format-detection content="telephone=no,email=no,address=no">
<title>Starbound用JSONジェネレーター</title>
<link rel=stylesheet href="./main.css">
<link rel=stylesheet href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.0.3/styles/vs2015.min.css" integrity="sha256-e+xyu+n0Lf/CcIJO7HoFhKOu97JVj09p39g25tzh6is=" crossorigin=anonymous>
<h1>Starbound用JSONジェネレーター</h1>
<form id=root-form><pre>
{
"objectName": "<input type=text name=objectName id=input-object-name required>",
"colonyTags": <output name=colonyTags for=input-object-name></output>,
"printable": <select name=printable data-value-set="true false"></select>,
"price": <input type=number name=price min=0 value=0>,
"rarity": <select name=rarity data-value-set="Common Uncommon Rare Legendary" data-value-replace='"%s"'></select>,
<span id=group-interact> "interactAction": "<input type=text name=interactAction value="OpenCraftingInterface">",
"interactData": {
"config": "/interface/windowconfig/<input type=text name=interactData.config>.config",
"filter": [
<textarea name=interactData.filter></textarea>
]
},</span>
"description": "<textarea name=description></textarea>",
"shortdescription": "<input type=text name=shortdescription>",
"race": <select name=race data-value-set="generic apex avian hylotl floran human glitch novakid" data-value-replace='"%s"'></select>,
"category": "<input type=text name=category>",
"apexDescription": "<textarea name=apexDescription data-input-group=species-description></textarea>",
"avianDescription": "<textarea name=avianDescription data-input-group=species-description></textarea>",
"floranDescription": "<textarea name=floranDescription data-input-group=species-description></textarea>",
"glitchDescription": "<textarea name=glitchDescription data-input-group=species-description></textarea>",
"humanDescription": "<textarea name=humanDescription data-input-group=species-description></textarea>",
"hylotlDescription": "<textarea name=hylotlDescription data-input-group=species-description></textarea>",
"novakidDescription": "<textarea name=novakidDescription data-input-group=species-description></textarea>",
"learnBlueprintsOnPickup": [<input type=text name=learnBlueprintsOnPickup>],
"inventoryIcon": <output name=inventoryIcon for=input-object-name></output>,
<span id=group-orientations> <label><input type=checkbox name=orientations-visibility checked> "orientations": [</label>
{
"dualImage": <output name=orientations.dualImage for=input-object-name></output>,
"direction": "<input type=text name=orientations.direction value=left>",
"flipimages": <select name=orientations.flipimages data-value-set="true false"></select>,
"imagePosition": [<input type=number name=orientations.imagePosition[0] value=0>, <input type=number name=orientations.imagePosition[1] value=0>],
"frames": <input type=number name=orientations.frames min=0 value=1>,
"animationCycle": <input type=number name=orientations.animationCycle min=0 step=0.01 value=1>,
"spaces": [
<textarea name=orientations.spaces></textarea>
],
<span id=group-orientations-collision> "collision": "<input type=text name=orientations.collision>",</span>
"anchors": [<select name=orientations.anchors data-value-set="bottom background" data-value-replace='"%s"'></select>]
}
]</span>
}
</pre></form>
<p><input type=button value="ダウンロード" id=download-button></p>
<details>
<summary>JSONプレビュー</summary>
<pre id=preview-area class=lang-json></pre>
</details>
<footer>
<h2>Gist</h2>
<p><a href="https://gist.github.com/sounisi5011/946c96f18c1314bec70569cd7a82d3e6">gist.github.com<wbr>/sounisi5011<wbr>/946c96f18c1314bec70569cd7a82d3e6</a>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stretchy/1.1.0/stretchy.min.js" integrity="sha256-l3bv+G97DLhWJ5U6QV8qbNPYCcqv6AWzu5DH/fsrjRw=" crossorigin=anonymous defer data-filter="input, textarea"></script>
<script src="./fix-stretchy.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/json5/0.5.1/json5.min.js" integrity="sha256-v8sXFkSe2BIXdw4jViVp7u0iImlKg4T834KxIMzZ2VQ=" crossorigin=anonymous defer></script>
<script src="https://unpkg.com/[email protected]/index.js" crossorigin=anonymous defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.0.3/highlight.min.js" integrity="sha256-/2C3CAfmuTGkUqK2mVrhkTacBscoR1caE0u2QZZ3Uh8=" crossorigin=anonymous defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.0.3/languages/json.min.js" integrity="sha256-6YP8JQrs9CYJQqz+ol+0hZowCn+z/TEvgkayTdzCSkA=" crossorigin=anonymous defer></script>
<script src="./main.js" defer></script>
details {
border: solid 1px #ccc;
padding: 0.5em;
}
details > summary {
cursor: pointer;
}
input,
textarea,
select {
vertical-align: text-top;
}
input,
textarea {
border: solid 1px #ccc;
padding: 0.2em 0.5em;
}
input {
min-width: 5em;
}
input[type="number"] {
min-width: 3em;
}
textarea {
width: calc(100% - 20em);
min-width: 15em;
min-height: 3em;
}
input:invalid,
textarea:invalid {
border-color: red;
outline-color: red;
background-color: PaleVioletRed;
}
.hljs[data-filename]:before {
content: attr(data-filename);
display: block;
margin-top: -0.5em;
margin-left: -0.5em;
padding-bottom: 0.5em;
}
#group-interact.hidden,
#group-orientations.hidden,
#group-orientations-collision.hidden {
color: rgba(0, 0, 0, 0.1);
}
#group-orientations.hidden input:not([name="orientations-visibility"]),
#group-orientations.hidden textarea,
#group-orientations.hidden select,
#group-interact.hidden > input:not(#input-interact-action) {
opacity: 0.2;
}
/*
* 定数を定義
*/
const STORAGE_KEY =
"https://gist.github.com/sounisi5011/946c96f18c1314bec70569cd7a82d3e6";
/*
* 汎用関数を定義
*/
let loadedData;
/*
* sessionStorageからデータを取得する
*/
function loadData() {
if (loadedData) return loadedData;
const dataText = sessionStorage.getItem(STORAGE_KEY);
if (dataText) {
try {
loadedData = JSON.parse(dataText);
return loadedData;
} catch (error) {
if (!(error instanceof SyntaxError)) throw error;
}
}
return null;
}
/*
* sessionStorageにデータを設定する
*/
function setData(object) {
const newData = Object.assign({}, loadData(), object);
loadedData = newData;
const dataText = JSON.stringify(newData);
sessionStorage.setItem(STORAGE_KEY, dataText);
}
/*
* JSON5断片を入力欄から読み取る
* Note: 正式なJSONにはコメントも書けないが、JSON5はコメントも書けるしプロパティ名をクォートで囲む必要もない便利なもの。
*/
function readJson5InputValue(elem, replacer) {
const jsonText = replacer(`\n${elem.value}\n`);
try {
const jsonData = JSON5.parse(jsonText);
elem.setCustomValidity("");
return jsonData;
} catch (e) {
elem.setCustomValidity("不正な形式のJSONです");
return undefined;
}
}
// ----- ----- ----- ----- ----- //
/*
* data-value-set属性を持つselect要素内にoption要素を追加
*/
document.querySelectorAll("select[data-value-set]").forEach((elem) => {
const valueList = elem.dataset.valueSet.split(/\s*[\s,]\s*/g);
const replaceText = elem.dataset.valueReplace;
valueList.forEach((value) => {
const optionElem = document.createElement("option");
if (replaceText) {
optionElem.value = value;
optionElem.textContent = replaceText.replace(/%s/g, value);
} else {
optionElem.textContent = value;
}
elem.appendChild(optionElem);
});
});
/*
* HTMLの要素に対応するDOM要素オブジェクトを取得
*/
const rootFormElem = document.getElementById("root-form");
const rootFormItems = rootFormElem.elements;
const speciesDescriptionInputElemList = [
...document.querySelectorAll("[data-input-group=species-description]"),
];
const interactGroupElem = document.getElementById("group-interact");
const orientationsGroupElem = document.getElementById("group-orientations");
const collisionGroupElem = document.getElementById(
"group-orientations-collision"
);
const dlButtonElem = document.getElementById("download-button");
const previewAreaElem = document.getElementById("preview-area");
/*
* JSONオブジェクト、JSON文字列、ファイル名を生成する。また、生成の際に、HTML上の表示も更新する
*/
function generateJsonData() {
const objectName = rootFormItems.objectName.value;
const interactDataConfigName =
rootFormItems["interactData.config"].value || objectName;
const description = rootFormItems.description.value;
/*
* 種族別のアイテムの説明文を生成
* 全ての入力値が空文字列の場合は、descriptionの内容を挿入する。
*/
const isEmptyAllSpeciesDescription = speciesDescriptionInputElemList
.map((elem) => elem.value)
.every((text) => text === "");
const speciesDescriptionData = {};
speciesDescriptionInputElemList.forEach((elem) => {
const name = elem.name;
let defaultSpeciesDescription = "";
speciesDescriptionData[name] = isEmptyAllSpeciesDescription
? (defaultSpeciesDescription = description)
: elem.value;
elem.placeholder = defaultSpeciesDescription;
Stretchy.resize(elem);
});
/*
* JSONデータを生成
*/
const jsonData = Object.assign(
{
objectName: objectName,
colonyTags: [objectName],
printable: rootFormItems.printable.value === "true",
price: rootFormItems.price.valueAsNumber,
rarity: rootFormItems.rarity.value,
interactAction: rootFormItems.interactAction.value,
interactData: {
config: `/interface/windowconfig/${interactDataConfigName}.config`,
filter: readJson5InputValue(
rootFormItems["interactData.filter"],
(s) => `[${s}]`
),
},
description: description,
shortdescription: rootFormItems.shortdescription.value,
race: rootFormItems.race.value,
category: rootFormItems.category.value,
},
speciesDescriptionData,
{
learnBlueprintsOnPickup: readJson5InputValue(
rootFormItems.learnBlueprintsOnPickup,
(s) => `[${s}]`
),
inventoryIcon: `${objectName}Icon.png`,
orientations: [
{
dualImage: `${objectName}.png:<color>.<flame>`,
direction: rootFormItems["orientations.direction"].value,
flipimages: rootFormItems["orientations.flipimages"].value === "true",
imagePosition: [
rootFormItems["orientations.imagePosition[0]"].valueAsNumber,
rootFormItems["orientations.imagePosition[1]"].valueAsNumber,
],
frames: rootFormItems["orientations.frames"].valueAsNumber,
animationCycle:
rootFormItems["orientations.animationCycle"].valueAsNumber,
spaces: readJson5InputValue(
rootFormItems["orientations.spaces"],
(s) => `[${s}]`
),
collision: rootFormItems["orientations.collision"].value || undefined,
anchors: [rootFormItems["orientations.anchors"].value],
},
],
}
);
/*
* colonyTagsの値を表示
*/
rootFormItems.colonyTags.textContent = JSON.stringify(jsonData.colonyTags);
/*
* interactActionが空文字列の場合はinteractActionとinteractDataを削除
*/
if (jsonData.interactAction === "") {
delete jsonData.interactAction;
delete jsonData.interactData;
interactGroupElem.classList.add("hidden");
} else {
interactGroupElem.classList.remove("hidden");
}
/*
* interactData内のconfigの初期値を設定
*/
rootFormItems["interactData.config"].placeholder = interactDataConfigName;
Stretchy.resize(rootFormItems["interactData.config"]);
/*
* inventoryIconの値を表示
*/
rootFormItems.inventoryIcon.textContent = JSON.stringify(
jsonData.inventoryIcon
);
rootFormItems["orientations.dualImage"].textContent = JSON.stringify(
jsonData.orientations[0].dualImage
);
/*
* orientations内のcollisionが未入力の場合はcollisionの入力欄を半透明表示に変更
*/
collisionGroupElem.classList.toggle(
"hidden",
!jsonData.orientations[0].collision
);
/*
* orientationsのチェックボックスが外れている場合はorientationsを削除
*/
if (rootFormItems["orientations-visibility"].checked) {
orientationsGroupElem.classList.remove("hidden");
} else {
delete jsonData.orientations;
orientationsGroupElem.classList.add("hidden");
}
/*
* sessionStorageに入力欄のデータを一時保存
*/
setData(
Array.from(rootFormItems)
.filter(
(elem) => elem.name && /^(?:input|textarea|select)$/i.test(elem.tagName)
)
.reduce((saveData, elem) => {
saveData[elem.name] = /^checkbox$/i.test(elem.type)
? elem.checked
: elem.value;
return saveData;
}, {})
);
/*
* 入力欄の入力内容が正しいか検証
*/
if (!rootFormElem.checkValidity()) {
delete previewAreaElem.dataset.filename;
previewAreaElem.textContent =
"入力された値が間違っています。赤の入力欄を修正してください";
return null;
}
/*
* ファイル名を生成
*/
const filenameStr = `${jsonData.objectName}.object`;
/*
* データをJSON文字列に変換
*/
const jsonText = prettyJSONStringify(jsonData, {
// インデントはスペース2文字
tab: " ",
// 配列の"["と"]"の内側にスペース1文字を入れる
spaceInsideArray: " ",
shouldExpand(obj, level, index) {
// 直下のinteractDataフィールドの内容は常に折り返す
if (level === 1 && index === "interactData") return true;
// 80文字を超える長さの値は折り返す
return JSON.stringify(obj).length > 80;
},
})
// 数字だけが入った配列の"["と"]"の内側にあるスペース文字を削除
.replace(
/\[\s*((?:\s*(?:,\s*)?-?\d+(?:\.\d+)?(?:[Ee][+-]?\d+)?)+)\s*\]/g,
(_, v) => `[${v}]`
);
/*
* JSONのプレビューを更新する
*/
// ファイル名を設定
previewAreaElem.dataset.filename = filenameStr;
// JSON文字列をHTMLに反映
previewAreaElem.textContent = jsonText;
// highlight.jsでシンタックスハイライトを適用
hljs.highlightBlock(previewAreaElem);
return {
obj: jsonData,
filename: filenameStr,
text: jsonText,
};
}
/*
* 初期化する
*/
function init() {
/*
* sessionStorageの一時保存データを入力欄に反映する
*/
const savedData = loadData();
if (savedData) {
Object.keys(savedData).forEach((name) => {
const formItemElem = rootFormItems[name];
if (formItemElem) {
if (/^checkbox$/i.test(formItemElem.type)) {
formItemElem.checked = savedData[name];
} else {
formItemElem.value = savedData[name];
Stretchy.resize(formItemElem);
}
}
});
}
generateJsonData();
}
/*
* 入力処理を設定
*/
rootFormElem.addEventListener("input", generateJsonData);
rootFormElem.addEventListener("change", generateJsonData);
/*
* フォームの送信を禁止
*/
rootFormElem.addEventListener("submit", (event) => {
event.preventDefault();
});
/*
* ダウンロードボタンの処理を設定
*/
dlButtonElem.addEventListener("click", () => {
/*
* フォームの入力内容を検証
*/
if (!rootFormElem.reportValidity()) return;
const jsonData = generateJsonData();
if (!jsonData) {
alert("入力された値が間違っています。赤の入力欄を修正してください");
return;
}
/*
* JSON文字列を元に、ファイルダウンロード用のオブジェクトURLを生成
*/
const blob = new Blob([jsonData.text], { type: "application/json" });
const downloadURL = URL.createObjectURL(blob);
/*
* オブジェクトURLを設定したa要素をクリックし、ダウンロードダイアログを表示
*/
const aElem = document.createElement("a");
aElem.href = downloadURL;
aElem.download = jsonData.filename;
aElem.click();
});
init();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment