poi-plugin-thief-akashi 0.3.2
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.
- package/LICENSE +21 -0
- package/README.md +0 -0
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/index.js +24 -0
- package/lib/popup-preload.js +36 -0
- package/lib/popup.html +18 -0
- package/lib/popup.js +55 -0
- package/lib/runtime.js +46 -0
- package/lib/settings.js +58 -0
- package/lib/timers.js +234 -0
- package/lib/view.js +55 -0
- package/package.json +40 -0
- package/third-party/logbook-LICENSE.txt +109 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Emiya6070
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 参考来源与许可
|
|
2
|
+
|
|
3
|
+
明石全局时钟的规则改写自 [航海日誌 扩张版](https://github.com/nekopanda/logbook),参考提交 `76fdfdc16bf4579caf471072adbef17856d53a60`:
|
|
4
|
+
|
|
5
|
+
- `main/logbook/data/context/GlobalContext.java` 的 `doPort` / `doChange`。
|
|
6
|
+
- `main/logbook/internal/AkashiTimer.java` 的 20 分钟门槛与按入渠时间推算每 HP 修理间隔的方法。
|
|
7
|
+
- `main/logbook/dto/DockDto.java` 的修理范围判定。
|
|
8
|
+
|
|
9
|
+
原项目使用 MIT 许可,原始完整许可随包保存于 `third-party/logbook-LICENSE.txt`(原始编码保留)。本项目未包含 Java 源码,也不包含舰娘图像或游戏资源。
|
|
10
|
+
|
|
11
|
+
本轮后续修理节点使用 `(api_ndock_time - 30 秒) / 缺失 HP` 推算间隔,节点向上取整到秒,20 分钟保底至少 1 HP。参考 [Wiki 明石泊地修理说明](https://wikiwiki.jp/kancolle/%E6%98%8E%E7%9F%B3)(2026-09-13 核对),不套用旧航海日誌额外的 60 秒保守偏移。节点为本地估计,实际修理量以回港数据为准;尚未计算朝日改带来的修理加成。
|
|
12
|
+
|
|
13
|
+
poi 插件接口核对 [poi v12.0.0](https://github.com/poooi/poi/tree/v12.0.0) 的 `read-plugin`、`lifecycle`、`plugin-window-wrapper` 与 `data-resolver`。本插件使用宿主提供的 React,不随插件安装另一份 React。
|
|
14
|
+
|
|
15
|
+
野埼机制补充参考 [舰これ Wiki 野埼](https://wikiwiki.jp/kancolle/%E9%87%8E%E5%9F%BC)(2026-09-13 读取)及 [poooi/plugin-anchorage-repair](https://github.com/poooi/plugin-anchorage-repair/tree/dd6de413d44895b7ea923ceeb6545bcc17deca73) 中的舰船 ID 与条件说明。野埼不是上述旧版航海日誌已经实现的功能。
|
|
16
|
+
|
|
17
|
+
到期回港无论是否实际给粮均重置野埼周期的规则,参考 [ぜかましねっと的母港给粮机制说明](https://zekamashi.net/kancolle-kouryaku/home-port-supply-ship-system/) 和 [野埼プリセット](https://note.com/shiroos/n/nba4295f43dca)(2026-09-13 核对)。
|
package/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const React = require('react');
|
|
4
|
+
const { createRuntime } = require('./lib/runtime');
|
|
5
|
+
|
|
6
|
+
const runtime = createRuntime();
|
|
7
|
+
const { createSettings, createSettingsView } = require('./lib/settings');
|
|
8
|
+
const { createPopup } = require('./lib/popup');
|
|
9
|
+
const settings = createSettings();
|
|
10
|
+
let popup = null;
|
|
11
|
+
|
|
12
|
+
// handleClick without reactClass is poi's window-only plugin interface.
|
|
13
|
+
exports.settingsClass = createSettingsView(React, settings);
|
|
14
|
+
exports.handleClick = () => popup.open();
|
|
15
|
+
exports.pluginDidLoad = () => {
|
|
16
|
+
settings.start(require('views/env').config);
|
|
17
|
+
runtime.start(window);
|
|
18
|
+
popup = createPopup(runtime, settings, require('@electron/remote').BrowserWindow);
|
|
19
|
+
};
|
|
20
|
+
exports.pluginWillUnload = () => {
|
|
21
|
+
popup?.close();
|
|
22
|
+
popup = null;
|
|
23
|
+
runtime.stop();
|
|
24
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ipcRenderer } = require('electron');
|
|
4
|
+
const { getRows } = require('./view');
|
|
5
|
+
|
|
6
|
+
// The popup receives display data only; Node and IPC are not exposed to its page.
|
|
7
|
+
ipcRenderer.on('poi-plugin-thief-akashi:display', (_event, { state, settings }) => {
|
|
8
|
+
const container = document.getElementById('timers');
|
|
9
|
+
container.style.fontSize = `${settings.fontSize}px`;
|
|
10
|
+
const rows = getRows(state, settings).map(row => {
|
|
11
|
+
const element = document.createElement('div');
|
|
12
|
+
element.className = `timer ${row.className}`;
|
|
13
|
+
element.setAttribute('role', 'timer');
|
|
14
|
+
element.setAttribute('aria-label', `${row.name}计时`);
|
|
15
|
+
element.title = row.title;
|
|
16
|
+
const label = document.createElement('span');
|
|
17
|
+
label.textContent = row.name;
|
|
18
|
+
const value = document.createElement('span');
|
|
19
|
+
value.className = 'value';
|
|
20
|
+
const time = document.createElement('span');
|
|
21
|
+
time.textContent = row.time;
|
|
22
|
+
const target = document.createElement('span');
|
|
23
|
+
target.className = row.targetClassName;
|
|
24
|
+
target.textContent = `/${row.target}${row.mark}`;
|
|
25
|
+
value.append(time, target);
|
|
26
|
+
if (row.next) {
|
|
27
|
+
const next = document.createElement('span');
|
|
28
|
+
next.className = row.nextClassName;
|
|
29
|
+
next.textContent = row.next;
|
|
30
|
+
value.append(next);
|
|
31
|
+
}
|
|
32
|
+
element.append(label, value);
|
|
33
|
+
return element;
|
|
34
|
+
});
|
|
35
|
+
container.replaceChildren(...rows);
|
|
36
|
+
});
|
package/lib/popup.html
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<meta charset="utf-8">
|
|
4
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">
|
|
5
|
+
<title>明石小偷</title>
|
|
6
|
+
<style>
|
|
7
|
+
body{margin:0;background:#20262d;font-family:system-ui,"Microsoft YaHei",sans-serif}
|
|
8
|
+
main{display:grid;gap:8px;padding:12px;font-weight:600;line-height:1.25;font-variant-numeric:tabular-nums}
|
|
9
|
+
.timer{display:flex;gap:12px;align-items:baseline;white-space:nowrap}
|
|
10
|
+
.value{display:flex;gap:8px;align-items:baseline}
|
|
11
|
+
.flagship{color:#24b47e}.not-flagship{color:#dca923}
|
|
12
|
+
.ready{color:#24b47e}.soon,.warning{color:#ef6464}.idle{color:#9ba4ad}
|
|
13
|
+
</style>
|
|
14
|
+
<main id="timers" style="font-size:28px">
|
|
15
|
+
<div class="timer not-flagship" role="timer" aria-label="明石计时"><span>明石</span><span class="value"><span>--:--</span><span class="idle">/20:00</span></span></div>
|
|
16
|
+
<div class="timer not-flagship" role="timer" aria-label="野崎计时"><span>野崎</span><span class="value"><span>--:--</span><span class="idle">/15:00</span></span></div>
|
|
17
|
+
</main>
|
|
18
|
+
</html>
|
package/lib/popup.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const CHANNEL = 'poi-plugin-thief-akashi:display';
|
|
5
|
+
|
|
6
|
+
function createPopup(runtime, settings, BrowserWindow) {
|
|
7
|
+
let popup = null;
|
|
8
|
+
let tick = null;
|
|
9
|
+
let unsubscribeRuntime = null;
|
|
10
|
+
let unsubscribeSettings = null;
|
|
11
|
+
const send = () => {
|
|
12
|
+
if (popup && !popup.isDestroyed()) {
|
|
13
|
+
popup.webContents.send(CHANNEL, { state: runtime.read(), settings: settings.read() });
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const cleanup = () => {
|
|
17
|
+
clearInterval(tick);
|
|
18
|
+
unsubscribeRuntime?.();
|
|
19
|
+
unsubscribeSettings?.();
|
|
20
|
+
tick = unsubscribeRuntime = unsubscribeSettings = null;
|
|
21
|
+
popup = null;
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
open() {
|
|
25
|
+
if (popup) {
|
|
26
|
+
if (popup.isMinimized()) popup.restore();
|
|
27
|
+
popup.show();
|
|
28
|
+
popup.focus();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
popup = new BrowserWindow({
|
|
32
|
+
title: '明石小偷', width: 440, height: 160, minWidth: 280, minHeight: 90,
|
|
33
|
+
show: false, autoHideMenuBar: true, backgroundColor: '#20262d',
|
|
34
|
+
webPreferences: {
|
|
35
|
+
preload: path.join(__dirname, 'popup-preload.js'),
|
|
36
|
+
nodeIntegration: false, contextIsolation: true, sandbox: false,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
popup.setMenu(null);
|
|
40
|
+
popup.webContents.on('did-finish-load', send);
|
|
41
|
+
popup.once('ready-to-show', () => { popup?.show(); });
|
|
42
|
+
popup.on('closed', cleanup);
|
|
43
|
+
unsubscribeRuntime = runtime.subscribe(send);
|
|
44
|
+
unsubscribeSettings = settings.subscribe(send);
|
|
45
|
+
tick = setInterval(send, 1000);
|
|
46
|
+
popup.loadFile(path.join(__dirname, 'popup.html')).catch(error => {
|
|
47
|
+
console.error('[明石小偷] 窗口加载失败', error);
|
|
48
|
+
popup?.close();
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
close() { popup?.close(); },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { createPopup, CHANNEL };
|
package/lib/runtime.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Timers, snapshot } = require('./timers');
|
|
4
|
+
|
|
5
|
+
function createRuntime() {
|
|
6
|
+
let timers = new Timers();
|
|
7
|
+
let host = null;
|
|
8
|
+
let error = null;
|
|
9
|
+
const listeners = new Set();
|
|
10
|
+
const emit = () => listeners.forEach(listener => listener());
|
|
11
|
+
const onResponse = event => {
|
|
12
|
+
const detail = event.detail;
|
|
13
|
+
if (!detail || typeof detail.path !== 'string') return;
|
|
14
|
+
try {
|
|
15
|
+
const info = host.getStore('info');
|
|
16
|
+
const constants = host.getStore('const');
|
|
17
|
+
timers.update(detail.path, detail.postBody || {}, snapshot(info, constants), Date.now());
|
|
18
|
+
error = null;
|
|
19
|
+
} catch (cause) {
|
|
20
|
+
error = `读取游戏数据失败:${cause.message}`;
|
|
21
|
+
console.error('[poi-plugin-thief-akashi]', cause);
|
|
22
|
+
}
|
|
23
|
+
emit();
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
start(window) {
|
|
27
|
+
if (host) return;
|
|
28
|
+
host = window;
|
|
29
|
+
host.addEventListener('game.response', onResponse);
|
|
30
|
+
},
|
|
31
|
+
stop() {
|
|
32
|
+
if (host) host.removeEventListener('game.response', onResponse);
|
|
33
|
+
host = null;
|
|
34
|
+
timers = new Timers();
|
|
35
|
+
error = null;
|
|
36
|
+
emit();
|
|
37
|
+
},
|
|
38
|
+
subscribe(listener) {
|
|
39
|
+
listeners.add(listener);
|
|
40
|
+
return () => listeners.delete(listener);
|
|
41
|
+
},
|
|
42
|
+
read: () => ({ ...timers.read(Date.now()), error }),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { createRuntime };
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const KEY = 'plugin.poi-plugin-thief-akashi.display';
|
|
4
|
+
const DEFAULTS = Object.freeze({ fontSize: 28, showAkashi: true, showNosaki: true });
|
|
5
|
+
|
|
6
|
+
function createSettings() {
|
|
7
|
+
let config = null;
|
|
8
|
+
let value = { ...DEFAULTS };
|
|
9
|
+
const listeners = new Set();
|
|
10
|
+
const emit = () => listeners.forEach(listener => listener());
|
|
11
|
+
return {
|
|
12
|
+
start(poiConfig) {
|
|
13
|
+
config = poiConfig;
|
|
14
|
+
value = { ...DEFAULTS, ...config.get(KEY, {}) };
|
|
15
|
+
emit();
|
|
16
|
+
},
|
|
17
|
+
read: () => ({ ...value }),
|
|
18
|
+
set(patch) {
|
|
19
|
+
const next = { ...value, ...patch };
|
|
20
|
+
if (!Number.isInteger(next.fontSize) || next.fontSize < 12 || next.fontSize > 72) {
|
|
21
|
+
throw new RangeError('字号须为 12–72 的整数');
|
|
22
|
+
}
|
|
23
|
+
if (typeof next.showAkashi !== 'boolean' || typeof next.showNosaki !== 'boolean') {
|
|
24
|
+
throw new TypeError('计时显示设置须为布尔值');
|
|
25
|
+
}
|
|
26
|
+
config.set(KEY, next);
|
|
27
|
+
value = next;
|
|
28
|
+
emit();
|
|
29
|
+
},
|
|
30
|
+
subscribe(listener) {
|
|
31
|
+
listeners.add(listener);
|
|
32
|
+
return () => listeners.delete(listener);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createSettingsView(React, settings) {
|
|
38
|
+
const h = React.createElement;
|
|
39
|
+
return function Settings() {
|
|
40
|
+
const [value, setValue] = React.useState(settings.read);
|
|
41
|
+
React.useEffect(() => settings.subscribe(() => setValue(settings.read())), []);
|
|
42
|
+
return h('div', { style: { display: 'grid', gap: 12, padding: '8px 0' } },
|
|
43
|
+
h('label', null, '字号 ', h('input', {
|
|
44
|
+
type: 'range', min: 12, max: 72, step: 1, value: value.fontSize,
|
|
45
|
+
'aria-label': '字号', onChange: e => settings.set({ fontSize: Number(e.target.value) }),
|
|
46
|
+
}), ` ${value.fontSize}px`),
|
|
47
|
+
h('label', null, h('input', {
|
|
48
|
+
type: 'checkbox', checked: value.showAkashi,
|
|
49
|
+
onChange: e => settings.set({ showAkashi: e.target.checked }),
|
|
50
|
+
}), ' 显示明石计时'),
|
|
51
|
+
h('label', null, h('input', {
|
|
52
|
+
type: 'checkbox', checked: value.showNosaki,
|
|
53
|
+
onChange: e => settings.set({ showNosaki: e.target.checked }),
|
|
54
|
+
}), ' 显示野崎计时'));
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { createSettings, createSettingsView, DEFAULTS };
|
package/lib/timers.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const AKASHI_MS = 20 * 60 * 1000;
|
|
4
|
+
const NOSAKI_MS = 15 * 60 * 1000;
|
|
5
|
+
const AKASHI_IDS = [182, 187];
|
|
6
|
+
const NOSAKI_IDS = [996, 1002];
|
|
7
|
+
const PORT = '/kcsapi/api_port/port';
|
|
8
|
+
const CHANGE = '/kcsapi/api_req_hensei/change';
|
|
9
|
+
|
|
10
|
+
function clock() {
|
|
11
|
+
return { startedAt: null, estimated: false, reason: '等待母港数据', warning: null };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function reset(timer, now, reason, estimated = false) {
|
|
15
|
+
Object.assign(timer, { startedAt: now, estimated, reason, warning: null });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Copy only the game fields we use. A previous snapshot must not change when
|
|
19
|
+
// poi updates its store; never retain API tokens or complete response bodies.
|
|
20
|
+
function snapshot(info, constants) {
|
|
21
|
+
return {
|
|
22
|
+
memberId: info.basic?.api_member_id,
|
|
23
|
+
fuel: info.resources?.[0],
|
|
24
|
+
fleets: (info.fleets || []).map(fleet => ({
|
|
25
|
+
id: fleet.api_id,
|
|
26
|
+
name: fleet.api_name,
|
|
27
|
+
ids: [...fleet.api_ship],
|
|
28
|
+
expedition: Boolean(fleet.api_mission?.[0]),
|
|
29
|
+
})),
|
|
30
|
+
ships: Object.fromEntries(Object.entries(info.ships || {}).map(([id, ship]) => [id, {
|
|
31
|
+
id: ship.api_id, masterId: ship.api_ship_id,
|
|
32
|
+
hp: ship.api_nowhp, maxHp: ship.api_maxhp,
|
|
33
|
+
dockTime: ship.api_ndock_time,
|
|
34
|
+
name: constants.$ships?.[ship.api_ship_id]?.api_name || `舰船 #${ship.api_id}`,
|
|
35
|
+
cond: ship.api_cond, fuel: ship.api_fuel, ammo: ship.api_bull,
|
|
36
|
+
slots: [...ship.api_slot],
|
|
37
|
+
}])),
|
|
38
|
+
docked: new Set((info.repairs || []).map(dock => dock.api_ship_id).filter(id => id > 0)),
|
|
39
|
+
facilities: new Set(Object.values(info.equips || {})
|
|
40
|
+
.filter(item => item.api_slotitem_id === 86).map(item => item.api_id)),
|
|
41
|
+
masters: Object.fromEntries(Object.entries(constants.$ships || {}).map(([id, ship]) => [id, {
|
|
42
|
+
fuel: ship.api_fuel_max, ammo: ship.api_bull_max,
|
|
43
|
+
}])),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function inspect(state) {
|
|
48
|
+
return state.fleets.map(fleet => {
|
|
49
|
+
const flagship = state.ships[fleet.ids[0]];
|
|
50
|
+
const akashi = flagship && AKASHI_IDS.includes(flagship.masterId);
|
|
51
|
+
const repairReasons = [];
|
|
52
|
+
let repairShips = [];
|
|
53
|
+
let repairTargets = [];
|
|
54
|
+
if (akashi) {
|
|
55
|
+
if (fleet.expedition) repairReasons.push('远征中');
|
|
56
|
+
if (state.docked.has(flagship.id)) repairReasons.push('明石入渠中');
|
|
57
|
+
if (flagship.hp <= flagship.maxHp / 2) repairReasons.push('明石中破及以上');
|
|
58
|
+
const capacity = 2 + flagship.slots.filter(id => state.facilities.has(id)).length;
|
|
59
|
+
repairShips = fleet.ids.slice(0, capacity).map(id => state.ships[id]).filter(Boolean);
|
|
60
|
+
repairTargets = repairShips.filter(ship =>
|
|
61
|
+
ship && !state.docked.has(ship.id) && ship.hp > ship.maxHp / 2 && ship.hp < ship.maxHp);
|
|
62
|
+
if (!repairTargets.length) repairReasons.push('修理范围内无可修理舰');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const nosaki = fleet.ids.slice(0, 2).map(id => state.ships[id])
|
|
66
|
+
.find(ship => ship && NOSAKI_IDS.includes(ship.masterId));
|
|
67
|
+
const moraleReasons = [];
|
|
68
|
+
let moraleShips = [];
|
|
69
|
+
let moraleTargets = [];
|
|
70
|
+
if (nosaki) {
|
|
71
|
+
const master = state.masters[nosaki.masterId];
|
|
72
|
+
if (!master) moraleReasons.push('等待舰船基础数据');
|
|
73
|
+
else if (nosaki.fuel !== master.fuel || nosaki.ammo !== master.ammo) moraleReasons.push('野埼未补满');
|
|
74
|
+
if (nosaki.hp <= nosaki.maxHp * 0.75) moraleReasons.push('野埼小破及以上');
|
|
75
|
+
if (nosaki.cond < 30) moraleReasons.push('野埼士气低于 30');
|
|
76
|
+
if (fleet.expedition) moraleReasons.push('远征中');
|
|
77
|
+
if (state.docked.has(nosaki.id)) moraleReasons.push('野埼入渠中');
|
|
78
|
+
if (state.fuel === 0) moraleReasons.push('母港燃料不足');
|
|
79
|
+
moraleShips = fleet.ids.map(id => state.ships[id]).filter(ship => ship && ship.id !== nosaki.id);
|
|
80
|
+
moraleTargets = moraleShips.filter(ship => !state.docked.has(ship.id) && ship.cond < 54);
|
|
81
|
+
if (!moraleTargets.length) moraleReasons.push('无可提升士气的随伴舰');
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
...fleet, akashi: Boolean(akashi), repairReasons, repairShips, repairTargets,
|
|
85
|
+
canRepair: Boolean(akashi) && !repairReasons.length,
|
|
86
|
+
nosaki, moraleReasons, moraleShips, moraleTargets,
|
|
87
|
+
canBoost: Boolean(nosaki) && !moraleReasons.length,
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A timeline records cumulative gains if no port settlement intervenes.
|
|
93
|
+
// All gains below the minimum wait collapse into the first guaranteed point.
|
|
94
|
+
function timeline(ship, damage, pointAt) {
|
|
95
|
+
const points = [];
|
|
96
|
+
for (let gain = 1; gain <= damage; gain++) {
|
|
97
|
+
const at = pointAt(gain);
|
|
98
|
+
if (points.length && points[points.length - 1].at === at) points[points.length - 1].gain = gain;
|
|
99
|
+
else points.push({ at, gain });
|
|
100
|
+
}
|
|
101
|
+
return { shipId: ship.id, name: ship.name, points };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function progress(timelines, elapsed, period, detail, unit, missing = []) {
|
|
105
|
+
const nextPoints = timelines.flatMap(ship => ship.points.filter(point => elapsed !== null && point.at > elapsed));
|
|
106
|
+
const nextAt = elapsed !== null && elapsed >= period && nextPoints.length
|
|
107
|
+
? Math.min(...nextPoints.map(point => point.at)) : null;
|
|
108
|
+
const lines = timelines.map(ship => `${ship.name}:${ship.points.map(point => `${formatTime(point.at)}(+${point.gain}${unit})`).join(' → ')}`);
|
|
109
|
+
if (missing.length) lines.push(`${missing.join('、')}:缺少有效入渠时间,无法估算后续节点`);
|
|
110
|
+
return { timelines, nextAt, nextIn: nextAt === null ? null : nextAt - elapsed, detail: [detail, ...lines].join('\n') };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function repairProgress(fleets, elapsed) {
|
|
114
|
+
const missing = [];
|
|
115
|
+
const timelines = fleets.filter(fleet => fleet.canRepair).flatMap(fleet => fleet.repairTargets.flatMap(ship => {
|
|
116
|
+
const damage = ship.maxHp - ship.hp;
|
|
117
|
+
// api_ndock_time includes a fixed 30 seconds. Anchorage repair does not;
|
|
118
|
+
// derive each HP interval, then round its node up to a whole second.
|
|
119
|
+
if (!Number.isFinite(ship.dockTime) || ship.dockTime <= 30000) {
|
|
120
|
+
missing.push(ship.name);
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
const perHp = (ship.dockTime - 30000) / damage;
|
|
124
|
+
return [timeline(ship, damage, gain => Math.max(AKASHI_MS, Math.ceil(perHp * gain / 1000) * 1000))];
|
|
125
|
+
}));
|
|
126
|
+
// The first HP is guaranteed at 20 minutes, including slow-repair ships.
|
|
127
|
+
for (const ship of timelines) {
|
|
128
|
+
if (ship.points[0].at > AKASHI_MS) ship.points[0].at = AKASHI_MS;
|
|
129
|
+
}
|
|
130
|
+
return progress(timelines, elapsed, AKASHI_MS,
|
|
131
|
+
'本轮修理量估计(以回港结果为准);+时间为下一修理节点相对 20:00 的偏移,已满或预计修满的舰不再提供节点', 'HP', missing);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function moraleProgress(fleets, elapsed) {
|
|
135
|
+
const timelines = fleets.filter(fleet => fleet.canBoost).flatMap(fleet => {
|
|
136
|
+
const step = fleet.nosaki.masterId === 1002 ? 3 : 2;
|
|
137
|
+
return fleet.moraleTargets.map(ship => timeline(ship, 54 - ship.cond, gain => Math.ceil(gain / step) * NOSAKI_MS));
|
|
138
|
+
});
|
|
139
|
+
return progress(timelines, elapsed, NOSAKI_MS,
|
|
140
|
+
'本轮给粮量估计:野崎每 15 分钟 +2,改 +3,上限 cond 54;未计自然恢复。+时间为下一节点相对 15:00 的偏移。到期回港即重新计时,不判断给粮成功或失败', 'cond');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
class Timers {
|
|
144
|
+
constructor() {
|
|
145
|
+
this.akashi = clock();
|
|
146
|
+
this.nosaki = clock();
|
|
147
|
+
this.previous = null;
|
|
148
|
+
this.fleets = [];
|
|
149
|
+
this.lastResponseAt = null;
|
|
150
|
+
this.memberId = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
update(path, postBody, current, now) {
|
|
154
|
+
// Wait for a live port response; poi may initially expose cached account data.
|
|
155
|
+
if (path === PORT && current.memberId !== this.memberId) {
|
|
156
|
+
this.akashi = clock();
|
|
157
|
+
this.nosaki = clock();
|
|
158
|
+
this.previous = null;
|
|
159
|
+
this.memberId = current.memberId;
|
|
160
|
+
}
|
|
161
|
+
if (!this.previous && path !== PORT) return;
|
|
162
|
+
const previous = this.previous;
|
|
163
|
+
const before = previous ? inspect(previous) : [];
|
|
164
|
+
const after = inspect(current);
|
|
165
|
+
|
|
166
|
+
if (path === PORT) {
|
|
167
|
+
// Only a gain in the previous eligible repair range confirms repair.
|
|
168
|
+
// Battle damage, unrelated ships and docking completions are not evidence.
|
|
169
|
+
const repaired = before.some(fleet => fleet.canRepair && after.some(item => item.id === fleet.id && item.akashi) &&
|
|
170
|
+
fleet.repairTargets.some(old => current.ships[old.id]?.hp > old.hp && !current.docked.has(old.id)));
|
|
171
|
+
if (this.akashi.startedAt === null) {
|
|
172
|
+
reset(this.akashi, now, '首次母港观测,起点估计', true);
|
|
173
|
+
} else if (repaired && now - this.akashi.startedAt >= AKASHI_MS) {
|
|
174
|
+
reset(this.akashi, now, '母港检测到修理范围内 HP 增加');
|
|
175
|
+
} else if (now - this.akashi.startedAt > AKASHI_MS && after.some(fleet => fleet.akashi)) {
|
|
176
|
+
// Keep this warning latched, even for full-HP fleets. An estimated port
|
|
177
|
+
// recalibration must not silently restore a supposedly ready timer.
|
|
178
|
+
this.akashi.warning = '超过 20 分钟回港未检测到 HP 变化,请重新触发明石计时';
|
|
179
|
+
} else if (!this.akashi.warning && !before.some(fleet => fleet.canRepair) && now - this.akashi.startedAt > AKASHI_MS) {
|
|
180
|
+
reset(this.akashi, now, '无可修理编成,母港校时', true);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (this.nosaki.startedAt === null && after.some(fleet => fleet.nosaki)) {
|
|
184
|
+
reset(this.nosaki, now, '首次观测野埼,起点估计', true);
|
|
185
|
+
} else if (previous && this.nosaki.startedAt !== null && now - this.nosaki.startedAt >= NOSAKI_MS) {
|
|
186
|
+
// A due port response consumes the global feeding cycle even when
|
|
187
|
+
// Nosaki is absent or feeding conditions fail. Actual morale gains are
|
|
188
|
+
// not the reset trigger; keep them separate from timer readiness.
|
|
189
|
+
reset(this.nosaki, now, '母港给粮到期判定');
|
|
190
|
+
}
|
|
191
|
+
} else if (path === CHANGE && Number(postBody.api_ship_id) !== -2) {
|
|
192
|
+
const targetId = Number(postBody.api_id);
|
|
193
|
+
const sourceId = before.find(fleet => fleet.ids.includes(Number(postBody.api_ship_id)))?.id;
|
|
194
|
+
const affected = after.filter(fleet => fleet.id === targetId || fleet.id === sourceId);
|
|
195
|
+
if (affected.some(fleet => fleet.akashi)) reset(this.akashi, now, '明石舰队编成变更');
|
|
196
|
+
if (affected.some(fleet => fleet.nosaki)) reset(this.nosaki, now, '野埼舰队编成变更');
|
|
197
|
+
}
|
|
198
|
+
// Presets, bulk removal, resupply, equipment changes, sorties and expedition
|
|
199
|
+
// result screens do not by themselves reset these global observation clocks.
|
|
200
|
+
// Removing a support ship keeps its clock, allowing preset-based operation.
|
|
201
|
+
this.previous = current;
|
|
202
|
+
this.fleets = after;
|
|
203
|
+
this.lastResponseAt = now;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
read(now) {
|
|
207
|
+
// Both sides describe this cycle only. Later gain nodes do not move the
|
|
208
|
+
// minimum threshold or consume a cycle; only a reset event can do that.
|
|
209
|
+
const describe = (timer, period, canTrigger) => ({
|
|
210
|
+
...timer, period, detail: '', nextAt: null, nextIn: null, timelines: [],
|
|
211
|
+
elapsed: timer.startedAt === null ? null : Math.max(0, now - timer.startedAt),
|
|
212
|
+
target: period,
|
|
213
|
+
ready: !timer.warning && timer.startedAt !== null && now - timer.startedAt >= period && canTrigger,
|
|
214
|
+
remaining: timer.startedAt === null ? null : Math.max(0, period - (now - timer.startedAt)),
|
|
215
|
+
overdue: timer.startedAt === null ? 0 : Math.max(0, now - timer.startedAt - period),
|
|
216
|
+
});
|
|
217
|
+
const akashi = describe(this.akashi, AKASHI_MS, this.fleets.some(fleet => fleet.canRepair));
|
|
218
|
+
const nosaki = describe(this.nosaki, NOSAKI_MS, this.fleets.some(fleet => fleet.canBoost));
|
|
219
|
+
return {
|
|
220
|
+
akashi: { ...akashi, ...(this.akashi.warning ? {} : repairProgress(this.fleets, akashi.elapsed)) },
|
|
221
|
+
nosaki: { ...nosaki, ...moraleProgress(this.fleets, nosaki.elapsed) },
|
|
222
|
+
fleets: this.fleets,
|
|
223
|
+
lastResponseAt: this.lastResponseAt,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function formatTime(ms, roundUp = true) {
|
|
229
|
+
if (ms === null) return '--:--';
|
|
230
|
+
const seconds = (roundUp ? Math.ceil : Math.floor)(Math.max(0, ms) / 1000);
|
|
231
|
+
return `${String(Math.floor(seconds / 60)).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = { Timers, snapshot, inspect, formatTime, AKASHI_MS, NOSAKI_MS };
|
package/lib/view.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { formatTime } = require('./timers');
|
|
4
|
+
const { DEFAULTS } = require('./settings');
|
|
5
|
+
|
|
6
|
+
function getRows(state, settings = DEFAULTS) {
|
|
7
|
+
return [
|
|
8
|
+
{ name: '明石', value: state.akashi, shown: settings.showAkashi, flagship: state.fleets.some(fleet => fleet.akashi) },
|
|
9
|
+
{ name: '野崎', value: state.nosaki, shown: settings.showNosaki, flagship: state.fleets.some(fleet => fleet.nosaki && fleet.ids[0] === fleet.nosaki.id) },
|
|
10
|
+
].filter(row => row.shown).map(row => {
|
|
11
|
+
const { elapsed, target, ready, detail, warning, nextAt, nextIn } = row.value;
|
|
12
|
+
const gap = target - elapsed;
|
|
13
|
+
const active = !state.error && elapsed !== null;
|
|
14
|
+
const targetClassName = !state.error && warning ? 'warning' : active && ready ? 'ready' : active && gap > 0 && gap <= 60000 ? 'soon' : 'idle';
|
|
15
|
+
return {
|
|
16
|
+
name: row.name,
|
|
17
|
+
time: state.error ? '--:--' : formatTime(elapsed, false),
|
|
18
|
+
target: state.error ? '--:--' : formatTime(target),
|
|
19
|
+
next: active && ready && nextAt != null ? `+${formatTime(nextAt - target)}` : '',
|
|
20
|
+
nextClassName: nextIn > 0 && nextIn <= 60000 ? 'soon' : 'idle',
|
|
21
|
+
mark: targetClassName === 'ready' ? '√' : '',
|
|
22
|
+
targetClassName,
|
|
23
|
+
title: state.error || warning || `${detail || `${row.name}计时`};${ready ? '当前编成满足条件,可回港触发' : '到时且编成满足条件才会显示 √'}`,
|
|
24
|
+
className: !state.error && warning ? 'warning' : !state.error && row.flagship ? 'flagship' : 'not-flagship',
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Shared display model; this React wrapper is used by the development preview.
|
|
30
|
+
function createView(React, runtime, settings = {read: () => DEFAULTS, subscribe: () => () => {}}) {
|
|
31
|
+
const h = React.createElement;
|
|
32
|
+
return function ThiefAkashi() {
|
|
33
|
+
const [state, setState] = React.useState(() => runtime.read());
|
|
34
|
+
const [options, setOptions] = React.useState(settings.read);
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
const refresh = () => setState(runtime.read());
|
|
37
|
+
const unsubscribe = runtime.subscribe(refresh);
|
|
38
|
+
const unsubscribeSettings = settings.subscribe(() => setOptions(settings.read()));
|
|
39
|
+
const tick = setInterval(refresh, 1000);
|
|
40
|
+
refresh();
|
|
41
|
+
return () => { unsubscribe(); unsubscribeSettings(); clearInterval(tick); };
|
|
42
|
+
}, []);
|
|
43
|
+
return h('main', {style: {padding:12, display:'grid', gap:8, fontSize:options.fontSize, fontWeight:600, fontFamily:'system-ui', fontVariantNumeric:'tabular-nums'}},
|
|
44
|
+
getRows(state, options).map(row => h('div', {
|
|
45
|
+
key:row.name, role:'timer', 'aria-label':`${row.name}计时`, title:row.title,
|
|
46
|
+
className:row.className, style:{display:'flex', gap:12, whiteSpace:'nowrap', color:{warning:'#ef6464',flagship:'#24b47e','not-flagship':'#dca923'}[row.className]},
|
|
47
|
+
}, h('span', null, row.name), h('span', {style:{display:'flex',gap:8,alignItems:'baseline'}},
|
|
48
|
+
h('span', null, row.time), h('span', {className:row.targetClassName,
|
|
49
|
+
style:{color:{warning:'#ef6464',ready:'#24b47e',soon:'#ef6464',idle:'#9ba4ad'}[row.targetClassName]}},
|
|
50
|
+
`/${row.target}${row.mark}`), row.next && h('span', {className:row.nextClassName,
|
|
51
|
+
style:{color:row.nextClassName === 'soon' ? '#ef6464' : '#9ba4ad'}}, row.next)))));
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { createView, getRows };
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "poi-plugin-thief-akashi",
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "编队白嫖明石/野崎计时工具",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"lib",
|
|
9
|
+
"LICENSE",
|
|
10
|
+
"THIRD_PARTY_NOTICES.md",
|
|
11
|
+
"third-party"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test test/*.test.js",
|
|
15
|
+
"check": "node --check index.js && node --check lib/timers.js && node --check lib/runtime.js && node --check lib/view.js && node --check lib/settings.js && node --check lib/popup.js && node --check lib/popup-preload.js",
|
|
16
|
+
"preview": "node scripts/preview.js"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Emiya6070/poi-plugin-thief-akashi.git"
|
|
21
|
+
},
|
|
22
|
+
"author": "Emiya6070",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"poi-plugin",
|
|
26
|
+
"kancolle",
|
|
27
|
+
"akashi",
|
|
28
|
+
"nosaki"
|
|
29
|
+
],
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"react": "18.3.1",
|
|
32
|
+
"react-dom": "18.3.1"
|
|
33
|
+
},
|
|
34
|
+
"poiPlugin": {
|
|
35
|
+
"title": "明石小偷",
|
|
36
|
+
"description": "编队白嫖明石/野崎计时工具",
|
|
37
|
+
"icon": "fa/clock-o",
|
|
38
|
+
"priority": 50
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014-2015 �q�C�����g���ŊJ����
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
�q�C�����g���ŊJ����
|
|
24
|
+
- Nekopanda
|
|
25
|
+
- �����iPush�ʒm�j
|
|
26
|
+
- twk@2ch�i�����ۃX�N���v�g�j
|
|
27
|
+
- asahui�i���K�o���l�v�Z�@�j
|
|
28
|
+
- taxi-wind�i�͑��^�u�̈ꕔ�\���j
|
|
29
|
+
|
|
30
|
+
------------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
�{�\�t�g�E�F�A�͍q�C�������x�[�X�ɂ��Ă��܂��B
|
|
33
|
+
�q�C�����̃��C�Z���X�͈ȉ��̒ʂ�
|
|
34
|
+
|
|
35
|
+
The MIT License (MIT)
|
|
36
|
+
|
|
37
|
+
Copyright (c) 2013-2015 sanae_hirotaka
|
|
38
|
+
|
|
39
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
40
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
41
|
+
in the Software without restriction, including without limitation the rights
|
|
42
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
43
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
44
|
+
furnished to do so, subject to the following conditions:
|
|
45
|
+
|
|
46
|
+
The above copyright notice and this permission notice shall be included in
|
|
47
|
+
all copies or substantial portions of the Software.
|
|
48
|
+
|
|
49
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
50
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
51
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
52
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
53
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
54
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
55
|
+
THE SOFTWARE.
|
|
56
|
+
|
|
57
|
+
------------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
�����ꗗ�̃X�N���v�g��twk@2ch����ɍ���Ă��������܂����B
|
|
60
|
+
�����ꗗ�X�N���v�g�̃��C�Z���X�͈ȉ��̒ʂ�
|
|
61
|
+
|
|
62
|
+
The MIT License (MIT)
|
|
63
|
+
|
|
64
|
+
Copyright (c) 2015 twk@2ch
|
|
65
|
+
|
|
66
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
67
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
68
|
+
in the Software without restriction, including without limitation the rights
|
|
69
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
70
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
71
|
+
furnished to do so, subject to the following conditions:
|
|
72
|
+
|
|
73
|
+
The above copyright notice and this permission notice shall be included in
|
|
74
|
+
all copies or substantial portions of the Software.
|
|
75
|
+
|
|
76
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
77
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
78
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
79
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
80
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
81
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
82
|
+
THE SOFTWARE.
|
|
83
|
+
|
|
84
|
+
------------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
�C���i���X�N���v�g��Nishisonic����ɍ���Ă��������܂����B
|
|
87
|
+
�C���i���X�N���v�g�̃��C�Z���X�͈ȉ��̒ʂ�
|
|
88
|
+
|
|
89
|
+
The MIT License (MIT)
|
|
90
|
+
|
|
91
|
+
Copyright (c) 2015 Nishisonic
|
|
92
|
+
|
|
93
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
94
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
95
|
+
in the Software without restriction, including without limitation the rights
|
|
96
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
97
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
98
|
+
furnished to do so, subject to the following conditions:
|
|
99
|
+
|
|
100
|
+
The above copyright notice and this permission notice shall be included in
|
|
101
|
+
all copies or substantial portions of the Software.
|
|
102
|
+
|
|
103
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
104
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
105
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
106
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
107
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
108
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
109
|
+
THE SOFTWARE.
|