Merge branch 'refs/heads/dev' into syntax-highlighter-unified

This commit is contained in:
bigguy345 committed 2026-03-11 03:34:28 +02:00
commit 88d34a970c
42 files changed
+4602 -84

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
<script>
(function() {
// Fix uppercase text in table cells
// Javadoc 11: method names are in th[scope="row"] a, not td a
function fixCase() {
document.querySelectorAll("th[scope='row'] a, td a, .memberNameLink a").forEach(function(el) {
var t = el.textContent.trim();
if (t.length > 2 && t === t.toUpperCase() && t.indexOf(" ") === -1) {
el.textContent = t.toLowerCase();
}
});
}
// Correct navPadding = topNav height + subNav height
// Uses !important to override jQuery's inline style
function fixPadding() {
var topNav = document.querySelector(".topNav");
var subNav = document.querySelector(".subNav");
var pad = document.querySelector(".navPadding");
if (!pad) return;
var h = (topNav ? topNav.offsetHeight : 0) + (subNav ? subNav.offsetHeight : 0);
if (h > 0) {
pad.style.setProperty("padding-top", h + "px", "important");
}
}
document.addEventListener("DOMContentLoaded", function() {
fixCase();
// Run fixPadding after jQuery has had its chance
setTimeout(fixPadding, 0);
setTimeout(fixPadding, 50);
setTimeout(fixPadding, 200);
// Use MutationObserver to permanently override any further jQuery writes
var pad = document.querySelector(".navPadding");
if (pad) {
var observer = new MutationObserver(function() {
fixPadding();
});
observer.observe(pad, { attributes: true, attributeFilter: ["style"] });
}
});
})();
</script>
+11
View File
@@ -0,0 +1,11 @@
<div id="javadoc-header">
<a href="/CustomNPC-Plus/" class="header-brand">
<div class="header-title">
CustomNPC+
<span>Javadoc Reference</span>
</div>
</a>
<a href="/CustomNPC-Plus/" class="header-back">
Back to Index
</a>
</div>
+954
View File
@@ -0,0 +1,954 @@
/* ============================================================
CustomNPC+ — Javadoc Theme (Javadoc 11)
Tokyo Night — Styled Edition
============================================================ */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
:root {
/* ── Tokyo Night palette ── */
--bg: #1a1b26;
--bg2: #1f2335;
--bg3: #24283b;
--bg-elevated: #292e42;
--border: #3b4261;
--border2: #414868;
--text: #c0caf5;
--text-mid: #a9b1d6;
--text-dim: #565f89;
--green: #9ece6a;
--green-dim: rgba(158, 206, 106, 0.12);
--blue: #7aa2f7;
--blue-dim: rgba(122, 162, 247, 0.12);
--amber: #e0af68;
--amber-dim: rgba(224, 175, 104, 0.10);
--purple: #bb9af7;
--purple-dim: rgba(187, 154, 247, 0.10);
--cyan: #7dcfff;
--cyan-dim: rgba(125, 207, 255, 0.10);
--red: #f7768e;
--highlight-bg: rgba(122, 162, 247, 0.08);
--hover-bg: rgba(187, 154, 247, 0.08);
--nav-h: 44px;
--subnav-h: 36px;
/* ── Font stacks ── */
--font-ui: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
}
* { box-sizing: border-box !important; }
html, body {
height: 100%;
}
body {
background: var(--bg) !important;
color: var(--text) !important;
font-family: var(--font-mono) !important;
font-size: 13px !important;
line-height: 1.6 !important;
margin: 0 !important;
padding: 0 !important;
min-height: 100vh !important;
display: flex !important;
flex-direction: column !important;
}
.skipNav { display: none !important; }
/* ══════════════════════════════════════════════
NAVIGATION
══════════════════════════════════════════════ */
.fixedNav {
position: fixed !important;
top: 0 !important; left: 0 !important; right: 0 !important;
z-index: 100 !important;
}
/* Top bar — gradient accent stripe on top */
.topNav {
position: relative !important;
background: var(--bg2) !important;
border-bottom: 1px solid var(--border) !important;
height: var(--nav-h) !important;
margin: 0 !important; padding: 0 !important;
display: flex !important;
align-items: stretch !important;
overflow: visible !important;
}
/* Gradient accent line across the very top */
.topNav::before {
content: '' !important;
position: absolute !important;
top: 0 !important; left: 0 !important; right: 0 !important;
height: 2px !important;
background: linear-gradient(90deg, var(--blue), var(--purple), var(--cyan)) !important;
z-index: 10 !important;
}
/* Nav list — leave room on right for brand */
.topNav > ul.navList {
list-style: none !important;
margin: 0 !important;
padding: 2px 0 0 8px !important;
padding-right: 320px !important;
display: flex !important;
align-items: stretch !important;
flex-shrink: 0 !important;
}
.topNav > ul.navList > li {
display: flex !important;
align-items: center !important;
margin: 0 !important;
padding: 0 16px !important;
height: var(--nav-h) !important;
font-family: var(--font-ui) !important;
font-size: 12px !important;
font-weight: 500 !important;
white-space: nowrap !important;
text-transform: none !important;
letter-spacing: 0.01em !important;
color: var(--text-dim) !important;
border-bottom: 2px solid transparent !important;
transition: color 0.2s !important;
}
.topNav > ul.navList > li > a {
display: flex !important;
align-items: center !important;
height: var(--nav-h) !important;
margin: 0 -16px !important;
padding: 0 16px !important;
color: var(--text-dim) !important;
font-family: var(--font-ui) !important;
font-size: 12px !important;
font-weight: 500 !important;
text-decoration: none !important;
text-transform: none !important;
letter-spacing: 0.01em !important;
white-space: nowrap !important;
border-bottom: 2px solid transparent !important;
transition: color 0.2s, border-color 0.2s !important;
}
.topNav > ul.navList > li > a:hover {
color: var(--text) !important;
border-bottom-color: var(--purple) !important;
background: transparent !important;
text-shadow: none !important;
text-decoration: none !important;
}
/* Active item — gradient underline */
.topNav > ul.navList > li.navBarCell1Rev {
color: var(--text) !important;
border-image: linear-gradient(90deg, var(--blue), var(--purple)) 1 !important;
border-bottom-width: 2px !important;
border-bottom-style: solid !important;
}
/* ── Brand — pinned to far right of topNav ── */
.topNav > .aboutLanguage {
position: absolute !important;
top: 0 !important; right: 0 !important;
height: var(--nav-h) !important;
display: flex !important;
align-items: center !important;
gap: 12px !important;
padding: 0 16px !important;
background: var(--bg2) !important;
border-left: 1px solid var(--border) !important;
z-index: 5 !important;
}
/* Hide the duplicate in bottom nav */
.bottomNav > .aboutLanguage,
.bottomNav .aboutLanguage {
display: none !important;
}
#javadoc-header {
display: flex !important;
align-items: center !important;
gap: 12px !important;
}
.header-brand { text-decoration: none !important; }
/* Brand title — gradient text */
.header-title {
font-family: var(--font-ui) !important;
font-size: 14px !important;
font-weight: 700 !important;
background: linear-gradient(135deg, var(--blue), var(--purple)) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
background-clip: text !important;
line-height: 1.4 !important;
white-space: nowrap !important;
}
.header-title span {
display: block !important;
font-family: var(--font-mono) !important;
font-size: 9px !important;
font-weight: 400 !important;
background: none !important;
-webkit-text-fill-color: var(--text-dim) !important;
color: var(--text-dim) !important;
margin-top: 1px !important;
letter-spacing: 0.1em !important;
text-transform: uppercase !important;
}
.header-back {
display: inline-flex !important;
align-items: center !important;
text-decoration: none !important;
color: var(--text-dim) !important;
font-family: var(--font-ui) !important;
font-size: 11px !important;
font-weight: 500 !important;
padding: 5px 12px !important;
border: 1px solid var(--border2) !important;
border-radius: 6px !important;
white-space: nowrap !important;
transition: all 0.2s !important;
}
.header-back:hover {
color: var(--text) !important;
border-color: var(--purple) !important;
background: var(--purple-dim) !important;
text-decoration: none !important;
text-shadow: none !important;
}
/* ── Sub nav ── */
.subNav {
background: var(--bg3) !important;
border-bottom: 1px solid var(--border) !important;
height: var(--subnav-h) !important;
margin: 0 !important;
padding: 0 14px !important;
display: flex !important;
align-items: center !important;
gap: 12px !important;
overflow: hidden !important;
}
/* Search first */
.subNav > ul.navListSearch {
order: -1 !important;
}
.subNav ul,
.subNav ul.navList,
.subNav ul.subNavList,
.subNav ul.navListSearch {
list-style: none !important;
margin: 0 !important;
padding: 0 !important;
display: flex !important;
align-items: center !important;
gap: 0 !important;
}
.subNav li {
display: flex !important;
align-items: center !important;
font-family: var(--font-ui) !important;
font-size: 11px !important;
color: var(--text-dim) !important;
padding: 0 6px !important;
white-space: nowrap !important;
border: none !important;
height: auto !important;
text-transform: none !important;
}
.subNav a {
color: var(--blue) !important;
text-decoration: none !important;
font-size: 11px !important;
font-family: var(--font-ui) !important;
transition: color 0.15s !important;
}
.subNav a:hover { color: var(--purple) !important; text-shadow: none !important; }
.subNav label {
font-family: var(--font-ui) !important;
font-size: 11px !important;
color: var(--text-dim) !important;
text-transform: none !important;
margin-right: 4px !important;
}
/* ══════════════════════════════════════════════
CONTENT
══════════════════════════════════════════════ */
main {
background: var(--bg) !important;
padding: 28px !important;
flex: 1 0 auto !important;
}
.contentContainer, div.contentContainer {
background: var(--bg) !important;
padding: 0 28px 0px !important;
}
.header {
padding: 0 28px 0 !important;
margin-bottom: 20px !important;
}
/* Subtitle under h1 on help/overview pages */
.header .subTitle,
div.subTitle {
color: var(--text-mid) !important;
font-family: var(--font-ui) !important;
font-size: 13px !important;
line-height: 1.7 !important;
margin-top: 8px !important;
}
.packageLabelInType {
font-family: var(--font-mono) !important;
font-size: 11px !important;
color: var(--text-dim) !important;
}
/* ── Headings ── */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-ui) !important;
font-weight: 600 !important;
line-height: 1.5 !important;
text-transform: none !important;
letter-spacing: -0.01em !important;
}
/* Page title — gradient accent bar */
h1.title, h2.title {
font-size: 22px !important;
font-weight: 700 !important;
color: var(--text) !important;
border-bottom: none !important;
padding-bottom: 16px !important;
padding-left: 16px !important;
margin-bottom: 24px !important;
margin-top: 0 !important;
border-left: 4px solid transparent !important;
border-image: linear-gradient(180deg, var(--blue), var(--purple)) 1 !important;
}
/* Section h2 — colored panel */
section > h2,
li.blockList > section > h2 {
font-size: 14px !important;
color: var(--green) !important;
border-left: 3px solid var(--green) !important;
border-bottom: none !important;
padding: 10px 14px !important;
margin: 0 !important;
background: var(--green-dim) !important;
}
/* Generic h2 (table section headers like "Method Summary") */
h2 {
font-size: 14px !important;
color: var(--amber) !important;
border-bottom: none !important;
border-left: 3px solid var(--amber) !important;
padding: 8px 12px !important;
margin-top: 28px !important;
margin-bottom: 12px !important;
background: var(--amber-dim) !important;
}
/* h3 — table section headers inside blockList */
h3 {
font-size: 12px !important;
color: var(--text-mid) !important;
margin: 0 !important;
padding: 10px 14px !important;
background: var(--bg3) !important;
border-bottom: 1px solid var(--border) !important;
}
/* h4 — method detail names */
h4 {
font-size: 13px !important;
font-weight: 600 !important;
color: var(--cyan) !important;
margin: 0 !important;
padding: 10px 14px !important;
background: var(--bg3) !important;
border-bottom: 1px solid var(--border) !important;
border-left: 3px solid var(--cyan) !important;
}
/* ── blockList sections — card-like containers ── */
ul.blockList > li.blockList,
ul.blockListLast > li.blockList {
border: 1px solid var(--border) !important;
border-radius: 6px !important;
margin-bottom: 14px !important;
padding: 0 !important;
background: var(--bg2) !important;
overflow: hidden !important;
}
/* Help page: section cards with padding around their content */
ul.blockList > li.blockList > section {
padding: 0 !important;
}
ul.blockList > li.blockList > section > p,
ul.blockList > li.blockList > section > ul:not(.blockList):not(.blockListLast) {
padding: 14px 18px !important;
margin: 0 !important;
color: var(--text-mid) !important;
font-family: var(--font-ui) !important;
font-size: 13px !important;
line-height: 1.7 !important;
}
/* Nested lists inside section content */
ul.blockList > li.blockList > section > ul:not(.blockList):not(.blockListLast) > li {
color: var(--text-mid) !important;
font-family: var(--font-ui) !important;
font-size: 13px !important;
padding: 3px 0 !important;
list-style: none !important;
}
ul.blockList > li.blockList > section > ul:not(.blockList):not(.blockListLast) > li::before {
content: '\203A' !important;
color: var(--purple) !important;
margin-right: 8px !important;
font-weight: 700 !important;
}
/* ── Inherited methods block ── */
ul.blockList > li.blockList > h3,
ul.blockList > li.blockList > ul.blockList > li.blockList > h3 {
font-size: 11px !important;
color: var(--text-dim) !important;
padding: 8px 14px !important;
background: var(--bg3) !important;
border-bottom: 1px solid var(--border) !important;
margin: 0 !important;
}
/* The comma-separated code block of inherited method names */
ul.blockList > li.blockList > code,
li.blockList > code {
display: block !important;
padding: 10px 16px !important;
color: var(--text-dim) !important;
font-family: var(--font-mono) !important;
font-size: 12px !important;
background: var(--bg) !important;
border: none !important;
line-height: 1.8 !important;
}
/* ── Inheritance chain ── */
ul.inheritance {
list-style: none !important;
padding: 0 !important;
margin: 0 0 16px !important;
font-size: 12px !important;
color: var(--text-dim) !important;
}
ul.inheritance li { padding-left: 16px !important; }
ul.inheritance li:first-child { padding-left: 0 !important; }
/* ── General content lists (help page) ── */
main ul:not(.blockList):not(.blockListLast):not(.navList):not(.subNavList):not(.navListSearch):not(.inheritance) {
padding-left: 0 !important;
margin: 4px 0 4px 12px !important;
list-style: none !important;
}
main ul:not(.blockList):not(.blockListLast):not(.navList):not(.subNavList):not(.navListSearch):not(.inheritance) > li {
color: var(--text-mid) !important;
font-family: var(--font-ui) !important;
font-size: 13px !important;
padding: 3px 0 !important;
line-height: 1.7 !important;
}
main ul:not(.blockList):not(.blockListLast):not(.navList):not(.subNavList):not(.navListSearch):not(.inheritance) > li::before {
content: '\203A' !important;
color: var(--purple) !important;
margin-right: 8px !important;
font-weight: 700 !important;
}
/* General paragraphs in content */
main > div p, .contentContainer p {
color: var(--text-mid) !important;
font-family: var(--font-ui) !important;
font-size: 13px !important;
line-height: 1.7 !important;
margin: 8px 0 !important;
}
/* Emphasized phrase */
.emphasizedPhrase {
display: block !important;
color: var(--text-dim) !important;
font-family: var(--font-mono) !important;
font-size: 12px !important;
font-style: italic !important;
border-left: 2px solid var(--purple) !important;
padding: 8px 14px !important;
margin-top: 16px !important;
background: var(--purple-dim) !important;
border-radius: 0 4px 4px 0 !important;
}
/* Bottom footer subNav */
footer {
/*margin-top: auto;*/
}
footer .subNav {
background: var(--bg3) !important;
border-top: 1px solid var(--border) !important;
border-bottom: none !important;
height: var(--subnav-h) !important;
padding: 0 14px !important;
display: flex !important;
align-items: center !important;
gap: 12px !important;
overflow: hidden !important;
}
/* ══════════════════════════════════════════════
TABLES
══════════════════════════════════════════════ */
table {
width: 100% !important;
border-collapse: collapse !important;
border: 1px solid var(--border) !important;
border-radius: 6px !important;
margin: 16px 0 !important;
font-size: 13px !important;
overflow: hidden !important;
}
caption {
background: var(--bg3) !important;
padding: 8px 14px !important;
text-align: left !important;
border-bottom: 1px solid var(--border2) !important;
caption-side: top !important;
}
/* Table tabs — pill style */
.activeTableTab, .tableTab {
font-family: var(--font-ui) !important;
font-size: 11px !important;
font-weight: 500 !important;
text-transform: none !important;
padding: 4px 12px !important;
border: 1px solid var(--border2) !important;
border-radius: 20px !important;
display: inline-block !important;
margin-right: 6px !important;
cursor: pointer !important;
transition: all 0.2s !important;
}
.activeTableTab {
color: var(--text) !important;
border-color: var(--blue) !important;
background: var(--blue-dim) !important;
}
.tableTab { color: var(--text-dim) !important; background: var(--bg3) !important; }
.tableTab a { color: var(--text-dim) !important; text-decoration: none !important; }
.tableTab a:hover { color: var(--text) !important; text-shadow: none !important; }
/* Column headers */
th[scope="col"] {
background: var(--bg-elevated) !important;
color: var(--text-dim) !important;
font-family: var(--font-ui) !important;
font-size: 10px !important;
font-weight: 600 !important;
letter-spacing: 0.06em !important;
text-transform: uppercase !important;
padding: 10px 14px !important;
border-bottom: 2px solid var(--border2) !important;
text-align: left !important;
}
/* Data cells */
th[scope="row"], td {
background: var(--bg2) !important;
color: var(--text) !important;
padding: 10px 14px !important;
border-bottom: 1px solid var(--border) !important;
vertical-align: top !important;
text-align: left !important;
font-family: var(--font-mono) !important;
font-size: 12px !important;
font-weight: 400 !important;
text-transform: none !important;
letter-spacing: 0 !important;
transition: background 0.15s !important;
}
/* Stronger alternating row contrast */
tr.altColor > td, tr.altColor > th[scope="row"] { background: var(--bg) !important; }
tr.rowColor > td, tr.rowColor > th[scope="row"] { background: var(--bg2) !important; }
tr:hover > td, tr:hover > th[scope="row"] {
background: var(--purple-dim) !important;
border-bottom-color: rgba(187, 154, 247, 0.25) !important;
}
tr:hover > td a, tr:hover > th[scope="row"] a {
color: var(--purple) !important;
}
/* ── Links ── */
a, a:visited { color: var(--blue) !important; text-decoration: none !important; transition: color 0.15s !important; }
a:hover { color: var(--purple) !important; text-decoration: none !important; }
.memberNameLink a { color: var(--cyan) !important; font-weight: 500 !important; }
.memberNameLink a:hover { color: var(--purple) !important; }
.externalLink { color: var(--blue) !important; }
/* ── Description blocks ── */
div.block {
color: var(--text-mid) !important;
font-family: var(--font-ui) !important;
font-size: 13px !important;
line-height: 1.7 !important;
padding: 4px 0 !important;
}
ul.blockList > li.blockList > div.block{ padding: 12px 16px !important; margin: 0 !important; }
/* ── Method detail ── */
ul.blockList, ul.blockListLast { list-style: none !important; padding: 0 !important; margin: 0 !important; }
ul.blockList > li.blockList, ul.blockListLast > li.blockList {
border: 1px solid var(--border) !important;
border-radius: 6px !important;
margin-bottom: 12px !important;
padding: 0 !important;
overflow: hidden !important;
}
/* Method/type signatures — accent left bar */
pre.methodSignature, pre.typeSignature {
background: var(--bg2) !important;
border: none !important;
border-bottom: 1px solid var(--border) !important;
border-left: 3px solid var(--cyan) !important;
margin: 0 !important;
padding: 14px 18px !important;
font-family: var(--font-mono) !important;
font-size: 12px !important;
color: var(--text) !important;
overflow-x: auto !important;
white-space: pre-wrap !important;
}
pre {
background: var(--bg2) !important;
border: 1px solid var(--border) !important;
border-left: 3px solid var(--blue) !important;
border-radius: 6px !important;
padding: 14px 18px !important;
font-family: var(--font-mono) !important;
font-size: 12px !important;
color: var(--text) !important;
overflow-x: auto !important;
margin: 0 !important;
white-space: pre-wrap !important;
}
code {
font-family: var(--font-mono) !important;
font-size: 12px !important;
color: var(--cyan) !important;
background: var(--bg3) !important;
padding: 2px 6px !important;
border-radius: 4px !important;
}
dl { padding: 12px 16px !important; margin: 0 !important; }
dt { margin-bottom: 4px !important; }
dd { margin: 0 0 8px 16px !important; color: var(--text-mid) !important; font-size: 12px !important; }
.paramLabel, .returnLabel, .throwsLabel, .seeLabel, .simpleTagLabel {
font-family: var(--font-ui) !important;
font-size: 10px !important;
font-weight: 600 !important;
color: var(--purple) !important;
text-transform: uppercase !important;
letter-spacing: 0.06em !important;
}
.typeNameLabel { color: var(--cyan) !important; }
.deprecationBlock {
background: var(--amber-dim) !important;
border-left: 3px solid var(--amber) !important;
border-radius: 0 6px 6px 0 !important;
padding: 10px 14px !important;
color: var(--amber) !important;
font-size: 12px !important;
}
/* ── Inputs ── */
input[type="text"] {
background: var(--bg) !important;
border: 1px solid var(--border2) !important;
color: var(--text) !important;
font-family: var(--font-mono) !important;
font-size: 12px !important;
padding: 5px 12px !important;
outline: none !important;
border-radius: 6px !important;
transition: border-color 0.2s, box-shadow 0.2s !important;
}
input[type="text"]:focus {
border-color: var(--blue) !important;
box-shadow: 0 0 0 2px var(--blue-dim) !important;
}
input[type="reset"] {
background: var(--bg3) !important;
border: 1px solid var(--border2) !important;
color: var(--text-dim) !important;
font-family: var(--font-ui) !important;
font-size: 11px !important;
padding: 5px 12px !important;
cursor: pointer !important;
border-radius: 6px !important;
transition: all 0.2s !important;
}
input[type="reset"]:hover {
border-color: var(--text-dim) !important;
color: var(--text-mid) !important;
}
/* ══════════════════════════════════════════════
BOTTOM NAV
══════════════════════════════════════════════ */
.bottomNav {
background: var(--bg2) !important;
border-top: 1px solid var(--border) !important;
min-height: var(--nav-h) !important;
padding: 0 !important;
margin-top: 32px !important;
overflow: hidden !important;
position: relative !important;
}
/* Gradient accent line on bottom nav too */
.bottomNav::after {
content: '' !important;
position: absolute !important;
bottom: 0 !important; left: 0 !important; right: 0 !important;
height: 2px !important;
background: linear-gradient(90deg, var(--blue), var(--purple), var(--cyan)) !important;
}
.bottomNav ul.navList[title="Navigation"] {
list-style: none !important;
margin: 0 !important;
padding: 0 0 0 8px !important;
display: flex !important;
align-items: stretch !important;
height: var(--nav-h) !important;
}
.bottomNav ul.navList[title="Navigation"] li {
display: flex !important;
align-items: center !important;
margin: 0 !important;
padding: 0 16px !important;
height: var(--nav-h) !important;
font-family: var(--font-ui) !important;
font-size: 12px !important;
font-weight: 500 !important;
white-space: nowrap !important;
text-transform: none !important;
letter-spacing: 0.01em !important;
color: var(--text-dim) !important;
border-top: 2px solid transparent !important;
transition: color 0.2s !important;
}
.bottomNav ul.navList[title="Navigation"] li > a {
display: flex !important;
align-items: center !important;
height: var(--nav-h) !important;
margin: 0 -16px !important;
padding: 0 16px !important;
color: var(--text-dim) !important;
text-decoration: none !important;
font-family: var(--font-ui) !important;
font-size: 12px !important;
font-weight: 500 !important;
transition: color 0.2s !important;
}
.bottomNav ul.navList[title="Navigation"] li > a:hover {
color: var(--text) !important;
text-shadow: none !important;
}
.bottomNav ul.navList[title="Navigation"] li.navBarCell1Rev {
color: var(--text) !important;
border-image: linear-gradient(90deg, var(--blue), var(--purple)) 1 !important;
border-top-width: 2px !important;
border-top-style: solid !important;
}
.bottomNav .aboutLanguage {
display: none !important;
}
.legalCopy {
background: var(--bg2) !important;
color: var(--text-dim) !important;
font-size: 11px !important;
font-family: var(--font-ui) !important;
padding: 14px 18px !important;
text-align: center !important;
border-top: 1px solid var(--border) !important;
}
/* ══════════════════════════════════════════════
SEARCH RESULTS DROPDOWN (jQuery UI Autocomplete)
══════════════════════════════════════════════ */
.ui-autocomplete {
background: var(--bg2) !important;
border: 1px solid var(--border2) !important;
border-radius: 8px !important;
max-height: 400px !important;
overflow-y: auto !important;
overflow-x: hidden !important;
z-index: 9999 !important;
list-style: none !important;
padding: 4px 0 !important;
margin: 4px 0 0 !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(122, 162, 247, 0.05) !important;
}
/* Category headers */
.ui-autocomplete .ui-autocomplete-category {
background: var(--bg-elevated) !important;
color: var(--text-dim) !important;
font-family: var(--font-ui) !important;
font-size: 10px !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.06em !important;
padding: 6px 14px !important;
border-bottom: 1px solid var(--border) !important;
border-top: 1px solid var(--border) !important;
cursor: default !important;
}
/* Individual result items */
.ui-autocomplete .ui-menu-item {
background: var(--bg2) !important;
margin: 0 !important;
list-style: none !important;
}
.ui-autocomplete .ui-menu-item > div,
.ui-autocomplete .ui-menu-item-wrapper {
background: var(--bg2) !important;
color: var(--text) !important;
font-family: var(--font-mono) !important;
font-size: 12px !important;
padding: 7px 14px !important;
border: none !important;
border-left: 2px solid transparent !important;
cursor: pointer !important;
line-height: 1.5 !important;
transition: all 0.1s !important;
}
/* Hover / focus state */
.ui-autocomplete .ui-menu-item > div.ui-state-active,
.ui-autocomplete .ui-menu-item > div.ui-state-focus,
.ui-autocomplete .ui-menu-item-wrapper.ui-state-active,
.ui-autocomplete .ui-menu-item-wrapper.ui-state-focus,
.ui-autocomplete .ui-state-active,
.ui-autocomplete .ui-state-focus {
background: var(--purple-dim) !important;
color: var(--text) !important;
border: none !important;
border-left: 2px solid var(--purple) !important;
margin: 0 !important;
}
/* Highlighted matching text */
.resultHighlight {
color: var(--cyan) !important;
font-weight: 600 !important;
}
/* Search tag result extras */
.searchTagHolderResult {
color: var(--text-dim) !important;
font-size: 11px !important;
}
.searchTagDescResult {
color: var(--text-dim) !important;
font-size: 11px !important;
font-style: italic !important;
}
/* "No results found" */
.ui-autocomplete .ui-menu-item:only-child > div {
color: var(--text-dim) !important;
font-style: italic !important;
}
/* Override jQuery UI defaults */
.ui-widget {
font-family: var(--font-mono) !important;
}
.ui-widget-content {
background: var(--bg2) !important;
color: var(--text) !important;
border-color: var(--border2) !important;
}
.ui-menu .ui-menu-item {
padding: 0 !important;
}
/* Watermark style for search input placeholder */
.watermark {
color: var(--text-dim) !important;
}
/* ══════════════════════════════════════════════
MISC
══════════════════════════════════════════════ */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
::selection { background: rgba(187, 154, 247, 0.25); color: var(--text); }
hr { border: none !important; border-top: 1px solid var(--border) !important; margin: 24px 0 !important; }
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Javadoc Index</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600&family=IBM+Plex+Sans:ital,wght@0,300;0,600;1,300&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+1679
View File
File diff suppressed because it is too large. Load diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "javadoc-index",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^7.3.1"
}
}
+64
View File
@@ -0,0 +1,64 @@
import styles from './App.module.css'
import { useDocs } from './useDocs'
import TerminalHeader from './components/TerminalHeader'
import Section from './components/Section'
import { ReleaseCard, ExperimentalCard } from './components/Card'
import { LoadingState, ErrorState, EmptyState } from './components/States'
import Footer from './components/Footer'
export default function App() {
const { data, loading, error } = useDocs()
const releases = data?.releases || []
const experimental = data?.experimental || []
return (
<div className={styles.root}>
<TerminalHeader />
{/* ── Releases ── */}
<Section
label="Release"
variant="releases"
count={loading ? undefined : releases.length}
>
{loading ? (
<LoadingState />
) : error ? (
<ErrorState message={error} />
) : releases.length === 0 ? (
<EmptyState label="releases" />
) : (
<div className={styles.grid}>
{releases.map((r, i) => (
<ReleaseCard key={r.id} release={r} index={i} />
))}
</div>
)}
</Section>
{/* ── Experimental ── */}
<Section
label="Experimental"
variant="experimental"
count={loading ? undefined : experimental.length}
>
{loading ? (
<LoadingState />
) : error ? null : experimental.length === 0 ? (
<EmptyState label="experimental branches" />
) : (
<div className={styles.grid}>
{experimental.map((b, i) => (
<ExperimentalCard key={b.id} branch={b} index={i} />
))}
</div>
)}
</Section>
{!loading && !error && (
<Footer source={data?.source} />
)}
</div>
)
}
+16
View File
@@ -0,0 +1,16 @@
.root {
max-width: 1100px;
margin: 0 auto;
padding: 48px 32px 96px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 10px;
}
@media (max-width: 600px) {
.root { padding: 24px 16px 64px; }
.grid { grid-template-columns: 1fr; }
}
+97
View File
@@ -0,0 +1,97 @@
import styles from './Card.module.css'
import { REPO_NAME } from '../config'
const BASE_PATH = `/${REPO_NAME}`
function formatDate(iso) {
if (!iso) return null
try {
return new Intl.DateTimeFormat('en-GB', {
day: '2-digit', month: 'short', year: 'numeric'
}).format(new Date(iso))
} catch (e) { return null }
}
export function ReleaseCard({ release, index }) {
const isLatest = release.isLatest
const date = formatDate(release.date)
const cardClass = isLatest
? styles.card + ' ' + styles.latest
: styles.card + ' ' + styles.release
return (
<a
href={BASE_PATH + '/' + release.path + '/'}
className={cardClass}
style={{ animationDelay: (index * 50) + 'ms' }}
>
<div className={styles.header}>
<div className={styles.badges}>
{isLatest && (
<span className={styles.badge + ' ' + styles.badgeLatest}>● latest</span>
)}
<span className={styles.badge + ' ' + styles.badgeRelease}>release</span>
</div>
<span className={styles.arrow}>→</span>
</div>
<div className={styles.version}>
v{release.version}
</div>
<div className={styles.meta}>
{date && (
<div className={styles.metaRow}>
<span className={styles.metaKey}>date</span>
<span className={styles.metaVal}>{date}</span>
</div>
)}
<div className={styles.metaRow}>
<span className={styles.metaKey}>path</span>
<span className={styles.metaVal}>{release.path}/</span>
</div>
</div>
</a>
)
}
export function ExperimentalCard({ branch, index }) {
const date = formatDate(branch.date)
return (
<a
href={BASE_PATH + '/' + branch.path + '/'}
className={styles.card + ' ' + styles.experimental}
style={{ animationDelay: (index * 50) + 'ms' }}
>
<div className={styles.header}>
<span className={styles.badge + ' ' + styles.badgeExperimental}>
experimental
</span>
<span className={styles.arrow}>→</span>
</div>
<div className={styles.version + ' ' + styles.versionBranch}>
{branch.branch}
</div>
<div className={styles.meta}>
{date && (
<div className={styles.metaRow}>
<span className={styles.metaKey}>date</span>
<span className={styles.metaVal}>{date}</span>
</div>
)}
{branch.hash && (
<div className={styles.metaRow}>
<span className={styles.metaKey}>sha</span>
<span className={styles.metaVal + ' ' + styles.hash}>{branch.hash}</span>
</div>
)}
<div className={styles.metaRow}>
<span className={styles.metaKey}>path</span>
<span className={styles.metaVal}>{branch.path}/</span>
</div>
</div>
</a>
)
}
@@ -0,0 +1,143 @@
.card {
display: block;
text-decoration: none;
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 6px;
padding: 20px 22px;
transition: border-color 0.15s, background 0.15s, transform 0.15s;
position: relative;
overflow: hidden;
animation: fadeUp 0.35s ease both;
}
.card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
opacity: 0;
transition: opacity 0.15s;
}
.release::before { background: var(--green); }
.latest::before { background: linear-gradient(90deg, var(--green), var(--blue)); }
.experimental::before { background: var(--amber); }
.card:hover {
border-color: var(--border2);
background: var(--bg3);
transform: translateY(-2px);
}
.card:hover::before { opacity: 1; }
/* ── Header ── */
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.badges {
display: flex;
align-items: center;
gap: 6px;
}
.badge {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 2px 8px;
border-radius: 2px;
border: 1px solid;
}
.badgeLatest {
color: var(--green);
border-color: var(--green-dim);
background: rgba(57, 211, 83, 0.08);
animation: pulseBorder 2s ease infinite;
}
.badgeRelease {
color: var(--text-dim);
border-color: var(--border2);
background: transparent;
}
.badgeExperimental {
color: var(--amber);
border-color: var(--amber-dim);
background: rgba(240, 160, 48, 0.08);
}
.arrow {
font-size: 14px;
color: var(--border2);
transition: color 0.15s, transform 0.15s;
}
.card:hover .arrow {
color: var(--text-mid);
transform: translateX(3px);
}
/* ── Version ── */
.version {
font-size: 20px;
font-weight: 500;
color: var(--text);
margin-bottom: 4px;
letter-spacing: -0.01em;
}
.latest .version { color: var(--green); }
.versionBranch { font-size: 16px; color: var(--amber); }
/* ── Meta ── */
.meta {
display: flex;
flex-direction: column;
gap: 3px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.metaRow {
display: flex;
align-items: center;
gap: 8px;
font-size: 10px;
color: var(--text-dim);
}
.metaKey {
min-width: 36px;
color: var(--text-dim);
}
.metaVal {
color: var(--text-mid);
font-family: 'IBM Plex Mono', monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hash { color: var(--blue); }
/* ── Animations ── */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulseBorder {
0%, 100% { border-color: var(--green-dim); }
50% { border-color: var(--green); }
}
+29
View File
@@ -0,0 +1,29 @@
import styles from './Footer.module.css'
import { REPO_OWNER, REPO_NAME } from '../useDocs'
export default function Footer({ source }) {
const now = new Intl.DateTimeFormat('en-GB', {
day: '2-digit', month: 'short', year: 'numeric',
hour: '2-digit', minute: '2-digit', timeZoneName: 'short'
}).format(new Date())
return (
<footer className={styles.footer}>
<span>
<span className={styles.dot} />
Auto-generated · {source === 'manifest' ? 'manifest.json' : 'GitHub API'}
</span>
<span>{now}</span>
<span>Template by:&nbsp;
<a
href={`https://github.com/somehussar/`}
target="_blank"
rel="noopener noreferrer"
className={styles.link}
>
github.com/somehussar
</a>
</span>
</footer>
)
}
@@ -0,0 +1,36 @@
.footer {
margin-top: 64px;
padding-top: 20px;
border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
font-size: 11px;
color: var(--text-dim);
}
.dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--green);
margin-right: 6px;
animation: blink 2s ease infinite;
}
.link {
color: var(--blue);
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
+18
View File
@@ -0,0 +1,18 @@
import styles from './Section.module.css'
export default function Section({ label, count, variant, children }) {
return (
<section className={styles.section}>
<div className={styles.header}>
<span className={`${styles.tag} ${styles[variant]}`}>{label}</span>
<div className={styles.line} />
{count !== undefined && (
<span className={styles.count}>
{count} {label.toLowerCase()}{count !== 1 ? 's' : ''}
</span>
)}
</div>
{children}
</section>
)
}
@@ -0,0 +1,46 @@
.section {
margin-top: 40px;
margin-bottom: 8px;
}
.header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.tag {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.15em;
text-transform: uppercase;
padding: 3px 10px;
border-radius: 3px;
border: 1px solid;
white-space: nowrap;
}
.releases {
color: var(--green);
border-color: var(--green-dim);
background: rgba(57, 211, 83, 0.06);
}
.experimental {
color: var(--amber);
border-color: var(--amber-dim);
background: rgba(240, 160, 48, 0.06);
}
.line {
flex: 1;
height: 1px;
background: var(--border);
}
.count {
font-size: 11px;
color: var(--text-dim);
white-space: nowrap;
}
+30
View File
@@ -0,0 +1,30 @@
import styles from './States.module.css'
export function LoadingState() {
return (
<div className={styles.box}>
<div className={styles.spinner} />
<p className={styles.label}>Fetching documentation index...</p>
</div>
)
}
export function ErrorState({ message }) {
return (
<div className={styles.box}>
<div className={styles.errorIcon}>⚠</div>
<p className={styles.errorMsg}>{message}</p>
<p className={styles.errorSub}>
Check that the gh-pages branch exists and is publicly accessible.
</p>
</div>
)
}
export function EmptyState({ label }) {
return (
<div className={`${styles.box} ${styles.empty}`}>
<p className={styles.label}>No {label} found.</p>
</div>
)
}
@@ -0,0 +1,47 @@
.box {
border: 1px solid var(--border);
border-radius: 6px;
padding: 48px;
text-align: center;
background: var(--bg2);
}
.empty {
padding: 24px;
}
.spinner {
width: 22px;
height: 22px;
border: 2px solid var(--border2);
border-top-color: var(--green);
border-radius: 50%;
animation: spin 0.75s linear infinite;
margin: 0 auto 14px;
}
.label {
font-size: 12px;
color: var(--text-dim);
}
.errorIcon {
font-size: 22px;
margin-bottom: 10px;
color: var(--red);
}
.errorMsg {
font-size: 13px;
color: var(--red);
margin-bottom: 6px;
}
.errorSub {
font-size: 11px;
color: var(--text-dim);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@@ -0,0 +1,186 @@
import { useState, useEffect } from 'react'
import styles from './TerminalHeader.module.css'
import { PROJECT_NAME, REPO_NAME, LINKS, AUTHORS } from '../config'
function useGitHubProfile(username) {
const [profile, setProfile] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const cacheKey = 'gh-profile-' + username
// Check sessionStorage first — avoids hammering the API on every render
try {
const cached = sessionStorage.getItem(cacheKey)
if (cached) {
setProfile(JSON.parse(cached))
setLoading(false)
return
}
} catch (e) {}
fetch('https://api.github.com/users/' + username)
.then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status)
return r.json()
})
.then(d => {
// Don't cache error responses like rate limit messages
if (!d.message) {
try { sessionStorage.setItem(cacheKey, JSON.stringify(d)) } catch (e) {}
}
setProfile(d)
setLoading(false)
})
.catch(() => setLoading(false))
}, [username])
return { profile, loading }
}
function AuthorTile({ username, active }) {
const { profile, loading } = useGitHubProfile(username)
const tileClass = styles.authorTile + (active ? ' ' + styles.authorTileActive : '')
if (loading) {
return (
<div className={tileClass}>
<div className={styles.authorAvatarSkeleton} />
<div className={styles.authorInfo}>
<div className={styles.authorSkeleton} style={{ width: '60px' }} />
<div className={styles.authorSkeleton} style={{ width: '80px', marginTop: '4px' }} />
<div className={styles.authorBioPlaceholder} />
</div>
</div>
)
}
if (!profile || profile.message) {
return (
<a
href={'https://github.com/' + username}
target="_blank"
rel="noopener noreferrer"
className={tileClass}
>
<div className={styles.authorAvatarSkeleton} />
<div className={styles.authorInfo}>
<span className={styles.authorName}>{username}</span>
<span className={styles.authorLogin}>@{username}</span>
<div className={styles.authorBioPlaceholder} />
</div>
</a>
)
}
return (
<a
href={profile.html_url}
target="_blank"
rel="noopener noreferrer"
className={tileClass}
>
<img
src={profile.avatar_url}
alt={profile.login}
className={styles.authorAvatar}
/>
<div className={styles.authorInfo}>
<span className={styles.authorName}>{profile.name || profile.login}</span>
<span className={styles.authorLogin}>@{profile.login}</span>
{profile.bio
? <span className={styles.authorBio}>{profile.bio}</span>
: <div className={styles.authorBioPlaceholder} />
}
</div>
</a>
)
}
function AuthorsTiles() {
const [active, setActive] = useState(0)
useEffect(() => {
if (AUTHORS.length <= 1) return
const t = setInterval(() => {
setActive(i => (i + 1) % AUTHORS.length)
}, 4000)
return () => clearInterval(t)
}, [])
return (
<div className={styles.authors}>
<div className={styles.outputLine}>
<span className={styles.chevron}>›</span> Authors:
</div>
<div className={styles.authorRow}>
{AUTHORS.map((username, i) => (
<AuthorTile
key={username}
username={username}
active={i === active}
/>
))}
</div>
</div>
)
}
export default function TerminalHeader() {
return (
<div className={styles.wrap}>
<div className={styles.bar}>
<div className={styles.dot + ' ' + styles.red} />
<div className={styles.dot + ' ' + styles.amber} />
<div className={styles.dot + ' ' + styles.green} />
<span className={styles.title}>javadoc-index — bash</span>
</div>
<div className={styles.body}>
<h1 className={styles.heading}>
{PROJECT_NAME}
</h1>
<div className={styles.promptLine}>
<span className={styles.prompt}>$</span>
<span className={styles.cmd}>browse-docs</span>
<span className={styles.arg}>{REPO_NAME}</span>
<span className={styles.flag}>--all-versions</span>
</div>
<div className={styles.outputLine}>
<span className={styles.chevron}>›</span> Fetching available documentation builds...
</div>
<div className={styles.outputLine}>
<span className={styles.chevron}>›</span> Found project links:
</div>
<div className={styles.links}>
{LINKS.map(function(l) {
return (
<a
key={l.tag}
href={l.href}
target="_blank"
rel="noopener noreferrer"
className={styles.link}
>
<span className={styles.linkTag} style={{ color: l.color, borderColor: l.color }}>
{l.tag}
</span>
<span className={styles.linkLabel}>{l.label}</span>
<span className={styles.linkArrow} style={{ color: l.color }}>↗</span>
</a>
)
})}
</div>
<AuthorsTiles />
<div className={styles.promptLine} style={{ marginTop: '16px' }}>
<span className={styles.prompt}>$</span>
<span className={styles.cursor} />
</div>
</div>
</div>
)
}
@@ -0,0 +1,303 @@
.wrap {
margin-bottom: 48px;
}
.bar {
display: flex;
align-items: center;
gap: 6px;
background: var(--bg3);
border: 1px solid var(--border2);
border-bottom: none;
border-radius: 8px 8px 0 0;
padding: 10px 16px;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.red { background: #ff5f57; }
.amber { background: #febc2e; }
.green { background: #28c840; }
.title {
margin-left: 8px;
font-size: 11px;
color: var(--text-dim);
letter-spacing: 0.05em;
}
.body {
background: var(--bg2);
border: 1px solid var(--border2);
border-radius: 0 0 8px 8px;
padding: 16px 36px 36px;
}
/* ── Heading ── */
.heading {
font-family: 'IBM Plex Sans', sans-serif;
font-size: clamp(28px, 5vw, 52px);
font-weight: 300;
letter-spacing: -0.02em;
color: var(--text);
line-height: 1.05;
margin-bottom: 20px;
}
.heading strong {
font-weight: 600;
color: var(--green);
}
/* ── Prompt ── */
.promptLine {
display: flex;
align-items: baseline;
gap: 10px;
margin-bottom: 6px;
font-size: 13px;
flex-wrap: wrap;
}
.prompt { color: var(--green); font-weight: 600; }
.cmd { color: var(--text); }
.arg { color: var(--blue); }
.flag { color: var(--amber); }
/* ── Output lines ── */
.outputLine {
font-size: 12px;
color: var(--text-dim);
line-height: 1.8;
padding-left: 20px;
}
.chevron {
color: var(--text-mid);
margin-right: 4px;
}
/* ── Links ── */
.links {
display: flex;
flex-direction: column;
gap: 4px;
padding-left: 36px;
margin-top: 4px;
margin-bottom: 12px;
}
.link {
display: inline-flex;
align-items: center;
gap: 10px;
text-decoration: none;
padding: 3px 0;
width: fit-content;
transition: opacity 0.15s;
}
.link:hover { opacity: 0.8; }
.link:hover .linkLabel {
text-decoration: underline;
text-underline-offset: 3px;
}
.linkTag {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.1em;
border: 1px solid;
border-radius: 2px;
padding: 1px 5px;
min-width: 28px;
text-align: center;
opacity: 0.9;
font-family: 'IBM Plex Mono', monospace;
}
.linkLabel {
font-size: 12px;
color: var(--text-mid);
font-family: 'IBM Plex Mono', monospace;
}
.linkArrow {
font-size: 11px;
opacity: 0.6;
transition: opacity 0.15s, transform 0.15s;
}
.link:hover .linkArrow {
opacity: 1;
transform: translate(2px, -2px);
}
/* ── Authors ── */
.authors {
margin-bottom: 8px;
}
.authorRow {
display: flex;
flex-direction: row;
gap: 8px;
padding-left: 36px;
margin-top: 6px;
flex-wrap: wrap;
}
/* ── Author tile ── */
.authorTile {
display: flex;
align-items: center;
gap: 10px;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
text-decoration: none;
cursor: pointer;
/* width is dynamic — grows with content */
width: max-content;
min-width: 140px;
position: relative;
overflow: hidden;
transition: border-color 0.2s, background 0.2s, transform 0.2s;
}
.authorTile::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: var(--green);
opacity: 0;
transition: opacity 0.2s;
}
.authorTileActive::before,
.authorTile:hover::before {
opacity: 1;
}
.authorTileActive {
border-color: var(--green-dim);
background: rgba(57, 211, 83, 0.04);
}
.authorTile:hover {
border-color: var(--border2);
background: #161b22;
transform: translateY(-1px);
}
/* ── Author content ── */
.authorAvatar {
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid var(--border2);
flex-shrink: 0;
}
.authorAvatarSkeleton {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--border2);
flex-shrink: 0;
animation: shimmer 1.5s ease infinite;
}
.authorInfo {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.authorName {
font-size: 12px;
font-weight: 500;
color: var(--text);
font-family: 'IBM Plex Sans', sans-serif;
white-space: nowrap;
}
.authorLogin {
font-size: 10px;
color: var(--blue);
font-family: 'IBM Plex Mono', monospace;
white-space: nowrap;
}
/* Bio — always reserves vertical height, width animates open */
.authorBio {
display: block;
font-size: 10px;
color: var(--text-dim);
font-family: 'IBM Plex Sans', sans-serif;
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* always reserve vertical space */
height: 14px;
/* collapsed horizontally */
width: 0;
opacity: 0;
transition: opacity 0.25s ease, width 0.3s ease;
}
.authorTileActive .authorBio,
.authorTile:hover .authorBio {
width: 200px;
opacity: 1;
}
/* Placeholder for loading/no-profile/no-bio states */
.authorBioPlaceholder {
height: 14px;
margin-top: 2px;
width: 0; /* never takes horizontal space, never expands */
}
.authorSkeleton {
height: 9px;
border-radius: 4px;
background: var(--border2);
animation: shimmer 1.5s ease infinite;
}
/* ── Cursor ── */
.cursor {
display: inline-block;
width: 8px;
height: 13px;
background: var(--green);
margin-left: 2px;
vertical-align: text-bottom;
animation: blink 1s step-end infinite;
}
/* ── Animations ── */
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@keyframes shimmer {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.8; }
}
@media (max-width: 600px) {
.body { padding: 20px; }
.links { padding-left: 20px; }
.authorRow { padding-left: 20px; }
}
+38
View File
@@ -0,0 +1,38 @@
// ─── Project Configuration ───────────────────────────────────
// This is the ONLY file that should differ between projects.
// Everything else in the pages app is generic.
// ─────────────────────────────────────────────────────────────
export const PROJECT_NAME = 'CustomNPC+'
export const REPO_OWNER = 'KAMKEEL'
export const REPO_NAME = 'CustomNPC-Plus'
export const LINKS = [
{
tag: 'GH',
label: 'github.com/KAMKEEL/CustomNPC-Plus',
href: 'https://github.com/KAMKEEL/CustomNPC-Plus',
color: '#e6edf3',
},
{
tag: 'DC',
label: 'discord.gg/94qmjEDxkp',
href: 'https://discord.gg/94qmjEDxkp',
color: '#5865f2',
},
{
tag: 'CF',
label: 'curseforge.com/minecraft/mc-mods/customnpc-plus',
href: 'https://www.curseforge.com/minecraft/mc-mods/customnpc-plus',
color: '#f16436',
},
{
tag: 'MR',
label: 'modrinth.com/mod/customnpc-plus',
href: 'https://modrinth.com/mod/customnpc-plus/',
color: '#1bd96a',
},
]
// GitHub usernames — profiles are fetched at runtime
export const AUTHORS = ['kamkeel', 'c0508383', 'bigguy345', 'somehussar']
+50
View File
@@ -0,0 +1,50 @@
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #080b0f;
--bg2: #0d1117;
--bg3: #131920;
--border: #1e2936;
--border2: #253040;
--green: #39d353;
--green-dim: #1a6b2a;
--amber: #f0a030;
--amber-dim: #7a4e10;
--blue: #58a6ff;
--blue-dim: #1a3a6b;
--text: #cdd9e5;
--text-dim: #636e7b;
--text-mid: #8b949e;
--red: #f85149;
}
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: 'IBM Plex Mono', monospace;
overflow-x: hidden;
}
/* Scanlines */
body::after {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0,0,0,0.025) 2px,
rgba(0,0,0,0.025) 4px
);
pointer-events: none;
z-index: 9999;
}
a { color: inherit; text-decoration: none; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg2); }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
+13
View File
@@ -0,0 +1,13 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { PROJECT_NAME } from './config'
import './index.css'
document.title = `${PROJECT_NAME} — Javadoc Index`
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
+94
View File
@@ -0,0 +1,94 @@
import { useState, useEffect } from 'react'
import { REPO_OWNER, REPO_NAME } from './config'
const BASE_PATH = `/${REPO_NAME}`
const API_BASE = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}`
// Cache bust on every page load — appends current timestamp to manifest URL
// so the browser never serves a stale manifest.json
async function fetchFromManifest() {
const bust = `?t=${Date.now()}`
const res = await fetch(`${BASE_PATH}/manifest.json${bust}`, {
cache: 'no-store', // tells browser not to cache this request at all
})
if (!res.ok) throw new Error('No manifest')
return res.json()
}
async function fetchFromGitHubAPI() {
const refRes = await fetch(`${API_BASE}/git/ref/heads/gh-pages`)
if (!refRes.ok) throw new Error('gh-pages branch not found')
const refData = await refRes.json()
const sha = refData.object.sha
const treeRes = await fetch(`${API_BASE}/git/trees/${sha}?recursive=1`)
if (!treeRes.ok) throw new Error('Could not fetch tree')
const treeData = await treeRes.json()
const commitsRes = await fetch(`${API_BASE}/commits?sha=gh-pages&per_page=100`)
const commits = commitsRes.ok ? await commitsRes.json() : []
const commitMap = {}
commits.forEach(c => {
const date = c.commit?.committer?.date || c.commit?.author?.date || ''
const hash = c.sha?.slice(0, 7) || ''
const msg = c.commit?.message || ''
const vMatch = msg.match(/releases\/([\d.]+)/)
if (vMatch) commitMap[`releases/${vMatch[1]}`] = { date, hash }
const eMatch = msg.match(/experimental\/([\w-]+)/)
if (eMatch) commitMap[`experimental/${eMatch[1]}`] = { date, hash }
})
const releaseDirs = new Set()
const experimentalDirs = new Set()
treeData.tree.forEach(item => {
if (item.type !== 'tree') return
const parts = item.path.split('/')
if (parts[0] === 'releases' && parts.length === 2) releaseDirs.add(parts[1])
if (parts[0] === 'experimental' && parts.length === 2) experimentalDirs.add(parts[1])
})
// Sort releases newest first, mark first as latest
const sortedReleases = [...releaseDirs]
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
const releases = sortedReleases.map((v, i) => {
const c = commitMap[`releases/${v}`] || {}
return {
id: v,
version: v,
path: `releases/${v}`,
date: c.date,
hash: c.hash,
isLatest: i === 0, // newest version is always latest
}
})
const experimental = [...experimentalDirs].sort().map(b => {
const c = commitMap[`experimental/${b}`] || {}
return { id: b, branch: b, path: `experimental/${b}`, date: c.date, hash: c.hash }
})
return { releases, experimental, source: 'api' }
}
export function useDocs() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetchFromManifest()
.then(d => { setData(d); setLoading(false) })
.catch(() =>
fetchFromGitHubAPI()
.then(d => { setData(d); setLoading(false) })
.catch(e => { setError(e.message); setLoading(false) })
)
}, [])
return { data, loading, error }
}
export { BASE_PATH, REPO_OWNER, REPO_NAME }
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { REPO_NAME } from './src/config.js'
export default defineConfig({
plugins: [react()],
base: `/${REPO_NAME}/`,
build: {
outDir: '../../.github/pages-dist',
emptyOutDir: true,
}
})
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
# -----------------------------------------------
# doc-config.sh — shared config for javadoc deploy
#
# Sourced by:
# - resolve-doc-path.sh
# - javadoc.yml (via sanity-check job)
#
# ADD WHITELISTED BRANCHES HERE AND NOWHERE ELSE.
# main is always handled automatically.
# -----------------------------------------------
MAIN_BRANCH_NAME="master"
EXPERIMENTAL_WHITELIST=(
"dev"
# "new-branch"
)
+107
View File
@@ -0,0 +1,107 @@
#!/bin/bash
set -e
RELEASES=()
EXPERIMENTAL=()
# Collect releases
if [ -d "./releases" ]; then
for dir in ./releases/*/; do
[ -d "$dir" ] || continue
name=$(basename "$dir")
RELEASES+=("$name")
done
fi
# Collect experimental branches
if [ -d "./experimental" ]; then
for dir in ./experimental/*/; do
[ -d "$dir" ] || continue
name=$(basename "$dir")
EXPERIMENTAL+=("$name")
done
fi
# Sort releases in reverse semver order (newest first)
if [ ${#RELEASES[@]} -gt 0 ]; then
IFS=$'\n' RELEASES=($(sort -rV <<< "${RELEASES[*]}")); unset IFS
fi
# ── Store source commit metadata for this deploy ───────────────
# DEPLOY_PATH, SOURCE_HASH, SOURCE_DATE are passed in as env vars from the workflow
if [ -n "$DEPLOY_PATH" ] && [ -n "$SOURCE_HASH" ]; then
mkdir -p ".doc-meta"
META_KEY="${DEPLOY_PATH//\//_}"
cat > ".doc-meta/${META_KEY}.json" << JSON
{"hash":"${SOURCE_HASH}","date":"${SOURCE_DATE}"}
JSON
echo "Stored source metadata for $DEPLOY_PATH → $SOURCE_HASH @ $SOURCE_DATE"
fi
# ── Helper: get source commit hash + date for a path ──────────
get_commit_info() {
local path="$1"
# If this is the path we just deployed, use the injected source commit
if [ "$path" = "$DEPLOY_PATH" ] && [ -n "$SOURCE_HASH" ]; then
echo "${SOURCE_HASH} ${SOURCE_DATE}"
return
fi
# Try reading from stored metadata file (set by previous deploys)
local meta=".doc-meta/${path//\//_}.json"
if [ -f "$meta" ]; then
local hash date
hash=$(python3 -c "import json,sys; d=json.load(open('$meta')); print(d.get('hash',''))" 2>/dev/null || grep -o '"hash":"[^"]*"' "$meta" | cut -d'"' -f4)
date=$(python3 -c "import json,sys; d=json.load(open('$meta')); print(d.get('date',''))" 2>/dev/null || grep -o '"date":"[^"]*"' "$meta" | cut -d'"' -f4)
echo "${hash:-} ${date:-}"
return
fi
# Last resort: gh-pages git log (hash will be wrong but better than empty)
local hash date
hash=$(git log --oneline -1 -- "$path" 2>/dev/null | awk '{print $1}')
date=$(git log --format="%cI" -1 -- "$path" 2>/dev/null)
echo "${hash:-} ${date:-}"
}
# ── Build releases JSON ────────────────────────────────────────
RELEASES_JSON="["
for i in "${!RELEASES[@]}"; do
version="${RELEASES[$i]}"
read -r HASH DATE <<< "$(get_commit_info "releases/$version")"
if [ "$i" -eq 0 ]; then
IS_LATEST="true"
else
IS_LATEST="false"
fi
RELEASES_JSON+="{\"id\":\"${version}\",\"version\":\"${version}\",\"path\":\"releases/${version}\",\"date\":\"${DATE}\",\"isLatest\":${IS_LATEST}},"
done
RELEASES_JSON="${RELEASES_JSON%,}]"
# ── Build experimental JSON ────────────────────────────────────
EXPERIMENTAL_JSON="["
for branch in "${EXPERIMENTAL[@]}"; do
read -r HASH DATE <<< "$(get_commit_info "experimental/$branch")"
EXPERIMENTAL_JSON+="{\"id\":\"${branch}\",\"branch\":\"${branch}\",\"path\":\"experimental/${branch}\",\"hash\":\"${HASH}\",\"date\":\"${DATE}\"},"
done
EXPERIMENTAL_JSON="${EXPERIMENTAL_JSON%,}]"
# ── Write manifest.json ────────────────────────────────────────
cat > manifest.json << JSON
{
"releases": ${RELEASES_JSON},
"experimental": ${EXPERIMENTAL_JSON},
"generated": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')",
"source": "manifest"
}
JSON
echo "manifest.json generated:"
cat manifest.json
echo ""
echo "Done."
+52
View File
@@ -0,0 +1,52 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/doc-config.sh"
BRANCH="${GITHUB_REF#refs/heads/}"
SAFE_BRANCH="${BRANCH//\//-}"
if [ "$BRANCH" = "$MAIN_BRANCH_NAME" ]; then
# Version resolution order:
# -> check if gradle.properties has a `modVersion` tag. If it does, just grab it.
# -> check if it has the `version` tag. Grab it if it does
# -> check build.gradle for the version tag.
if [ -f "gradle.properties" ] && grep -q "^modVersion=" gradle.properties; then
VERSION=$(grep "^modVersion=" gradle.properties | cut -d'=' -f2 | tr -d '[:space:]')
elif [ -f "gradle.properties" ] && grep -q "^version=" gradle.properties; then
VERSION=$(grep "^version=" gradle.properties | cut -d'=' -f2 | tr -d '[:space:]')
elif [ -f "build.gradle" ]; then
VERSION=$(grep "^version" build.gradle | head -1 | sed "s/version\s*=\s*['\"]//;s/['\"].*//;s/\s//g")
elif [ -f "build.gradle.kts" ]; then
VERSION=$(grep "^version" build.gradle.kts | head -1 | sed "s/version\s*=\s*\"//;s/\".*//;s/\s//g")
else
echo "Could not find version. Defaulting to 'unknown'."
VERSION="unknown"
fi
echo "Resolved version: $VERSION"
# Only deploy to releases/<version> — isLatest is handled by generate-index.sh
echo "path=releases/$VERSION" >> $GITHUB_OUTPUT
echo "extra_path=" >> $GITHUB_OUTPUT
else
MATCHED=false
for ALLOWED in "${EXPERIMENTAL_WHITELIST[@]}"; do
if [ "$BRANCH" = "$ALLOWED" ]; then
MATCHED=true
break
fi
done
if [ "$MATCHED" = true ]; then
echo "path=experimental/$SAFE_BRANCH" >> $GITHUB_OUTPUT
echo "extra_path=" >> $GITHUB_OUTPUT
echo "Branch '$BRANCH' is whitelisted, deploying to experimental/$SAFE_BRANCH"
else
echo "path=" >> $GITHUB_OUTPUT
echo "extra_path=" >> $GITHUB_OUTPUT
echo "Branch '$BRANCH' is not whitelisted. Skipping deploy."
fi
fi
+2 -2
View File
@@ -24,8 +24,8 @@ jobs:
with:
fetch-depth: 0
- name: Initialize CNPC+ API submodule
run: git submodule update --init --recursive src/api
- name: Initialize submodules
run: git submodule update --init --recursive
- name: Checkout workflows repo
uses: actions/checkout@v4
+224
View File
@@ -0,0 +1,224 @@
name: Generate and Deploy Javadoc
on:
push:
branches:
- master
- '**'
delete:
workflow_dispatch:
inputs:
force_deploy:
description: 'Force redeploy Javadocs without a new commit'
required: false
default: 'true'
jobs:
sanity-check:
runs-on: ubuntu-latest
outputs:
should_deploy: ${{ steps.deploy_check.outputs.should_deploy }}
steps:
- uses: actions/checkout@v4
- name: Check for deploy tag
id: deploy_check
run: |
source .github/scripts/doc-config.sh
BRANCH="${GITHUB_REF#refs/heads/}"
# Always deploy on manual trigger
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "should_deploy=true" >> $GITHUB_OUTPUT
echo "Reason: workflow_dispatch"
exit 0
fi
# Main branch: only deploy if commit message contains [deploy-docs]
if [ "$BRANCH" = "$MAIN_BRANCH_NAME" ]; then
if echo "${{ github.event.head_commit.message }}" | grep -q "\[deploy-docs\]"; then
echo "should_deploy=true" >> $GITHUB_OUTPUT
echo "Reason: master branch with [deploy-docs] tag"
else
echo "should_deploy=false" >> $GITHUB_OUTPUT
echo "Reason: master branch without [deploy-docs] tag, skipping"
fi
exit 0
fi
# Experimental branches: only deploy if whitelisted
for ALLOWED in "${EXPERIMENTAL_WHITELIST[@]}"; do
if [ "$BRANCH" = "$ALLOWED" ]; then
echo "should_deploy=true" >> $GITHUB_OUTPUT
echo "Reason: branch '$BRANCH' is whitelisted"
exit 0
fi
done
echo "should_deploy=false" >> $GITHUB_OUTPUT
echo "Reason: branch '$BRANCH' is not whitelisted, skipping"
build-and-deploy-javadoc:
needs: sanity-check
if: needs.sanity-check.outputs.should_deploy == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout source branch
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Initialize submodules
run: git submodule update --init --recursive
- name: Setup JDK 8 (compile)
uses: actions/setup-java@v4
with:
java-version: '8'
distribution: 'temurin'
- name: Set up JDK 8 (Zulu)
uses: actions/setup-java@v4
with:
java-version: '8'
distribution: 'zulu'
- name: Setup JDK 11 (javadoc)
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'temurin'
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Extract convention plugin version
id: convention
run: |
VERSION=$(grep "id 'com.gtnewhorizons.gtnhsettingsconvention'" settings.gradle settings.gradle.kts 2>/dev/null | grep -o "version '[0-9]\+\.[0-9]\+\.[0-9]\+'" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Detected convention version: $VERSION"
- name: Get source commit info
id: source_commit
run: |
echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "date=$(git log -1 --format='%cI')" >> $GITHUB_OUTPUT
- name: Cache Gradle wrapper
uses: actions/cache@v4
with:
path: ~/.gradle/wrapper
key: gradle-wrapper-${{ hashFiles('**/gradle-wrapper.properties') }}
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/caches/modules-*
~/.gradle/caches/jars-*
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/gradle.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Cache RFG
uses: actions/cache@v4
with:
path: |
build/rfg
build/tmp/.cache/expanded
build/tmp/deobfuscateMergedJarToSrg
key: ${{ runner.os }}-rfg-convention${{ steps.convention.outputs.version }}
restore-keys: |
${{ runner.os }}-rfg-
- name: Cache Node modules
uses: actions/cache@v4
with:
path: .github/pages/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('.github/pages/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Generate Javadoc
run: ./gradlew javadoc
- name: Get deploy path
id: deploy_path
run: bash .github/scripts/resolve-doc-path.sh
- name: Deploy to gh-pages
if: steps.deploy_path.outputs.path != ''
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build/docs/javadoc
destination_dir: ${{ steps.deploy_path.outputs.path }}
keep_files: true
# Give GitHub a moment to settle after the peaceiris push
- name: Wait for gh-pages to settle
run: sleep 10
- name: Checkout gh-pages
uses: actions/checkout@v4
with:
ref: gh-pages
path: gh-pages-index
- name: Generate manifest
run: |
cd gh-pages-index
SOURCE_HASH="${{ steps.source_commit.outputs.hash }}" \
SOURCE_DATE="${{ steps.source_commit.outputs.date }}" \
DEPLOY_PATH="${{ steps.deploy_path.outputs.path }}" \
bash ../.github/scripts/generate-index.sh
- name: Install page dependencies
run: npm ci --prefix .github/pages
- name: Build index page
run: |
mkdir -p .github/pages/public
cp gh-pages-index/manifest.json .github/pages/public/manifest.json
npm run build --prefix .github/pages
- name: Deploy index to gh-pages
run: |
cp -r .github/pages-dist/* gh-pages-index/
cd gh-pages-index
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git add -A
git commit -m "Regenerate index and manifest [skip ci]" || echo "Unchanged"
git push
cleanup-on-delete:
if: github.event_name == 'delete' && github.event.ref_type == 'branch'
runs-on: ubuntu-latest
steps:
- name: Checkout gh-pages
uses: actions/checkout@v4
with:
ref: 'gh-pages'
- name: Remove deleted branch docs
run: |
BRANCH="${{ github.event.ref }}"
SAFE="experimental/${BRANCH//\//-}"
if [ -d "./$SAFE" ]; then
rm -rf "./$SAFE"
META_KEY="${SAFE//\//_}"
rm -f ".doc-meta/${META_KEY}.json"
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git add -A
git commit -m "Remove docs for deleted branch: $BRANCH"
git push
else
echo "No docs found for $SAFE, skipping."
fi
+2
View File
@@ -45,4 +45,6 @@ src/main/resources/assets/customnpcs/api
df/
tmp/
src/test
node_modules
.github/pages-dist
+24 -3
View File
@@ -6,7 +6,7 @@ plugins {
id "dts.typescript-generator"
}
version = "1.11-prerelease3"
version = "1.11"
group= "kamkeel.customnpc-plus"
archivesBaseName = "CustomNPC-Plus"
@@ -81,11 +81,32 @@ processResources.dependsOn generateTypeScriptDefinitions
sourcesJar.dependsOn generateTypeScriptDefinitions
// Register @hookName as a valid Javadoc tag
tasks.withType(Javadoc) {
// Javadoc configuration
javadoc {
options.encoding = 'UTF-8'
options.charSet = 'UTF-8'
options.docEncoding = 'UTF-8'
options.tags = [
'hookName:a:Hook Name:'
]
options.stylesheetFile = file('.github/javadoc/javadoc-theme.css')
options.header = file('.github/javadoc/javadoc-header.html').text.replaceAll('\\s+', ' ').trim()
options.addBooleanOption('-allow-script-in-comments', true)
options.footer = file('.github/javadoc/javadoc-footer.html').text.replaceAll('\\s+', ' ').trim()
// Only document the public API, not net.minecraft stubs
include 'noppes/npcs/api/**'
options.links(
'https://docs.oracle.com/en/java/javase/11/docs/api/'
)
javadocTool = javaToolchains.javadocToolFor {
languageVersion = JavaLanguageVersion.of(11)
}
}
// Modify the existing 'build' task to depend on 'updateAPI'
+1 -1
Submodule src/api updated: efce1a99b7...f91e17ab3e.
@@ -63,9 +63,9 @@ public class RenderEnergyBeam extends RenderEnergy {
double renderOriginY = y - headOffsetY;
double renderOriginZ = z - headOffsetZ;
// Get beam properties
float beamWidth = beam.getBeamWidth();
float headSize = beam.getHeadSize();
// Get beam properties (interpolated for smooth size transitions)
float beamWidth = beam.getInterpolatedBeamWidth(partialTicks);
float headSize = beam.getInterpolatedHeadSize(partialTicks);
int innerColor = beam.getInnerColor();
int outerColor = beam.getOuterColor();
float outerAlpha = beam.getOuterColorAlpha() * proximityAlpha;
@@ -105,7 +105,7 @@ public class RenderEnergyBeam extends RenderEnergy {
* Grows from 0 to headSize based on charge progress.
*/
private void renderChargingOrb(EntityAbilityBeam beam, double x, double y, double z, float partialTicks, float proximityAlpha) {
float headSize = beam.getHeadSize();
float headSize = beam.getInterpolatedHeadSize(partialTicks);
float chargeProgress = beam.getInterpolatedChargeProgress(partialTicks);
float size = headSize * chargeProgress;
@@ -31,8 +31,8 @@ public class RenderEnergyDisc extends RenderEnergy {
// Get interpolated values
float size = disc.getInterpolatedSize(partialTicks);
float radius = disc.getDiscRadius() * size;
float thickness = disc.getDiscThickness() * size;
float radius = disc.getInterpolatedDiscRadius(partialTicks) * size;
float thickness = disc.getInterpolatedDiscThickness(partialTicks) * size;
// Subtle pulsing effect
float pulseTime = entity.ticksExisted + partialTicks;
@@ -43,7 +43,7 @@ public class RenderEnergyLaser extends RenderEnergy {
double endX = laser.getInterpolatedEndX(partialTicks);
double endY = laser.getInterpolatedEndY(partialTicks);
double endZ = laser.getInterpolatedEndZ(partialTicks);
float width = laser.getLaserWidth();
float width = laser.getInterpolatedLaserWidth(partialTicks);
float alpha = 1.0f;
// Calculate render offset (entity position is used as reference)
@@ -156,7 +156,7 @@ public class RenderEnergyLaser extends RenderEnergy {
}
private void renderChargingOrb(EntityAbilityLaser laser, double x, double y, double z, float partialTicks, float proximityAlpha) {
float headSize = laser.getLaserWidth() * 0.5f;
float headSize = laser.getInterpolatedLaserWidth(partialTicks) * 0.5f;
float chargeProgress = laser.getInterpolatedChargeProgress(partialTicks);
float size = headSize * chargeProgress;
@@ -41,6 +41,8 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
// Beam shape properties
private float beamWidth = 0.3f;
private float headSize = 0.5f;
private float prevBeamWidth = 0.3f;
private float prevHeadSize = 0.5f;
// Trail - list of points showing beam path (RELATIVE to origin!)
private List<Vec3> trailPoints = new ArrayList<>();
@@ -240,10 +242,12 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
return;
}
// Store previous head offset for interpolation
// Store previous values for interpolation
prevHeadOffsetX = headOffsetX;
prevHeadOffsetY = headOffsetY;
prevHeadOffsetZ = headOffsetZ;
prevBeamWidth = beamWidth;
prevHeadSize = headSize;
// Age trail points for fading effect
if (isFadingMode()) {
@@ -580,7 +584,12 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
return beamWidth;
}
public float getInterpolatedBeamWidth(float partialTicks) {
return prevBeamWidth + (beamWidth - prevBeamWidth) * partialTicks;
}
public void setBeamWidth(float beamWidth) {
this.prevBeamWidth = this.beamWidth;
this.beamWidth = beamWidth;
}
@@ -588,7 +597,12 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
return headSize;
}
public float getInterpolatedHeadSize(float partialTicks) {
return prevHeadSize + (headSize - prevHeadSize) * partialTicks;
}
public void setHeadSize(float headSize) {
this.prevHeadSize = this.headSize;
this.headSize = headSize;
}
@@ -747,6 +761,8 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
@Override
protected void applyProjectileClientSyncData(NBTTagCompound nbt) {
prevBeamWidth = beamWidth;
prevHeadSize = headSize;
beamWidth = nbt.getFloat("BeamWidth");
headSize = nbt.getFloat("HeadSize");
setAttachedToOwner(nbt.getBoolean("AttachedToOwner"));
@@ -758,6 +774,8 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
protected void readProjectileNBT(NBTTagCompound nbt) {
this.beamWidth = sanitize(nbt.hasKey("BeamWidth") ? nbt.getFloat("BeamWidth") : 0.3f, 0.3f, MAX_ENTITY_SIZE);
this.headSize = sanitize(nbt.hasKey("HeadSize") ? nbt.getFloat("HeadSize") : 0.5f, 0.5f, MAX_ENTITY_SIZE);
this.prevBeamWidth = this.beamWidth;
this.prevHeadSize = this.headSize;
this.headOffsetX = nbt.hasKey("HeadOffsetX") ? nbt.getDouble("HeadOffsetX") : 0;
this.headOffsetY = nbt.hasKey("HeadOffsetY") ? nbt.getDouble("HeadOffsetY") : 0;
this.headOffsetZ = nbt.hasKey("HeadOffsetZ") ? nbt.getDouble("HeadOffsetZ") : 0;
@@ -33,6 +33,8 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
// Disc shape properties
private float discRadius = 1.0f; // Width of disc
private float discThickness = 0.2f; // Height of disc
private float prevDiscRadius = 1.0f;
private float prevDiscThickness = 0.2f;
// Boomerang owner-gone tracking
private int returnOwnerNullTicks = 0;
@@ -134,6 +136,10 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
@Override
protected void updateProjectile() {
// Store previous values for interpolation
prevDiscRadius = discRadius;
prevDiscThickness = discThickness;
// Handle charging state (windup phase)
if (isCharging()) {
updateCharging();
@@ -347,7 +353,12 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
return discRadius;
}
public float getInterpolatedDiscRadius(float partialTicks) {
return prevDiscRadius + (discRadius - prevDiscRadius) * partialTicks;
}
public void setDiscRadius(float radius) {
this.prevDiscRadius = this.discRadius;
this.discRadius = radius;
}
@@ -355,7 +366,12 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
return discThickness;
}
public float getInterpolatedDiscThickness(float partialTicks) {
return prevDiscThickness + (discThickness - prevDiscThickness) * partialTicks;
}
public void setDiscThickness(float thickness) {
this.prevDiscThickness = this.discThickness;
this.discThickness = thickness;
}
@@ -443,6 +459,8 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
@Override
protected void applyProjectileClientSyncData(NBTTagCompound nbt) {
prevDiscRadius = discRadius;
prevDiscThickness = discThickness;
discRadius = nbt.getFloat("DiscRadius");
discThickness = nbt.getFloat("DiscThickness");
vertical = nbt.getBoolean("Vertical");
@@ -456,6 +474,8 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
this.boomerangDelay = nbt.hasKey("BoomerangDelay") ? nbt.getInteger("BoomerangDelay") : 40;
this.discRadius = sanitize(nbt.hasKey("DiscRadius") ? nbt.getFloat("DiscRadius") : 1.0f, 1.0f, MAX_ENTITY_SIZE);
this.discThickness = sanitize(nbt.hasKey("DiscThickness") ? nbt.getFloat("DiscThickness") : 0.2f, 0.2f, MAX_ENTITY_SIZE);
this.prevDiscRadius = this.discRadius;
this.prevDiscThickness = this.discThickness;
this.vertical = nbt.hasKey("Vertical") ? nbt.getBoolean("Vertical") : false;
this.returning = nbt.hasKey("Returning") && nbt.getBoolean("Returning");
// Charging state (common fields handled by base)
@@ -33,6 +33,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
// Laser-specific properties
private float laserWidth = 0.2f;
private float prevLaserWidth = 0.2f;
private float expansionSpeed = 2.0f; // Blocks per tick
private float maxLength = 32.0f; // Maximum beam reach in blocks
@@ -144,6 +145,9 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
@Override
protected void updateProjectile() {
// Store previous values for interpolation
prevLaserWidth = laserWidth;
if (isCharging()) {
updateCharging();
return;
@@ -703,7 +707,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
return laserWidth;
}
public float getInterpolatedLaserWidth(float partialTicks) {
return prevLaserWidth + (laserWidth - prevLaserWidth) * partialTicks;
}
public void setLaserWidth(float width) {
this.prevLaserWidth = this.laserWidth;
this.laserWidth = width;
}
@@ -879,6 +888,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
@Override
protected void applyProjectileClientSyncData(NBTTagCompound nbt) {
prevLaserWidth = laserWidth;
laserWidth = nbt.getFloat("LaserWidth");
expansionSpeed = nbt.getFloat("ExpansionSpeed");
maxLength = nbt.getFloat("MaxLength");
@@ -892,6 +902,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
@Override
protected void readProjectileNBT(NBTTagCompound nbt) {
this.laserWidth = sanitize(nbt.hasKey("LaserWidth") ? nbt.getFloat("LaserWidth") : 0.2f, 0.2f, MAX_ENTITY_SIZE);
this.prevLaserWidth = this.laserWidth;
this.expansionSpeed = sanitize(nbt.hasKey("ExpansionSpeed") ? nbt.getFloat("ExpansionSpeed") : 2.0f, 0.1f, MAX_ENTITY_SIZE);
this.maxLength = sanitize(nbt.hasKey("MaxLength") ? nbt.getFloat("MaxLength") : 32.0f, 1.0f, MAX_ENTITY_SIZE);
this.dirX = nbt.getDouble("DirX");
@@ -348,7 +348,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
// Fire tick event (server-side, non-preview only)
if (!previewMode && !worldObj.isRemote) {
EventHooks.onEnergyProjectileTick(this);
EventHooks.onEnergyProjectileTick(this, ticksExisted);
}
debugLogTick();
@@ -738,8 +738,8 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
motionY = nbt.getDouble("MotionY");
motionZ = nbt.getDouble("MotionZ");
syncPositionStateToCurrent(true);
// Size & visual
setProjectileSize(nbt.getFloat("Size"));
// Size & visual (set target only - let the lerp in onUpdate smooth the transition)
this.size = nbt.getFloat("Size");
displayData.setRotationSpeed(nbt.getFloat("RotationSpeed"));
displayData.setInnerAlpha(nbt.getFloat("InnerAlpha"));
displayData.setOuterColorEnabled(nbt.getBoolean("OuterColorEnabled"));
+1 -1
View File
@@ -124,7 +124,7 @@ import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-prerelease3")
@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11")
public class CustomNpcs {
@SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy")
+2 -2
View File
@@ -482,9 +482,9 @@ public class EventHooks {
NpcAPI.EVENT_BUS.post(event);
}
public static void onEnergyProjectileTick(EntityEnergyProjectile projectile) {
public static void onEnergyProjectileTick(EntityEnergyProjectile projectile, int tick) {
IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile);
EnergyProjectileEvent.UpdateEvent event = new EnergyProjectileEvent.UpdateEvent(wrapped);
EnergyProjectileEvent.UpdateEvent event = new EnergyProjectileEvent.UpdateEvent(wrapped, tick);
ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_TICK, event);
Entity owner = projectile.worldObj.getEntityByID(projectile.getOwnerEntityId());
@@ -88,7 +88,7 @@ public class ScriptDocument {
// Also matches ',' as a delimiter so that "int x, y, z;" is recognized
// (the first declarator "int x," is captured; continuations are scanned manually).
private static final Pattern FIELD_DECL_PATTERN = Pattern.compile(
"\\b([A-Za-z_][a-zA-Z0-9_.<>,[ \\t]\\[\\]]*)[ \\t]+([a-zA-Z_][a-zA-Z0-9_]*)[ \\t]*(=|;|,)");
"\\b([A-Za-z_][a-zA-Z0-9_.<>,[ \\t\\n\\r]\\[\\]]*)[ \\t]+([a-zA-Z_][a-zA-Z0-9_]*)[ \\t]*(=|;|,)");
private static final Pattern NEW_TYPE_PATTERN = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:<([^>]*)>)?");
// Cast expression pattern: captures type name inside cast parentheses
@@ -785,8 +785,10 @@ public class ScriptDocument {
*/
private void parseScriptTypes() {
// ========== PASS 1: Discover all type declarations (flat list) ==========
// Regex only anchors on keyword + name — generic clauses are extracted manually
// below to correctly handle nested angle brackets like <T extends Comparable<T>>.
Pattern typeDecl = Pattern.compile(
"(?:(?:public|private|protected|static|final|abstract)\\s+)*(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:<([^>]*)>)?\\s*(?:\\(\\))?\\s*(?:extends\\s+([A-Za-z_][a-zA-Z0-9_.]*))?\\s*(?:implements\\s+([A-Za-z_][a-zA-Z0-9_.,\\s]*))?\\s*\\{");
"(?:(?:public|private|protected|static|final|abstract)\\s+)*(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)");
List<RawTypeDeclaration> rawDeclarations = new ArrayList<>();
Matcher m = typeDecl.matcher(text);
@@ -796,16 +798,58 @@ public class ScriptDocument {
String kindStr = m.group(1);
String typeName = m.group(2);
String typeParamsClause = m.group(3);
String extendsClause = m.group(4);
String implementsClause = m.group(5);
int bodyStart = text.indexOf('{', m.start());
int bodyEnd = findMatchingBrace(bodyStart);
if (bodyEnd < 0) {
bodyEnd = text.length();
// Manually extract everything between the type name and the opening '{',
// handling nested angle brackets so <T extends Comparable<T>> is captured whole.
int scanPos = m.end();
while (scanPos < text.length() && Character.isWhitespace(text.charAt(scanPos)))
scanPos++;
// Extract type params clause <...> if present (depth-aware)
String typeParamsClause = null;
if (scanPos < text.length() && text.charAt(scanPos) == '<') {
int depth = 1, i = scanPos + 1;
while (i < text.length() && depth > 0) {
char c = text.charAt(i++);
if (c == '<') depth++;
else if (c == '>') depth--;
}
if (depth == 0) {
typeParamsClause = text.substring(scanPos + 1, i - 1);
scanPos = i;
while (scanPos < text.length() && Character.isWhitespace(text.charAt(scanPos)))
scanPos++;
}
}
// Find the opening brace, extracting extends/implements from text between here and '{'
int bracePos = scanPos;
int angleDepth = 0;
while (bracePos < text.length()) {
char c = text.charAt(bracePos);
if (c == '<') angleDepth++;
else if (c == '>') angleDepth--;
else if (c == '{' && angleDepth == 0) break;
bracePos++;
}
if (bracePos >= text.length()) continue;
String betweenNameAndBrace = text.substring(scanPos, bracePos).trim();
// Parse extends/implements from that window
String extendsClause = null;
String implementsClause = null;
java.util.regex.Matcher extM = Pattern.compile(
"extends\\s+([A-Za-z_][a-zA-Z0-9_.]*(?:<[^{]*>)?)").matcher(betweenNameAndBrace);
if (extM.find()) extendsClause = extM.group(1).trim();
java.util.regex.Matcher implM = Pattern.compile(
"implements\\s+(.+)$").matcher(betweenNameAndBrace);
if (implM.find()) implementsClause = implM.group(1).trim();
int bodyStart = bracePos;
int bodyEnd = findMatchingBrace(bodyStart);
if (bodyEnd < 0) bodyEnd = text.length();
TypeInfo.Kind kind;
switch (kindStr) {
case "interface": kind = TypeInfo.Kind.INTERFACE; break;
@@ -813,7 +857,7 @@ public class ScriptDocument {
default: kind = TypeInfo.Kind.CLASS; break;
}
String fullMatch = text.substring(m.start(), m.end());
String fullMatch = text.substring(m.start(), bodyStart + 1);
int modifiers = parseModifiers(fullMatch);
JSDocInfo jsDoc = jsDocParser.extractJSDocBefore(text, m.start());
@@ -870,6 +914,11 @@ public class ScriptDocument {
// Resolve extends clause (parent class inheritance)
if (raw.extendsClause != null && !raw.extendsClause.trim().isEmpty()) {
String parentName = raw.extendsClause.trim();
// Strip generic args (e.g., "NumberBox<T>" -> "NumberBox") for type resolution
int genIdx = parentName.indexOf('<');
if (genIdx >= 0) {
parentName = parentName.substring(0, genIdx);
}
TypeInfo parentType = resolveType(parentName, raw.declOffset);
if (parentType == null) {
parentType = TypeInfo.unresolved(parentName, parentName);
@@ -884,6 +933,11 @@ public class ScriptDocument {
String trimmedName = ifaceName.trim();
if (trimmedName.isEmpty())
continue;
// Strip generic args (e.g., "Comparable<T>" -> "Comparable")
int genIdx = trimmedName.indexOf('<');
if (genIdx >= 0) {
trimmedName = trimmedName.substring(0, genIdx);
}
TypeInfo ifaceType = resolveType(trimmedName, raw.declOffset);
if (ifaceType == null) {
ifaceType = TypeInfo.unresolved(trimmedName, trimmedName);
@@ -920,14 +974,16 @@ public class ScriptDocument {
String boundName = null;
if (extendsIdx >= 0) {
String afterExtends = trimmed.substring(extendsIdx + 7).trim();
if (!afterExtends.isEmpty()) {
// Split on & to get the primary bound (first one); additional bounds are interfaces
String primaryBound = afterExtends.split("&")[0].trim();
if (!primaryBound.isEmpty()) {
int boundEndIdx = 0;
while (boundEndIdx < afterExtends.length() &&
(Character.isJavaIdentifierPart(afterExtends.charAt(boundEndIdx)) || afterExtends.charAt(boundEndIdx) == '.')) {
while (boundEndIdx < primaryBound.length() &&
(Character.isJavaIdentifierPart(primaryBound.charAt(boundEndIdx)) || primaryBound.charAt(boundEndIdx) == '.')) {
boundEndIdx++;
}
if (boundEndIdx > 0) {
boundName = afterExtends.substring(0, boundEndIdx);
boundName = primaryBound.substring(0, boundEndIdx);
}
}
}
@@ -941,7 +997,7 @@ public class ScriptDocument {
}
scriptType.addDeclaredTypeParam(typeParam);
}
}
}
private TypeInfo parameterizeWithClause(TypeInfo baseType, String typeArgsClause, int position) {
String[] argNames = typeArgsClause.split(",");
@@ -964,42 +1020,68 @@ public class ScriptDocument {
private void markTypeParamDeclarations(List<ScriptLine.Mark> marks, int afterNameEnd, ScriptTypeInfo typeInfo) {
int ltPos = text.indexOf('<', afterNameEnd);
if (ltPos < 0 || ltPos > afterNameEnd + 5) return;
int gtPos = text.indexOf('>', ltPos);
if (gtPos < 0) return;
String clause = text.substring(ltPos + 1, gtPos);
int offset = ltPos + 1;
for (TypeParamInfo param : typeInfo.getDeclaredTypeParams()) {
int paramIdx = clause.indexOf(param.getName(), offset - (ltPos + 1));
if (paramIdx >= 0) {
int paramStart = ltPos + 1 + paramIdx;
int paramLen = param.getName().length();
int paramEnd = paramStart + paramLen;
marks.add(new ScriptLine.Mark(paramStart, paramEnd, TokenType.GENERIC_TYPE_PARAM, param));
int depth = 1, i = ltPos + 1;
while (i < text.length() && depth > 0) {
char c = text.charAt(i++);
if (c == '<') depth++;
else if (c == '>') depth--;
}
if (depth != 0) return;
int gtPos = i - 1;
int searchFrom = paramIdx + param.getName().length();
int extendsIdx = clause.indexOf("extends", searchFrom);
if (extendsIdx >= 0) {
int extendsStart = ltPos + 1 + extendsIdx;
marks.add(new ScriptLine.Mark(extendsStart, extendsStart + 7, TokenType.KEYWORD));
markTypeParamsInClause(marks, ltPos + 1, gtPos - 1, typeInfo.getDeclaredTypeParams());
}
TypeInfo boundType = param.getBoundTypeInfo();
if (boundType != null) {
String remaining = clause.substring(extendsIdx + 7).trim();
if (!remaining.isEmpty()) {
int boundEnd = remaining.indexOf(',');
if (boundEnd < 0) boundEnd = remaining.indexOf('&');
String boundName = boundEnd < 0 ? remaining : remaining.substring(0, boundEnd).trim();
if (!boundName.isEmpty()) {
int boundStart = extendsStart + 7;
while (boundStart < gtPos && Character.isWhitespace(text.charAt(boundStart))) boundStart++;
marks.add(new ScriptLine.Mark(boundStart, boundStart + boundName.length(), TokenType.getByType(boundType), boundType));
}
}
}
private void markTypeParamsInClause(List<ScriptLine.Mark> marks, int clauseStart, int clauseEnd, List<TypeParamInfo> typeParams) {
if (clauseStart >= clauseEnd) return;
String clause = text.substring(clauseStart, clauseEnd);
Map<String, TypeParamInfo> paramMap = new HashMap<>();
for (TypeParamInfo param : typeParams) {
paramMap.put(param.getName(), param);
}
int i = 0;
while (i < clause.length()) {
char c = clause.charAt(i);
if (c == '<') {
int innerStart = clauseStart + i + 1;
int depth = 1;
int j = i + 1;
while (j < clause.length() && depth > 0) {
char innerC = clause.charAt(j);
if (innerC == '<') depth++;
else if (innerC == '>') depth--;
j++;
}
if (depth == 0) {
markTypeParamsInClause(marks, innerStart, clauseStart + j - 1, typeParams);
i = j;
} else {
i++;
}
} else if (c == '&') {
i++;
} else if (c == ',') {
i++;
} else if (Character.isJavaIdentifierStart(c)) {
int start = i;
while (i < clause.length() && Character.isJavaIdentifierPart(clause.charAt(i))) {
i++;
}
String name = clause.substring(start, i);
TypeParamInfo param = paramMap.get(name);
if (param != null) {
int absStart = clauseStart + start;
int absEnd = clauseStart + i;
marks.add(new ScriptLine.Mark(absStart, absEnd, TokenType.GENERIC_TYPE_PARAM, param));
}
} else {
i++;
}
}
}
}
/**
* Parse fields and methods inside a script-defined type.
@@ -5038,8 +5120,8 @@ public class ScriptDocument {
// Extended pattern to capture extends and implements clauses
// Groups: 1=class|interface|enum, 2=TypeName, 3=extends clause, 4=implements clause
Pattern classWithInheritance = Pattern.compile(
"\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:<[^>]*>)?\\s*(?:\\(\\))?\\s*" +
"(?:(extends)\\s+([A-Za-z_][a-zA-Z0-9_.]*))?\\s*" +
"\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:<[^{]*>)?\\s*(?:\\(\\))?\\s*" +
"(?:(extends)\\s+([A-Za-z_][a-zA-Z0-9_.]*)(?:<[^{]*>)?)?\\s*" +
"(?:(implements)\\s+([A-Za-z_][a-zA-Z0-9_.,\\s]*))?\\s*(?=\\{)");
Matcher m = classWithInheritance.matcher(text);
@@ -5087,7 +5169,7 @@ public class ScriptDocument {
if (m.group(3) != null) {
//Extends key word
marks.add(new ScriptLine.Mark(m.start(3), m.end(3), TokenType.IMPORT_KEYWORD));
markExtendsClause(marks, m.start(4), m.group(4));
markExtendsClause(marks, m.start(4), m.group(4), typeInfo);
}
// Mark implements clause
@@ -5126,36 +5208,50 @@ public class ScriptDocument {
* Mark the extends clause with proper coloring.
* The parent class is colored based on its resolved type.
*/
private void markExtendsClause(List<ScriptLine.Mark> marks, int clauseStart, String parentName) {
private void markExtendsClause(List<ScriptLine.Mark> marks, int clauseStart, String parentName, ScriptTypeInfo typeInfo) {
String trimmedName = parentName.trim();
if (trimmedName.isEmpty())
return;
int actualStart = clauseStart;
while (actualStart < clauseStart + trimmedName.length() &&
Character.isWhitespace(text.charAt(actualStart))) {
actualStart++;
}
int baseNameEnd = actualStart;
while (baseNameEnd < text.length() && Character.isJavaIdentifierPart(text.charAt(baseNameEnd))) {
baseNameEnd++;
}
String baseName = text.substring(actualStart, baseNameEnd);
// Resolve the parent type with position context for inner class names
TypeInfo parentType = resolveType(trimmedName, clauseStart);
TokenType tokenType;
if (parentType != null && parentType.isResolved()) {
// Use the type's specific token type (CLASS, INTERFACE, ENUM)
tokenType = parentType.getTokenType();
} else {
// Unresolved - mark as undefined
tokenType = TokenType.UNDEFINED_VAR;
if (parentType == null) {
parentType = TypeInfo.unresolved(trimmedName, trimmedName);
}
}
// Find actual position in text (might have leading whitespace in the group)
int actualStart = clauseStart;
String fullClause = parentName;
while (actualStart < clauseStart + fullClause.length() &&
Character.isWhitespace(text.charAt(actualStart))) {
actualStart++;
}
marks.add(new ScriptLine.Mark(actualStart, baseNameEnd, tokenType, parentType));
marks.add(new ScriptLine.Mark(actualStart, actualStart + trimmedName.length(), tokenType, parentType));
if (typeInfo != null && typeInfo.hasDeclaredTypeParams() && baseNameEnd < text.length() && text.charAt(baseNameEnd) == '<') {
int ltPos = baseNameEnd;
int depth = 1;
int i = ltPos + 1;
while (i < text.length() && depth > 0) {
char c = text.charAt(i++);
if (c == '<') depth++;
else if (c == '>') depth--;
}
if (depth == 0) {
markTypeParamsInClause(marks, ltPos + 1, i - 1, typeInfo.getDeclaredTypeParams());
}
}
}
/**
@@ -6059,7 +6155,7 @@ public class ScriptDocument {
while (actualStart < i && Character.isWhitespace(text.charAt(actualStart))) {
actualStart++;
}
int actualEnd = i;
int actualEnd = i;
while (actualEnd > actualStart && Character.isWhitespace(text.charAt(actualEnd - 1))) {
actualEnd--;
}
@@ -49,10 +49,16 @@ public class EnergyProjectileEvent extends CustomNPCsEvent implements IEnergyPro
* energyProjectileTick
*/
public static class UpdateEvent extends EnergyProjectileEvent implements IEnergyProjectileEvent.UpdateEvent {
public UpdateEvent(IEnergyProjectile projectile) {
private final int tick;
public UpdateEvent(IEnergyProjectile projectile, int tick) {
super(projectile);
this.tick = tick;
}
@Override
public int getTick() { return this.tick; }
@Override
public String getHookName() {
return EnumScriptType.ENERGY_PROJECTILE_TICK.function;