Skip to content

Commit 4667866

Browse files
committed
feat: add title screen BGM/assets, text effects, and new project wizard
1 parent 5a571ec commit 4667866

File tree

12 files changed

+1506
-113
lines changed

12 files changed

+1506
-113
lines changed

core/src/main/java/com/jvn/core/menu/MainMenuScene.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ public class MainMenuScene implements Scene {
2424
private final String defaultScriptName;
2525
private final AudioFacade audio;
2626
private int selected = 0;
27+
28+
// Title screen configuration
29+
private String titleBgmPath = null;
30+
private double titleBgmVolume = 0.7;
31+
private boolean bgmStarted = false;
2732

2833
public MainMenuScene(Engine engine, VnSettings settingsModel, VnSaveManager saveManager, String defaultScriptName, AudioFacade audio) {
2934
this.engine = engine;
@@ -33,6 +38,17 @@ public MainMenuScene(Engine engine, VnSettings settingsModel, VnSaveManager save
3338
this.audio = audio;
3439
}
3540

41+
/**
42+
* Configure title screen BGM
43+
*/
44+
public void setTitleBgm(String path, double volume) {
45+
this.titleBgmPath = path;
46+
this.titleBgmVolume = Math.max(0, Math.min(1, volume));
47+
}
48+
49+
public String getTitleBgmPath() { return titleBgmPath; }
50+
public double getTitleBgmVolume() { return titleBgmVolume; }
51+
3652
public int getSelected() { return selected; }
3753
public void moveSelection(int delta) {
3854
int count = 4;
@@ -104,5 +120,28 @@ private VnScenario loadScenario(String scriptName) {
104120

105121
@Override
106122
public void update(long deltaMs) {
123+
// Start title BGM on first update if configured
124+
if (!bgmStarted && titleBgmPath != null && audio != null) {
125+
audio.setBgmVolume((float) titleBgmVolume);
126+
audio.playBgm(titleBgmPath, true);
127+
bgmStarted = true;
128+
}
129+
}
130+
131+
@Override
132+
public void onEnter() {
133+
// Resume title BGM if returning to menu
134+
if (titleBgmPath != null && audio != null) {
135+
if (!bgmStarted) {
136+
audio.setBgmVolume((float) titleBgmVolume);
137+
audio.playBgm(titleBgmPath, true);
138+
bgmStarted = true;
139+
}
140+
}
141+
}
142+
143+
@Override
144+
public void onExit() {
145+
// Don't stop BGM when pushing new scene - let child scenes control it
107146
}
108147
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.jvn.core.vn.text;
2+
3+
/**
4+
* Text effect types for dialogue rendering.
5+
*/
6+
public enum TextEffect {
7+
NONE, // No effect
8+
SHAKE, // Text shakes/vibrates
9+
WAVE, // Text moves in a wave pattern
10+
BOUNCE, // Text bounces up and down
11+
FADE_IN, // Text fades in
12+
RAINBOW, // Text cycles through colors
13+
BOLD, // Bold text (render thicker)
14+
ITALIC, // Italic text (render slanted)
15+
TYPEWRITER // Extra delay between characters
16+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package com.jvn.core.vn.text;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.regex.Matcher;
6+
import java.util.regex.Pattern;
7+
8+
/**
9+
* Parses inline markup in dialogue text and converts to TextSpan list.
10+
*
11+
* Markup format:
12+
* - {shake}text{/shake} - Shaking text
13+
* - {wave}text{/wave} - Wave effect
14+
* - {bounce}text{/bounce} - Bouncing text
15+
* - {color=#FF0000}text{/color} - Colored text
16+
* - {speed=0.5}text{/speed} - Slower text (0.5x)
17+
* - {speed=2.0}text{/speed} - Faster text (2x)
18+
* - {delay=500} - 500ms pause
19+
* - {b}text{/b} - Bold
20+
* - {i}text{/i} - Italic
21+
* - {rainbow}text{/rainbow} - Rainbow colors
22+
*
23+
* Effects can be nested: {shake}{color=#FF0000}scary{/color}{/shake}
24+
*/
25+
public class TextParser {
26+
27+
// Pattern to match tags like {shake}, {/shake}, {color=#FF0000}, {delay=500}
28+
private static final Pattern TAG_PATTERN = Pattern.compile("\\{(/?)([a-zA-Z]+)(?:=([^}]+))?\\}");
29+
30+
/**
31+
* Parse text with inline markup into a list of TextSpans
32+
*/
33+
public static List<TextSpan> parse(String text) {
34+
if (text == null || text.isEmpty()) {
35+
return List.of(new TextSpan(""));
36+
}
37+
38+
List<TextSpan> spans = new ArrayList<>();
39+
40+
// Track current state
41+
TextEffect currentEffect = TextEffect.NONE;
42+
String currentColor = null;
43+
float currentSpeed = 1.0f;
44+
int pendingDelay = 0;
45+
46+
Matcher matcher = TAG_PATTERN.matcher(text);
47+
int lastEnd = 0;
48+
49+
while (matcher.find()) {
50+
// Add text before this tag
51+
if (matcher.start() > lastEnd) {
52+
String segment = text.substring(lastEnd, matcher.start());
53+
if (!segment.isEmpty()) {
54+
spans.add(new TextSpan(segment, currentEffect, currentColor, currentSpeed, pendingDelay));
55+
pendingDelay = 0; // Clear delay after use
56+
}
57+
}
58+
59+
String isClosing = matcher.group(1);
60+
String tagName = matcher.group(2).toLowerCase();
61+
String tagValue = matcher.group(3);
62+
63+
if (isClosing.isEmpty()) {
64+
// Opening tag
65+
switch (tagName) {
66+
case "shake" -> currentEffect = TextEffect.SHAKE;
67+
case "wave" -> currentEffect = TextEffect.WAVE;
68+
case "bounce" -> currentEffect = TextEffect.BOUNCE;
69+
case "rainbow" -> currentEffect = TextEffect.RAINBOW;
70+
case "b", "bold" -> currentEffect = TextEffect.BOLD;
71+
case "i", "italic" -> currentEffect = TextEffect.ITALIC;
72+
case "color" -> {
73+
if (tagValue != null) currentColor = tagValue;
74+
}
75+
case "speed" -> {
76+
if (tagValue != null) {
77+
try { currentSpeed = Float.parseFloat(tagValue); }
78+
catch (NumberFormatException ignored) {}
79+
}
80+
}
81+
case "delay" -> {
82+
if (tagValue != null) {
83+
try { pendingDelay = Integer.parseInt(tagValue); }
84+
catch (NumberFormatException ignored) {}
85+
}
86+
}
87+
}
88+
} else {
89+
// Closing tag
90+
switch (tagName) {
91+
case "shake", "wave", "bounce", "rainbow", "b", "bold", "i", "italic" -> currentEffect = TextEffect.NONE;
92+
case "color" -> currentColor = null;
93+
case "speed" -> currentSpeed = 1.0f;
94+
}
95+
}
96+
97+
lastEnd = matcher.end();
98+
}
99+
100+
// Add remaining text
101+
if (lastEnd < text.length()) {
102+
String segment = text.substring(lastEnd);
103+
if (!segment.isEmpty()) {
104+
spans.add(new TextSpan(segment, currentEffect, currentColor, currentSpeed, pendingDelay));
105+
}
106+
}
107+
108+
// Ensure at least one span
109+
if (spans.isEmpty()) {
110+
spans.add(new TextSpan(""));
111+
}
112+
113+
return spans;
114+
}
115+
116+
/**
117+
* Get plain text without markup tags
118+
*/
119+
public static String stripTags(String text) {
120+
if (text == null) return "";
121+
return TAG_PATTERN.matcher(text).replaceAll("");
122+
}
123+
124+
/**
125+
* Calculate total character count excluding markup
126+
*/
127+
public static int plainLength(String text) {
128+
return stripTags(text).length();
129+
}
130+
131+
/**
132+
* Get character at reveal index, accounting for speed modifiers.
133+
* Returns the actual character position and any accumulated delay.
134+
*/
135+
public static RevealInfo getRevealInfo(List<TextSpan> spans, int revealIndex) {
136+
int charCount = 0;
137+
int totalDelay = 0;
138+
float avgSpeed = 1.0f;
139+
140+
for (TextSpan span : spans) {
141+
int spanLen = span.length();
142+
if (charCount + spanLen > revealIndex) {
143+
// This span contains the reveal position
144+
int posInSpan = revealIndex - charCount;
145+
totalDelay += span.getDelayMs();
146+
avgSpeed = span.getSpeedMultiplier();
147+
return new RevealInfo(revealIndex, totalDelay, avgSpeed, span);
148+
}
149+
charCount += spanLen;
150+
totalDelay += span.getDelayMs();
151+
}
152+
153+
// Past end
154+
return new RevealInfo(charCount, totalDelay, 1.0f, null);
155+
}
156+
157+
/**
158+
* Information about a reveal position
159+
*/
160+
public static class RevealInfo {
161+
public final int charIndex;
162+
public final int accumulatedDelayMs;
163+
public final float speedMultiplier;
164+
public final TextSpan currentSpan;
165+
166+
public RevealInfo(int charIndex, int accumulatedDelayMs, float speedMultiplier, TextSpan currentSpan) {
167+
this.charIndex = charIndex;
168+
this.accumulatedDelayMs = accumulatedDelayMs;
169+
this.speedMultiplier = speedMultiplier;
170+
this.currentSpan = currentSpan;
171+
}
172+
}
173+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.jvn.core.vn.text;
2+
3+
/**
4+
* Represents a span of text with optional effects.
5+
* Effects are applied via inline markup in dialogue text.
6+
*/
7+
public class TextSpan {
8+
private final String text;
9+
private final TextEffect effect;
10+
private final String colorHex; // e.g., "#FF0000" for red
11+
private final float speedMultiplier; // 1.0 = normal, 0.5 = half speed, 2.0 = double speed
12+
private final int delayMs; // pause before this span
13+
14+
public TextSpan(String text) {
15+
this(text, TextEffect.NONE, null, 1.0f, 0);
16+
}
17+
18+
public TextSpan(String text, TextEffect effect, String colorHex, float speedMultiplier, int delayMs) {
19+
this.text = text;
20+
this.effect = effect;
21+
this.colorHex = colorHex;
22+
this.speedMultiplier = speedMultiplier;
23+
this.delayMs = delayMs;
24+
}
25+
26+
public String getText() { return text; }
27+
public TextEffect getEffect() { return effect; }
28+
public String getColorHex() { return colorHex; }
29+
public float getSpeedMultiplier() { return speedMultiplier; }
30+
public int getDelayMs() { return delayMs; }
31+
32+
public boolean hasEffect() { return effect != TextEffect.NONE; }
33+
public boolean hasColor() { return colorHex != null && !colorHex.isEmpty(); }
34+
public boolean hasSpeedChange() { return speedMultiplier != 1.0f; }
35+
public boolean hasDelay() { return delayMs > 0; }
36+
37+
public int length() { return text != null ? text.length() : 0; }
38+
39+
@Override
40+
public String toString() {
41+
return "TextSpan{" + text + ", effect=" + effect + ", color=" + colorHex + ", speed=" + speedMultiplier + "}";
42+
}
43+
}

0 commit comments

Comments
 (0)