Compare commits
10 Commits
5bba77c8ea
...
262983a8b0
| Author | SHA1 | Date | |
|---|---|---|---|
| 262983a8b0 | |||
| 643ad6a2b5 | |||
| e7c4405a82 | |||
| b6acbdfbed | |||
| a29a5f21f8 | |||
| 89b0ad05ec | |||
| b975b5e805 | |||
| 30c1a9863f | |||
| 90ce89f880 | |||
| dadc6f69ef |
@@ -0,0 +1,7 @@
|
||||
.git
|
||||
.github
|
||||
.godot
|
||||
dist
|
||||
addons/godot-sqlite/bin
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [Excello-Recherche-Education] # Up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
# patreon: # Replace with a single Patreon username
|
||||
# open_collective: # Replace with a single Open Collective username
|
||||
# ko_fi: # Replace with a single Ko-fi username
|
||||
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
# liberapay: # Replace with a single Liberapay username
|
||||
# issuehunt: # Replace with a single IssueHunt username
|
||||
# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
# polar: # Replace with a single Polar username
|
||||
# buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
# thanks_dev: # Replace with a single thanks.dev username
|
||||
# custom: # Up to 4 custom URLs e.g., [link1, link2]
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
ARG GODOT_VERSION=4.6.3
|
||||
|
||||
FROM ubuntu:24.04 AS web-builder
|
||||
ARG GODOT_VERSION
|
||||
ARG SQLITE_VERSION=4.8
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl libfontconfig1 unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN case "$TARGETARCH" in \
|
||||
amd64) GODOT_ARCH="x86_64" ;; \
|
||||
arm64) GODOT_ARCH="arm64" ;; \
|
||||
*) echo "Unsupported build architecture: $TARGETARCH" >&2; exit 1 ;; \
|
||||
esac \
|
||||
&& curl -fsSL "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}-stable/Godot_v${GODOT_VERSION}-stable_linux.${GODOT_ARCH}.zip" -o /tmp/godot.zip \
|
||||
&& unzip -q /tmp/godot.zip -d /usr/local/bin \
|
||||
&& mv "/usr/local/bin/Godot_v${GODOT_VERSION}-stable_linux.${GODOT_ARCH}" /usr/local/bin/godot \
|
||||
&& chmod +x /usr/local/bin/godot \
|
||||
&& mkdir -p "/root/.local/share/godot/export_templates/${GODOT_VERSION}.stable" \
|
||||
&& curl -fsSL "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}-stable/Godot_v${GODOT_VERSION}-stable_export_templates.tpz" -o /tmp/templates.tpz \
|
||||
&& unzip -q /tmp/templates.tpz -d /tmp/godot-templates \
|
||||
&& mv /tmp/godot-templates/templates/* "/root/.local/share/godot/export_templates/${GODOT_VERSION}.stable/" \
|
||||
&& rm -rf /tmp/godot.zip /tmp/templates.tpz /tmp/godot-templates
|
||||
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p addons/godot-sqlite/bin \
|
||||
&& curl -fsSL "https://github.com/2shady4u/godot-sqlite/releases/download/v${SQLITE_VERSION}/addons.zip" -o /tmp/godot-sqlite.zip \
|
||||
&& unzip -j /tmp/godot-sqlite.zip \
|
||||
'addons/godot-sqlite/bin/libgdsqlite.web.template_release.wasm32.wasm' \
|
||||
-d addons/godot-sqlite/bin \
|
||||
&& rm /tmp/godot-sqlite.zip \
|
||||
&& find assets -type f -name '*.import' -exec sed -i 's|^compress/mode=2$|compress/mode=0|' {} +
|
||||
|
||||
RUN mkdir -p dist \
|
||||
&& godot --headless --path /src --import \
|
||||
&& find .godot/imported -name 'title.png-*.ctex' ! -name '*.etc2.ctex' ! -name '*.s3tc.ctex' -type f -size +0c | grep -q . \
|
||||
&& find .godot/imported -name 'kalulu_sprite_sheet.png-*.ctex' ! -name '*.etc2.ctex' ! -name '*.s3tc.ctex' -type f -size +0c | grep -q .
|
||||
|
||||
RUN godot --headless --path /src --export-release "Web Test" dist/index.html \
|
||||
&& mv dist/index.pck dist/index-3.0.1-fr-joy.pck \
|
||||
&& sed -i 's|"index.pck":|"index-3.0.1-fr-joy.pck":|; s|"focusCanvas":true,|"focusCanvas":true,"mainPack":"index-3.0.1-fr-joy.pck",|' dist/index.html \
|
||||
&& test -s dist/index.html \
|
||||
&& test -s dist/index.wasm \
|
||||
&& test -s dist/index-3.0.1-fr-joy.pck
|
||||
|
||||
FROM alpine:3.22 AS french-content
|
||||
ARG FRENCH_PACK_VERSION=v20260615-9e5d0d4
|
||||
RUN apk add --no-cache ca-certificates curl \
|
||||
&& mkdir -p /content \
|
||||
&& curl -fsSL "https://github.com/Excello-Recherche-Education/Kalulu-Languages/releases/download/${FRENCH_PACK_VERSION}/fr_FR.zip" -o /content/fr_FR.zip \
|
||||
&& test -s /content/fr_FR.zip
|
||||
|
||||
FROM nginx:1.29.1-alpine AS runtime
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=web-builder /src/dist/ /usr/share/nginx/html/
|
||||
COPY --from=french-content /content/fr_FR.zip /usr/share/nginx/html/content/fr_FR.zip
|
||||
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD wget -q -O /dev/null http://127.0.0.1:8080/healthz || exit 1
|
||||
+18
-1
@@ -2,6 +2,23 @@
|
||||
|
||||
# 📖 Kalulu — Apprends à lire intelligemment
|
||||
|
||||
## Version Web Docker autonome — Joy
|
||||
|
||||
Cette variante déploie Kalulu en français sur `https://kalulu.agendx.ch` avec un profil local **Joy**, 5 ans, niveau débutant (Grande Section). Elle n’utilise pas l’API officielle de Kalulu : le pack `fr_FR` est servi par la même image et la progression reste dans le stockage IndexedDB du navigateur.
|
||||
|
||||
Prérequis : Docker avec BuildKit, un réseau Docker externe `traefik_proxy` et un Traefik fournissant HTTPS.
|
||||
|
||||
```bash
|
||||
docker network inspect traefik_proxy >/dev/null 2>&1 || docker network create traefik_proxy
|
||||
docker compose build kalulu
|
||||
docker compose up -d kalulu
|
||||
./scripts/verify-live.sh
|
||||
```
|
||||
|
||||
Le premier lancement charge environ 34 Mo d’application puis télécharge 66 Mo de contenu pédagogique depuis la même origine. Le pack est ensuite extrait et conservé localement ; cette première installation peut prendre quelques minutes. Effacer les données du site dans le navigateur réinitialise le profil et la progression.
|
||||
|
||||
L’image construit l’export avec Godot 4.6.3, les modèles d’export officiels et Godot-SQLite 4.8 (la version compilée pour Godot 4.6.3). Nginx sert WebAssembly avec les en-têtes COOP/COEP requis pour les threads.
|
||||
|
||||
## 🌍 Langues disponibles
|
||||
|
||||
- 🇬🇧 [English](README.md)
|
||||
@@ -126,4 +143,4 @@ Oui ! Des versions pour Windows, Mac et Linux sont disponibles dans la section [
|
||||
|
||||
### 🌐 Kalulu est-il disponible directement sur le web ?
|
||||
|
||||
Nous y travaillons, restez à l’écoute !
|
||||
Oui, cette variante autonome est disponible sur [kalulu.agendx.ch](https://kalulu.agendx.ch).
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
services:
|
||||
kalulu:
|
||||
image: kalulu-web:3.0.1-fr-joy
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: kalulu-web
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- traefik_proxy
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik_proxy
|
||||
- traefik.http.routers.kalulu.rule=Host(`kalulu.agendx.ch`)
|
||||
- traefik.http.routers.kalulu.entrypoints=websecure
|
||||
- traefik.http.routers.kalulu.tls=true
|
||||
- traefik.http.routers.kalulu.tls.certresolver=myresolver
|
||||
- traefik.http.routers.kalulu.middlewares=crowdsec-bouncer@docker
|
||||
- traefik.http.services.kalulu.loadbalancer.server.port=8080
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
networks:
|
||||
traefik_proxy:
|
||||
external: true
|
||||
+6
-6
@@ -38,7 +38,7 @@ architectures/armeabi-v7a=false
|
||||
architectures/arm64-v8a=true
|
||||
architectures/x86=false
|
||||
architectures/x86_64=false
|
||||
version/code=78
|
||||
version/code=79
|
||||
version/name=""
|
||||
package/unique_name="org.godotengine.kalulu"
|
||||
package/name="Kalulu"
|
||||
@@ -264,7 +264,7 @@ architectures/armeabi-v7a=false
|
||||
architectures/arm64-v8a=true
|
||||
architectures/x86=false
|
||||
architectures/x86_64=false
|
||||
version/code=78
|
||||
version/code=79
|
||||
version/name=""
|
||||
package/unique_name="org.godotengine.kalulu"
|
||||
package/name="Kalulu"
|
||||
@@ -490,7 +490,7 @@ architectures/armeabi-v7a=true
|
||||
architectures/arm64-v8a=false
|
||||
architectures/x86=false
|
||||
architectures/x86_64=false
|
||||
version/code=78
|
||||
version/code=79
|
||||
version/name=""
|
||||
package/unique_name="org.godotengine.kalulu"
|
||||
package/name="Kalulu"
|
||||
@@ -1360,7 +1360,7 @@ custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="../Export/web/Kalulu.html"
|
||||
export_path="dist/index.html"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
@@ -1378,9 +1378,9 @@ script_export_mode=2
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
variant/extensions_support=false
|
||||
variant/extensions_support=true
|
||||
variant/thread_support=true
|
||||
vram_texture_compression/for_desktop=true
|
||||
vram_texture_compression/for_desktop=false
|
||||
vram_texture_compression/for_mobile=false
|
||||
html/export_icon=true
|
||||
html/custom_html_shell=""
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer" always;
|
||||
|
||||
default_type application/octet-stream;
|
||||
types {
|
||||
text/html html;
|
||||
text/css css;
|
||||
application/javascript js;
|
||||
application/wasm wasm;
|
||||
application/json json;
|
||||
image/png png;
|
||||
image/svg+xml svg;
|
||||
image/x-icon ico;
|
||||
application/zip zip;
|
||||
}
|
||||
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
return 204;
|
||||
}
|
||||
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache" always;
|
||||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /content/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ~* \.(wasm|pck|js)$ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ boot_splash/bg_color=Color(0.141176, 0.141176, 0.141176, 1)
|
||||
config/icon="res://assets/kalulu_icon.png"
|
||||
custom/unlock_everything=false
|
||||
custom/unlock_everything.debug=false
|
||||
custom/local_web_mode=true
|
||||
|
||||
[audio]
|
||||
|
||||
@@ -36,6 +37,7 @@ Globals="*res://sources/language_tool/globals.gd"
|
||||
LessonLogger="*res://sources/utils/autoloads/lesson_logger.gd"
|
||||
MusicManager="*res://sources/utils/autoloads/music_manager.tscn"
|
||||
OpeningCurtain="*res://sources/utils/autoloads/opening_curtain.tscn"
|
||||
SceneLoader="*res://sources/utils/autoloads/scene_loader.gd"
|
||||
ServerManager="*res://sources/utils/autoloads/server_manager.tscn"
|
||||
UnicodeNormalizer="*res://sources/utils/autoloads/unicode_normalizer.gd"
|
||||
UserDataManager="*res://sources/utils/autoloads/user_data_manager.gd"
|
||||
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
BASE_URL="${1:-https://kalulu.agendx.ch}"
|
||||
HEADERS="$(mktemp)"
|
||||
trap 'rm -f "$HEADERS"' EXIT
|
||||
|
||||
curl -fsS -D "$HEADERS" -o /dev/null "$BASE_URL/"
|
||||
grep -qi '^cross-origin-opener-policy: same-origin' "$HEADERS"
|
||||
grep -qi '^cross-origin-embedder-policy: require-corp' "$HEADERS"
|
||||
curl -fsS -o /dev/null "$BASE_URL/index.wasm"
|
||||
curl -fsS -r 0-1023 -o /dev/null "$BASE_URL/content/fr_FR.zip"
|
||||
|
||||
printf 'Kalulu Web OK: %s\n' "$BASE_URL"
|
||||
+32
-35
@@ -20,21 +20,25 @@ const LESSON_BUTTON_OUTLINE_RADIUS: float = 192.0
|
||||
# button center, mirroring the movie icon just above it. The 384x384 button has
|
||||
# its center at y=192; the label (vertical-centered) then centers at y=(this+384)/2.
|
||||
const LESSON_BUTTON_LABEL_TOP_OFFSET: float = 104.0
|
||||
const GARDEN_SCENES: Array[PackedScene] = [
|
||||
preload("res://resources/gardens/garden_01.tscn"),
|
||||
preload("res://resources/gardens/garden_02.tscn"),
|
||||
preload("res://resources/gardens/garden_03.tscn"),
|
||||
preload("res://resources/gardens/garden_04.tscn"),
|
||||
preload("res://resources/gardens/garden_05.tscn"),
|
||||
preload("res://resources/gardens/garden_06.tscn"),
|
||||
preload("res://resources/gardens/garden_07.tscn"),
|
||||
preload("res://resources/gardens/garden_08.tscn"),
|
||||
preload("res://resources/gardens/garden_09.tscn"),
|
||||
preload("res://resources/gardens/garden_10.tscn"),
|
||||
preload("res://resources/gardens/garden_11.tscn"),
|
||||
preload("res://resources/gardens/garden_12.tscn"),
|
||||
# Loaded on demand instead of preloaded: this script never unloads (it has
|
||||
# static variables), so preloaded constants would pin every garden's assets in
|
||||
# memory for the whole app lifetime — including while minigames run, which
|
||||
# OOM-crashes low-memory devices.
|
||||
const GARDEN_SCENE_PATHS: Array[String] = [
|
||||
"res://resources/gardens/garden_01.tscn",
|
||||
"res://resources/gardens/garden_02.tscn",
|
||||
"res://resources/gardens/garden_03.tscn",
|
||||
"res://resources/gardens/garden_04.tscn",
|
||||
"res://resources/gardens/garden_05.tscn",
|
||||
"res://resources/gardens/garden_06.tscn",
|
||||
"res://resources/gardens/garden_07.tscn",
|
||||
"res://resources/gardens/garden_08.tscn",
|
||||
"res://resources/gardens/garden_09.tscn",
|
||||
"res://resources/gardens/garden_10.tscn",
|
||||
"res://resources/gardens/garden_11.tscn",
|
||||
"res://resources/gardens/garden_12.tscn",
|
||||
]
|
||||
const LOOK_AND_LEARN_SCENE: PackedScene = preload("res://sources/look_and_learn/look_and_learn.tscn")
|
||||
const LOOK_AND_LEARN_SCENE_PATH: String = "res://sources/look_and_learn/look_and_learn.tscn"
|
||||
const BOSS_BUTTON_SCENE: PackedScene = preload("res://sources/gardens/boss_button.tscn")
|
||||
const BOSS_MINIGAME_SCENE_PATH: String = "res://sources/minigames/boss/boss_minigame.tscn"
|
||||
const GARDEN_SIZE: int = 2400
|
||||
@@ -71,7 +75,6 @@ static var cached_layout_lessons: int = 0
|
||||
@export var minigames_body_icons: Array[Texture] = []
|
||||
@export var minigames_face_icons: Array[Texture] = []
|
||||
|
||||
var _minigame_scene_cache: Dictionary = {}
|
||||
var lessons: Dictionary = {}
|
||||
var _gardens_layout: GardensLayout
|
||||
var points: Array[Array] = []
|
||||
@@ -988,20 +991,13 @@ func _lesson_button_label_color(status: StudentProgression.Status) -> Color:
|
||||
return lesson_button.unlocked_label_color
|
||||
|
||||
|
||||
func _get_minigame_scene(scene_index: int) -> PackedScene:
|
||||
func _get_minigame_scene_path(scene_index: int) -> String:
|
||||
if scene_index < 0 or scene_index >= minigame_scene_paths.size():
|
||||
return null
|
||||
if _minigame_scene_cache.has(scene_index):
|
||||
return _minigame_scene_cache[scene_index] as PackedScene
|
||||
return ""
|
||||
var scene_path: String = minigame_scene_paths[scene_index]
|
||||
if scene_path.is_empty():
|
||||
return null
|
||||
var scene_resource: Resource = load(scene_path)
|
||||
if scene_resource is PackedScene:
|
||||
var packed_scene: PackedScene = scene_resource as PackedScene
|
||||
_minigame_scene_cache[scene_index] = packed_scene
|
||||
return packed_scene
|
||||
return null
|
||||
if scene_path.is_empty() or not ResourceLoader.exists(scene_path):
|
||||
return ""
|
||||
return scene_path
|
||||
|
||||
|
||||
func _count_completed_minigames(lesson_number: int) -> int:
|
||||
@@ -1073,7 +1069,8 @@ func add_gardens() -> void:
|
||||
for layout_index: int in range(gardens_layout.gardens.size()):
|
||||
Log.trace("Gardens: Preparing garden %s with layout index %s" % [str(garden_index), str(layout_index)])
|
||||
var garden_layout: GardenLayout = gardens_layout.gardens[layout_index]
|
||||
var garden: Garden = GARDEN_SCENES[layout_index].instantiate()
|
||||
var garden_scene: PackedScene = load(GARDEN_SCENE_PATHS[layout_index]) as PackedScene
|
||||
var garden: Garden = garden_scene.instantiate()
|
||||
garden_parent.add_child(garden)
|
||||
garden.garden_index = garden_index
|
||||
garden_index += 1
|
||||
@@ -1422,7 +1419,7 @@ func _on_lesson_button_pressed() -> void:
|
||||
current_garden_index = current_garden.garden_index,
|
||||
look_and_learn_completed = false
|
||||
}
|
||||
get_tree().change_scene_to_packed(LOOK_AND_LEARN_SCENE)
|
||||
SceneLoader.change_scene(LOOK_AND_LEARN_SCENE_PATH)
|
||||
|
||||
|
||||
func _on_boss_button_pressed(lesson_number: int, garden_index: int) -> void:
|
||||
@@ -1438,7 +1435,7 @@ func _on_boss_button_pressed(lesson_number: int, garden_index: int) -> void:
|
||||
skip_minigame_layout = true,
|
||||
boss_gate_lesson = lesson_number
|
||||
}
|
||||
get_tree().change_scene_to_file(BOSS_MINIGAME_SCENE_PATH)
|
||||
SceneLoader.change_scene(BOSS_MINIGAME_SCENE_PATH)
|
||||
|
||||
|
||||
func _on_final_boss_button_pressed(lesson_number: int, garden_index: int) -> void:
|
||||
@@ -1455,14 +1452,14 @@ func _on_final_boss_button_pressed(lesson_number: int, garden_index: int) -> voi
|
||||
boss_gate_lesson = lesson_number,
|
||||
is_final_boss = true
|
||||
}
|
||||
get_tree().change_scene_to_file(BOSS_MINIGAME_SCENE_PATH)
|
||||
SceneLoader.change_scene(BOSS_MINIGAME_SCENE_PATH)
|
||||
|
||||
|
||||
func _on_minigame_button_pressed(scene_index: int, minigame_number: int) -> void:
|
||||
if is_locked:
|
||||
return
|
||||
var minigame_scene: PackedScene = _get_minigame_scene(scene_index)
|
||||
if not minigame_scene:
|
||||
var scene_path: String = _get_minigame_scene_path(scene_index)
|
||||
if scene_path.is_empty():
|
||||
Log.error("Gardens: Missing minigame scene for index %d" % scene_index)
|
||||
return
|
||||
# Block a second wedge click during the curtain-close await below.
|
||||
@@ -1476,7 +1473,7 @@ func _on_minigame_button_pressed(scene_index: int, minigame_number: int) -> void
|
||||
minigame_number = minigame_number,
|
||||
minigame_completed = false
|
||||
}
|
||||
get_tree().change_scene_to_packed(minigame_scene)
|
||||
SceneLoader.change_scene(scene_path)
|
||||
|
||||
|
||||
func _on_scroll_container_gui_input(event: InputEvent) -> void:
|
||||
@@ -1515,7 +1512,7 @@ func _scroll_by_garden(p_direction: int) -> void:
|
||||
func _confirm_back_button_pressed() -> void:
|
||||
UserDataManager.logout_student()
|
||||
await (OpeningCurtain as OpeningCurtainClass).close()
|
||||
get_tree().change_scene_to_file("res://sources/menus/login/login.tscn")
|
||||
SceneLoader.change_scene("res://sources/menus/login/login.tscn")
|
||||
|
||||
|
||||
func _on_back_button_button_down() -> void:
|
||||
|
||||
@@ -160,7 +160,7 @@ func _back_to_gardens() -> void:
|
||||
Log.info("LookAndLearn: Returning to gardens for lesson %d" % lesson_nb)
|
||||
await (OpeningCurtain as OpeningCurtainClass).close()
|
||||
Gardens.transition_data = gardens_data
|
||||
get_tree().change_scene_to_file("res://sources/gardens/gardens.tscn")
|
||||
SceneLoader.change_scene("res://sources/gardens/gardens.tscn")
|
||||
|
||||
|
||||
func _on_back_button_pressed() -> void:
|
||||
|
||||
@@ -15,6 +15,12 @@ var teacher_settings: TeacherSettings
|
||||
|
||||
func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
if (ServerManager as ServerManagerClass).is_local_web_mode():
|
||||
UserDataManager.ensure_joy_local_profile()
|
||||
UserDataManager.set_language("fr_FR", true)
|
||||
UserDataManager.save_all()
|
||||
go_to_package_download()
|
||||
return
|
||||
device_language = UserDataManager.get_device_settings().language
|
||||
Log.trace("LanguageCheck: Starting with device language %s" % device_language)
|
||||
# Check the teacher settings, if we are logged in (this scene should not be accessible otherwise)
|
||||
|
||||
@@ -15,6 +15,9 @@ const MAIN_MENU_SCENE_PATH: String = "res://sources/menus/main/main_menu.tscn"
|
||||
const DEVICE_SELECTION_SCENE_PATH: String = "res://sources/menus/device_selection/device_selection.tscn"
|
||||
const LOGIN_SCENE_PATH: String = "res://sources/menus/login/login.tscn"
|
||||
const USER_LANGUAGE_RESOURCES_PATH: String = "user://language_resources"
|
||||
const LOCAL_WEB_PACK_PATH: String = "/content/fr_FR.zip"
|
||||
const LOCAL_WEB_PACK_VERSION: Dictionary = {"year": 2026, "month": 6, "day": 15}
|
||||
const GARDENS_SCENE_PATH: String = "res://sources/gardens/gardens.tscn"
|
||||
# Translation key shown in the error popup for each DownloadError value
|
||||
const ERROR_MESSAGES: Array[String] = [
|
||||
"DISCONNECTED_ERROR",
|
||||
@@ -63,6 +66,12 @@ func _ready() -> void:
|
||||
Log.trace("PackageDownloader: Using language from teacherSettings: %s" % language)
|
||||
current_language_path = USER_LANGUAGE_RESOURCES_PATH.path_join(language)
|
||||
current_language_version = UserDataManager.get_device_settings().language_versions.get(language, {})
|
||||
|
||||
if (ServerManager as ServerManagerClass).is_local_web_mode():
|
||||
server_language_version = LOCAL_WEB_PACK_VERSION
|
||||
var origin: String = str(JavaScriptBridge.eval("window.location.origin"))
|
||||
_start_pack_if_needed(origin + LOCAL_WEB_PACK_PATH)
|
||||
return
|
||||
|
||||
Log.trace("PackageDownloader: Checking internet access")
|
||||
if not await ServerManager.check_internet_access():
|
||||
@@ -97,6 +106,10 @@ func _ready() -> void:
|
||||
_show_error(DownloadError.DOWNLOAD_FAILED)
|
||||
return
|
||||
|
||||
_start_pack_if_needed(res.body.url as String)
|
||||
|
||||
|
||||
func _start_pack_if_needed(pack_url: String) -> void:
|
||||
# If the language pack is not already downloaded or an update is needed
|
||||
if not DirAccess.dir_exists_absolute(current_language_path) or current_language_version != server_language_version:
|
||||
Log.trace("PackageDownloader: A new version of the language pack has been detected.\n Current version = " + str(current_language_version) + "\n Server version = " + str(server_language_version))
|
||||
@@ -116,8 +129,8 @@ func _ready() -> void:
|
||||
# still run offline if the download fails; it is only removed during
|
||||
# extraction, once the new pack has been fully downloaded.
|
||||
http_request.set_download_file(USER_LANGUAGE_RESOURCES_PATH.path_join(language + ".zip"))
|
||||
Log.trace("PackageDownloader: Downloading pack from %s" % res.body.url)
|
||||
var request_error: Error = http_request.request(res.body.url as String)
|
||||
Log.trace("PackageDownloader: Downloading pack from %s" % pack_url)
|
||||
var request_error: Error = http_request.request(pack_url)
|
||||
if request_error != OK:
|
||||
Log.error("PackageDownloader: Cannot start language pack download: %s" % error_string(request_error))
|
||||
_show_error(DownloadError.DOWNLOAD_FAILED)
|
||||
@@ -255,6 +268,14 @@ func _go_to_next_scene() -> void:
|
||||
|
||||
if not server_language_version.is_empty():
|
||||
UserDataManager.set_language_version(language, server_language_version)
|
||||
|
||||
if (ServerManager as ServerManagerClass).is_local_web_mode():
|
||||
if not UserDataManager.login_student("123"):
|
||||
Log.error("PackageDownloader: Could not open Joy local profile")
|
||||
_show_error(DownloadError.DISCONNECTED)
|
||||
return
|
||||
SceneLoader.change_scene(GARDENS_SCENE_PATH)
|
||||
return
|
||||
|
||||
# Check if we have a valid device id
|
||||
if not UserDataManager.get_device_settings().device_id:
|
||||
|
||||
@@ -2,7 +2,7 @@ extends Control
|
||||
|
||||
const TEACHER_PASSWORD: String = "42"
|
||||
const BACK_SCENE_PATH: String = "res://sources/menus/main/main_menu.tscn"
|
||||
const NEXT_SCENE: PackedScene = preload("res://sources/gardens/gardens.tscn")
|
||||
const NEXT_SCENE_PATH: String = "res://sources/gardens/gardens.tscn"
|
||||
const TEACHER_SCENE_PATH: String = "res://sources/menus/settings/teacher_settings.tscn"
|
||||
const DEVELOPER_SCENE_PATH: String = "res://sources/menus/settings/developer_settings.tscn"
|
||||
const PACKAGE_LOADER_SCENE_PATH: String = "res://sources/menus/language_selection/package_downloader.tscn"
|
||||
@@ -73,8 +73,7 @@ func _on_code_keyboard_password_entered(password: String) -> void:
|
||||
await kalulu.play_kalulu_speech(right_password_speech)
|
||||
await OpeningCurtain.close()
|
||||
Log.trace("LoginScreen: Start loading next scene")
|
||||
get_tree().change_scene_to_packed(NEXT_SCENE)
|
||||
Log.trace("LoginScreen: End loading next scene")
|
||||
SceneLoader.change_scene(NEXT_SCENE_PATH)
|
||||
else:
|
||||
Log.warn("LoginScreen: Unknown student code entered (length=%d)" % password.length())
|
||||
kalulu_button.hide()
|
||||
|
||||
@@ -22,6 +22,7 @@ var dev_last_click_time: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
UserDataManager.ensure_joy_local_profile()
|
||||
Log.info("MainMenu: Loaded successfuly")
|
||||
version_label.text = Utils.get_application_version_with_code()
|
||||
teacher_label.text = UserDataManager.get_device_settings().teacher
|
||||
@@ -33,6 +34,9 @@ func _ready() -> void:
|
||||
func _on_main_button_pressed() -> void:
|
||||
Log.info("MainMenu: Main button pressed")
|
||||
play_button.set_disabled(true)
|
||||
if (ServerManager as ServerManagerClass).is_local_web_mode():
|
||||
_on_login_in()
|
||||
return
|
||||
if UserDataManager.get_device_settings().teacher:
|
||||
Log.info("MainMenu: Device settings has teacher info, so start auto login-in")
|
||||
_on_login_in()
|
||||
|
||||
@@ -435,7 +435,7 @@ func _go_back_to_the_garden() -> void:
|
||||
_save_logs()
|
||||
|
||||
Gardens.transition_data = gardens_data
|
||||
get_tree().change_scene_to_file("res://sources/gardens/gardens.tscn")
|
||||
SceneLoader.change_scene("res://sources/gardens/gardens.tscn")
|
||||
|
||||
|
||||
func _play_stimulus() -> void:
|
||||
|
||||
@@ -5,8 +5,7 @@ signal lilypad_in_center(lilypad: Lilypad)
|
||||
|
||||
const LILYPAD_SCENE_PATH: String = "res://sources/minigames/frog/lilypad.tscn"
|
||||
|
||||
static var _lilypad_scene: PackedScene
|
||||
|
||||
var _lilypad_scene: PackedScene
|
||||
var top_to_bottom: bool = false
|
||||
var is_stopped: bool = false
|
||||
var is_cleared: bool = false
|
||||
|
||||
@@ -38,14 +38,20 @@ var words_path: String = get_language_folder() + "/words/"
|
||||
var additional_word_list: Dictionary = {}
|
||||
var is_open: bool = false
|
||||
|
||||
@onready var db: SQLite = SQLite.new()
|
||||
@onready var db: Object = (
|
||||
ClassDB.instantiate(&"SQLite") if ClassDB.class_exists(&"SQLite") else null
|
||||
)
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
db.close_db()
|
||||
if db:
|
||||
db.close_db()
|
||||
|
||||
|
||||
func connect_to_db() -> void:
|
||||
if not db:
|
||||
Log.error("Database: SQLite extension is not available")
|
||||
return
|
||||
if is_open:
|
||||
Log.trace("Database: Opening database while a connection is already opened. Closing the previous one.")
|
||||
db.close_db()
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
extends Node
|
||||
## Scene changer that keeps peak memory low on low-end devices.
|
||||
##
|
||||
## [method SceneTree.change_scene_to_file] loads the new scene while the old
|
||||
## one is still fully resident, so transitions between heavy scenes (gardens,
|
||||
## minigames) momentarily need the memory of both — enough to OOM-crash old
|
||||
## tablets. This helper frees the current scene first, then streams the new
|
||||
## scene from disk on a background thread so the main thread keeps rendering
|
||||
## frames instead of freezing.
|
||||
##
|
||||
## Callers are expected to have covered the screen (OpeningCurtain closed)
|
||||
## before calling [method change_scene]; the curtain stays visible while the
|
||||
## tree has no current scene.
|
||||
|
||||
## Scene shown if the requested scene fails to load, so the player is never
|
||||
## stuck on a black screen.
|
||||
const FALLBACK_SCENE_PATH: String = "res://sources/menus/main/main_menu.tscn"
|
||||
|
||||
var _is_changing: bool = false
|
||||
|
||||
|
||||
func change_scene(scene_path: String) -> void:
|
||||
if _is_changing:
|
||||
Log.warn("SceneLoader: Change to %s requested while another change is in progress; ignoring" % scene_path)
|
||||
return
|
||||
if not ResourceLoader.exists(scene_path):
|
||||
Log.error("SceneLoader: Scene does not exist: %s" % scene_path)
|
||||
return
|
||||
_is_changing = true
|
||||
# Defer so the calling scene is never freed while one of its methods is
|
||||
# still on the stack.
|
||||
_change_scene_deferred.call_deferred(scene_path)
|
||||
|
||||
|
||||
func _change_scene_deferred(scene_path: String) -> void:
|
||||
var tree: SceneTree = get_tree()
|
||||
# Free the old scene before loading the new one: this roughly halves the
|
||||
# memory peak of the transition.
|
||||
if tree.current_scene:
|
||||
tree.unload_current_scene()
|
||||
var error: Error = ResourceLoader.load_threaded_request(scene_path)
|
||||
if error != OK:
|
||||
Log.error("SceneLoader: Could not start loading %s (error %s)" % [scene_path, error_string(error)])
|
||||
_finish_with_fallback(tree, scene_path)
|
||||
return
|
||||
var status: ResourceLoader.ThreadLoadStatus = ResourceLoader.load_threaded_get_status(scene_path)
|
||||
while status == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
|
||||
await tree.process_frame
|
||||
status = ResourceLoader.load_threaded_get_status(scene_path)
|
||||
if status != ResourceLoader.THREAD_LOAD_LOADED:
|
||||
Log.error("SceneLoader: Failed to load %s (status %d)" % [scene_path, status])
|
||||
_finish_with_fallback(tree, scene_path)
|
||||
return
|
||||
var packed_scene: PackedScene = ResourceLoader.load_threaded_get(scene_path) as PackedScene
|
||||
if not packed_scene:
|
||||
Log.error("SceneLoader: %s is not a PackedScene" % scene_path)
|
||||
_finish_with_fallback(tree, scene_path)
|
||||
return
|
||||
error = tree.change_scene_to_packed(packed_scene)
|
||||
if error != OK:
|
||||
Log.error("SceneLoader: Could not change to %s (error %s)" % [scene_path, error_string(error)])
|
||||
_finish_with_fallback(tree, scene_path)
|
||||
return
|
||||
_is_changing = false
|
||||
|
||||
|
||||
func _finish_with_fallback(tree: SceneTree, failed_scene_path: String) -> void:
|
||||
_is_changing = false
|
||||
if failed_scene_path != FALLBACK_SCENE_PATH:
|
||||
tree.change_scene_to_file(FALLBACK_SCENE_PATH)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bm3xnmft2e4ul
|
||||
@@ -25,6 +25,10 @@ var environment_setting: int = 1
|
||||
@onready var loading_rect: TextureRect = $TextureRect
|
||||
|
||||
|
||||
func is_local_web_mode() -> bool:
|
||||
return OS.has_feature("web") and bool(ProjectSettings.get_setting("application/custom/local_web_mode", false))
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var config: ConfigFile = ConfigFile.new()
|
||||
var load_error: Error = config.load(CONFIG_PATH)
|
||||
@@ -84,6 +88,8 @@ func _save_environment_config() -> void:
|
||||
|
||||
|
||||
func first_login_student() -> void:
|
||||
if is_local_web_mode():
|
||||
return
|
||||
await _post_json_request("submit_student_session", {"student_id": UserDataManager.student})
|
||||
|
||||
|
||||
@@ -185,6 +191,8 @@ func reset_language(language: String) -> Dictionary:
|
||||
#region Sender functions
|
||||
|
||||
func check_internet_access() -> bool:
|
||||
if is_local_web_mode():
|
||||
return false
|
||||
Log.trace("ServerManager: Sending simple request to " + INTERNET_CHECK_URL + " to check if internet is available")
|
||||
var res: Error = internet_check.request(INTERNET_CHECK_URL)
|
||||
if res == OK:
|
||||
|
||||
@@ -107,6 +107,8 @@ func _process(_delta: float) -> void:
|
||||
#region synchronization
|
||||
|
||||
func start_synchronization_timer() -> void:
|
||||
if (ServerManager as ServerManagerClass).is_local_web_mode():
|
||||
return
|
||||
if not synchronization_timer_running:
|
||||
synchronization_timer = 0
|
||||
synchronization_timer_running = true
|
||||
@@ -117,6 +119,52 @@ func stop_synchronization_timer() -> void:
|
||||
synchronization_timer_running = false
|
||||
Log.trace("UserDataManager: Synchronization timer stopped")
|
||||
|
||||
|
||||
func ensure_joy_local_profile() -> void:
|
||||
if not (ServerManager as ServerManagerClass).is_local_web_mode():
|
||||
return
|
||||
|
||||
var settings: DeviceSettings = get_device_settings()
|
||||
settings.language = "fr_FR"
|
||||
settings.teacher = "profil-local"
|
||||
settings.device_id = 1
|
||||
_save_device_settings()
|
||||
|
||||
var teacher_path: String = get_teacher_settings_path()
|
||||
if FileAccess.file_exists(teacher_path):
|
||||
_load_teacher_settings()
|
||||
if not teacher_settings:
|
||||
teacher_settings = TeacherSettings.new()
|
||||
teacher_settings.account_type = TeacherSettings.AccountType.PARENT
|
||||
teacher_settings.education_method = TeacherSettings.EducationMethod.APP_ONLY
|
||||
teacher_settings.email = "profil-local"
|
||||
teacher_settings.token = "local-only"
|
||||
teacher_settings.language = "fr_FR"
|
||||
teacher_settings.server_language_validated = true
|
||||
var joy: StudentData = StudentData.new()
|
||||
joy.code = 123
|
||||
joy.name = "Joy"
|
||||
joy.age = 5
|
||||
joy.level = StudentData.Level.BEGINNER
|
||||
var joy_students: Array[StudentData] = [joy]
|
||||
teacher_settings.students[1] = joy_students
|
||||
DirAccess.make_dir_recursive_absolute(get_teacher_folder())
|
||||
save_teacher_settings()
|
||||
else:
|
||||
teacher_settings.language = "fr_FR"
|
||||
teacher_settings.server_language_validated = true
|
||||
if not teacher_settings.students.has(1) or (teacher_settings.students[1] as Array).is_empty():
|
||||
var joy: StudentData = StudentData.new()
|
||||
joy.code = 123
|
||||
joy.name = "Joy"
|
||||
joy.age = 5
|
||||
joy.level = StudentData.Level.BEGINNER
|
||||
var joy_students: Array[StudentData] = [joy]
|
||||
teacher_settings.students[1] = joy_students
|
||||
save_teacher_settings()
|
||||
|
||||
TranslationServer.set_locale("fr_FR")
|
||||
|
||||
#endregion
|
||||
|
||||
#region Registering and logging in
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://34wj4ytlkuvn
|
||||
Reference in New Issue
Block a user