1use std::sync::Arc;
13
14use lattice_cells::{Cell, HeaderlineRow, SimpleHeaderlineHandle};
15use lattice_mode::registry::ModeRegistry;
16use lattice_mode::{Keymap, LifecycleFuture, Mode, ModeContext, ModeId, ModeKind};
17
18pub struct TutorMode;
31
32impl TutorMode {
33 pub fn mode_id() -> ModeId {
34 ModeId::new("tutor-mode")
35 }
36}
37
38impl Mode for TutorMode {
39 type Guard = ();
40
41 fn id(&self) -> ModeId {
42 Self::mode_id()
43 }
44
45 fn kind(&self) -> ModeKind {
46 ModeKind::Minor
47 }
48
49 fn keymap(&self) -> Keymap {
54 use std::sync::OnceLock;
55 static ENTRIES: OnceLock<Vec<lattice_mode::KeymapEntry>> = OnceLock::new();
56 Keymap::from_entries(ENTRIES.get_or_init(|| {
57 vec![
58 lattice_mode::keymap_entry! {
59 mode: Normal, chord: "<CR>",
60 doc: "Advance to the next tutor exercise (or lesson)",
61 cmd: "ex:tutor-next"
62 },
63 lattice_mode::keymap_entry! {
64 mode: Normal, chord: "<C-j>",
65 doc: "Advance to the next tutor exercise (or lesson)",
66 cmd: "ex:tutor-next"
67 },
68 lattice_mode::keymap_entry! {
69 mode: Normal, chord: "<C-k>",
70 doc: "Retreat to the previous tutor exercise",
71 cmd: "ex:tutor-prev"
72 },
73 ]
74 }))
75 }
76
77 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
78 Box::pin(async { Ok(()) })
79 }
80}
81
82pub fn register_tutor_modes(registry: &mut ModeRegistry) {
86 registry
87 .register(TutorMode)
88 .expect("tutor-mode must register without conflict");
89}
90
91#[derive(Debug, Default)]
98pub struct TutorViewState {
99 pub spans: Vec<(String, u32)>,
101 pub row_bg: u32,
103}
104
105impl TutorViewState {
106 pub fn update_for_display(&mut self, session: &super::TutorSession, kind: TutorHudKind) {
110 let (spans, row_bg) = match kind {
111 TutorHudKind::Normal => tutor_headerline_spans(session),
112 TutorHudKind::StageClear => tutor_stage_clear_spans(session),
113 };
114 self.spans = spans;
115 self.row_bg = row_bg;
116 }
117}
118
119#[derive(Copy, Clone, Debug)]
124pub enum TutorHudKind {
125 Normal,
127 StageClear,
130}
131
132pub const TUTOR_PROVIDER_TAG: u64 = 0x7475_746F_725F_6865; #[derive(Clone)]
146pub struct TutorHeaderlineState(pub SimpleHeaderlineHandle<TutorViewState>);
147
148impl lattice_mode::BufferLocal for TutorHeaderlineState {
149 const NAME: &'static str = "tutor-mode.headerline";
150 const DOC: &'static str = "SimpleHeaderlineHandle for the tutor buffer's sticky HUD row.";
151 const OWNER_MODE: &'static str = "tutor-mode";
152
153 fn describe(&self) -> String {
154 format!("version={}", self.0.version())
155 }
156}
157
158mod pal {
167 pub const BG: u32 = 0x0a0a1a;
168 pub const HEART_ON: u32 = 0xff2a2a;
169 pub const HEART_OFF: u32 = 0x4a2020;
170 pub const SEP: u32 = 0x333355;
171 pub const LV_LABEL: u32 = 0x00ccaa;
172 pub const LV_NUM: u32 = 0xe0e0e0;
173 pub const SCORE_LABEL: u32 = 0xffd700;
174 pub const SCORE_VAL: u32 = 0xffffff;
175 pub const HI_LABEL: u32 = 0xff8c00;
176 pub const HI_VAL: u32 = 0xffd700;
177 pub const DESC: u32 = 0x88ccff;
178 pub const HINT_LABEL: u32 = 0xff8c00;
179 pub const HINT_VAL: u32 = 0xffcc44;
180 pub const CLEAR: u32 = 0x00ff88;
181 pub const GAME_OVER: u32 = 0xff4444;
182 pub const WIN: u32 = 0xff00ff;
183 pub const DONE: u32 = 0xffd700;
184 pub const KEY_HINT: u32 = 0x666699;
185}
186
187fn tutor_headerline_spans(session: &super::TutorSession) -> (Vec<(String, u32)>, u32) {
191 use super::TutorGameState;
192
193 let mut s: Vec<(String, u32)> = Vec::new();
194 let sep = (" | ".to_owned(), pal::SEP);
195
196 match &session.state {
197 TutorGameState::AllComplete => {
198 s.push((" *** YOU WIN! *** ".to_owned(), pal::WIN));
199 s.push(sep.clone());
200 s.push(("ALL LESSONS DONE".to_owned(), pal::DONE));
201 s.push(sep.clone());
202 s.push(("FINAL SCORE: ".to_owned(), pal::SCORE_LABEL));
203 s.push((format!("{}", session.score), pal::SCORE_VAL));
204 s.push((" ".to_owned(), pal::SCORE_VAL));
205 }
206 TutorGameState::GameOver => {
207 s.push((" === GAME OVER === ".to_owned(), pal::GAME_OVER));
208 s.push(sep.clone());
209 s.push(("LV.".to_owned(), pal::LV_LABEL));
210 s.push((
211 format!("{}-{}", session.lesson, session.current + 1),
212 pal::LV_NUM,
213 ));
214 s.push(sep.clone());
215 s.push(("SCORE: ".to_owned(), pal::SCORE_LABEL));
216 s.push((format!("{:>5}", session.score), pal::SCORE_VAL));
217 s.push(sep.clone());
218 s.push(("<CR>=skip <C-k>=retry ".to_owned(), pal::KEY_HINT));
219 }
220 TutorGameState::Active => {
221 if session.is_complete() {
222 let new_record = session.score > session.high_score && session.high_score > 0;
223 s.push((" *** LESSON CLEAR! *** ".to_owned(), pal::CLEAR));
224 s.push(sep.clone());
225 s.push(("LV.".to_owned(), pal::LV_LABEL));
226 s.push((format!("{}", session.lesson), pal::LV_NUM));
227 s.push(sep.clone());
228 s.push(("SCORE: ".to_owned(), pal::SCORE_LABEL));
229 s.push((format!("{:>5}", session.score), pal::SCORE_VAL));
230 if session.high_score > 0 {
231 s.push(sep.clone());
232 s.push(("HI: ".to_owned(), pal::HI_LABEL));
233 s.push((format!("{:>5}", session.high_score), pal::HI_VAL));
234 }
235 if new_record {
236 s.push((" NEW RECORD!".to_owned(), pal::WIN));
237 }
238 s.push(sep.clone());
239 s.push(("<CR>=next lesson ".to_owned(), pal::KEY_HINT));
240 } else {
241 s.push((" ".to_owned(), pal::SEP));
242 for i in 0..super::MAX_LIVES {
243 if i < session.lives {
244 s.push(("♥".to_owned(), pal::HEART_ON));
245 } else {
246 s.push(("♡".to_owned(), pal::HEART_OFF));
247 }
248 }
249 s.push(sep.clone());
250 s.push(("LV.".to_owned(), pal::LV_LABEL));
251 s.push((
252 format!("{}-{}", session.lesson, session.current + 1),
253 pal::LV_NUM,
254 ));
255 s.push(sep.clone());
256 s.push(("SCORE: ".to_owned(), pal::SCORE_LABEL));
257 s.push((format!("{:>5}", session.score), pal::SCORE_VAL));
258 if session.high_score > 0 {
259 s.push(sep.clone());
260 s.push(("HI: ".to_owned(), pal::HI_LABEL));
261 s.push((format!("{:>5}", session.high_score), pal::HI_VAL));
262 }
263 if let Some(ex) = session.current_exercise() {
264 s.push(sep.clone());
265 s.push((ex.description.clone(), pal::DESC));
266 if session.attempt_count >= 2 && !ex.hint.is_empty() {
267 s.push((" Hint: ".to_owned(), pal::HINT_LABEL));
268 s.push((ex.hint.clone(), pal::HINT_VAL));
269 }
270 }
271 s.push((" ".to_owned(), pal::SEP));
272 }
273 }
274 }
275
276 (s, pal::BG)
277}
278
279fn tutor_stage_clear_spans(session: &super::TutorSession) -> (Vec<(String, u32)>, u32) {
281 let ex_id = session
282 .current_exercise()
283 .map(|e| e.id.clone())
284 .unwrap_or_default();
285 let sep = (" | ".to_owned(), pal::SEP);
286 let s = vec![
287 (" *** STAGE CLEAR! *** ".to_owned(), pal::CLEAR),
288 sep.clone(),
289 ("LV.".to_owned(), pal::LV_LABEL),
290 (
291 format!("{}-{}", session.lesson, session.current + 1),
292 pal::LV_NUM,
293 ),
294 sep.clone(),
295 ("SCORE: ".to_owned(), pal::SCORE_LABEL),
296 (format!("{:>5}", session.score), pal::SCORE_VAL),
297 sep.clone(),
298 (format!("Ex {} done", ex_id), pal::DESC),
299 (" <CR>=next ".to_owned(), pal::KEY_HINT),
300 ];
301 (s, pal::BG)
302}
303
304pub fn render_tutor_headerline(s: &TutorViewState) -> Option<HeaderlineRow> {
310 if s.spans.is_empty() {
311 return None;
312 }
313 let cells: Arc<[Cell]> = s
314 .spans
315 .iter()
316 .flat_map(|(text, fg)| {
317 let fg = *fg;
318 text.chars().map(move |c| Cell::new(c as u32, fg, 0, 0))
319 })
320 .collect::<Vec<_>>()
321 .into();
322 Some(HeaderlineRow {
323 cells,
324 bg: Some(s.row_bg),
325 })
326}