dotzuki-cli 0.7.0

dotzuki CLI: scaffold (dotzuki new), compile-check (dotzuki check) and play (dotzuki run) zero-Rust JRPG projects
import { preferences } from '@kit.ArkData';
import { util } from '@kit.ArkTS';
import DotzukiContext from '../types/DotzukiContext';

const SAVE_STORE = 'dotzuki-game';
const SAVE_KEY = 'save-v1';

@Entry
@Component
struct Index {
  private native: DotzukiContext | undefined;
  private inputBits: number = 0;
  private saveTimer: number = -1;
  private saving: boolean = false;
  private lastSave: string = '';

  async startGame(context: DotzukiContext): Promise<void> {
    const pack = await getContext(this).resourceManager.getRawFileContent('game.dzpk');
    const store = await preferences.getPreferences(getContext(this), SAVE_STORE);
    const save = await store.get(SAVE_KEY, '') as string;
    if (!context.start(pack, save)) {
      throw new Error('dotzuki native runtime failed to start');
    }
    this.native = context;
    this.lastSave = save;
    if (this.saveTimer !== -1) clearInterval(this.saveTimer);
    this.saveTimer = setInterval(() => { this.persistSave(); }, 500);
  }

  async persistSave(): Promise<void> {
    if (this.saving) return;
    const bytes = this.native?.exportSave();
    if (!bytes || bytes.length === 0) {
      return;
    }
    const decoder: util.TextDecoder = util.TextDecoder.create('utf-8');
    const save: string = decoder.decodeToString(bytes);
    if (save === this.lastSave) return;
    this.saving = true;
    try {
      const store = await preferences.getPreferences(getContext(this), SAVE_STORE);
      await store.put(SAVE_KEY, save);
      await store.flush();
      this.lastSave = save;
    } finally { this.saving = false; }
  }

  setButton(bit: number, pressed: boolean): void {
    if (pressed) {
      this.inputBits |= bit;
    } else {
      this.inputBits &= ~bit;
    }
    this.native?.setInput(this.inputBits);
  }

  @Builder
  button(label: string, bit: number) {
    Button(label)
      .width(label.length > 2 ? 74 : 54)
      .height(54)
      .fontSize(label.length > 2 ? 10 : 20)
      .fontWeight(FontWeight.Bold)
      .backgroundColor('#66333A4D')
      .onTouch((event: TouchEvent) => {
        this.setButton(bit, event.type === TouchType.Down || event.type === TouchType.Move);
        return true;
      })
  }

  onPageShow(): void {
    this.native?.resume();
  }

  onPageHide(): void {
    this.inputBits = 0;
    this.native?.setInput(0);
    this.native?.pause();
    this.persistSave();
  }

  aboutToDisappear(): void {
    if (this.saveTimer !== -1) clearInterval(this.saveTimer);
    this.saveTimer = -1;
    this.native?.pause();
    this.persistSave();
  }

  build() {
    Column() {
      XComponent({ id: 'dotzuki', type: XComponentType.SURFACE, libraryname: 'entry' })
        .width('100%')
        .layoutWeight(1)
        .backgroundColor(Color.Black)
        .onLoad((context) => {
          this.startGame(context as DotzukiContext);
        })

      Row() {
        Stack() {
          Row() { this.button('←', 1 << 5); this.button('→', 1 << 4) }
          Column() { this.button('↑', 1 << 6); Blank().height(38); this.button('↓', 1 << 7) }
        }
        .width(150)
        .height(150)

        Blank()

        Column() {
          Row() { this.button('SELECT', 1 << 2); this.button('START', 1 << 3) }
          Row() { this.button('B', 1 << 1); this.button('A', 1 << 0) }
        }
      }
      .width('100%')
      .padding({ left: 20, right: 20, bottom: 20 })
      .alignItems(VerticalAlign.Bottom)
      .height(200)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Black)
  }
}