├── fox ├── core │ ├── app.gd.uid │ ├── debug.gd.uid │ ├── files.gd.uid │ ├── globals.gd.uid │ ├── router.gd.uid │ ├── sound.gd.uid │ ├── files.gd │ ├── debug.gd │ ├── globals.gd │ ├── app.gd │ ├── sound.gd │ └── router.gd ├── libs │ ├── bundle.gd.uid │ ├── generate.gd.uid │ ├── gesture.gd.uid │ ├── http.gd.uid │ ├── wait.gd.uid │ ├── time-tools.gd.uid │ ├── underscore.gd.uid │ ├── generate.gd │ ├── wait.gd │ ├── bundle.gd │ ├── http.gd │ ├── gesture.gd │ ├── time-tools.gd │ └── underscore.gd ├── animations │ ├── animate.gd.uid │ ├── framer.gd.uid │ ├── intro-animation.gd.uid │ ├── splash-animation.gd.uid │ ├── framer.tscn │ ├── intro-animation.gd │ ├── framer.gd │ ├── splash-animation.gd │ └── intro-animation.tscn ├── behaviours │ ├── rotation.gd.uid │ ├── dropArea2D.gd.uid │ ├── pencil.gdshader.uid │ ├── draggable-camera.gd.uid │ ├── interactiveArea2D.gd.uid │ ├── multitouchArea.gd.uid │ ├── radial-blur.gdshader.uid │ ├── simple-blur.gdshader.uid │ ├── spinning-sphere.gdshader.uid │ ├── pixelate.tres │ ├── rotation.gd │ ├── dropArea2D.tscn │ ├── simple-blur.gdshader │ ├── interactiveArea2D.tscn │ ├── notifications.tscn │ ├── saturation-material.tres │ ├── multitouchArea.tscn │ ├── spinning-sphere.gdshader │ ├── dropArea2D.gd │ ├── outline.tres │ ├── radial-blur.gdshader │ ├── disintegrate-material.tres │ ├── pencil.gdshader │ ├── multitouchArea.gd │ ├── interactiveArea2D.gd │ └── draggable-camera.gd ├── components │ ├── popup.gd.uid │ ├── blur.gdshader.uid │ ├── screen-fader.gd.uid │ ├── fullscreen-loader.gd.uid │ ├── gradient.gdshader.uid │ ├── review │ │ ├── ask-for-review.gd.uid │ │ └── ask-for-review.gd │ ├── screen-fader.gd │ ├── screen-fader.tscn │ ├── gradient.gdshader │ ├── fullscreen-loader.gd │ ├── fullscreen-loader.tscn │ ├── popup.gd │ ├── pool.tscn │ ├── blur.gdshader │ └── blur.tscn ├── stores │ ├── appstore.gd.uid │ ├── playstore.gd.uid │ └── appstore.gd ├── assets │ ├── splash │ │ ├── a.png │ │ ├── l.png │ │ ├── r.png │ │ ├── s.png │ │ ├── u.png │ │ ├── y.png │ │ ├── dot.png │ │ ├── boot-empty.png │ │ └── uralys-banner.webp │ └── gui │ │ └── loader.webp ├── env │ └── default_env.tres └── default.config.json ├── assets ├── logo.jpg ├── docs │ ├── cli-export.png │ ├── games │ │ ├── xoozz.webp │ │ ├── battle-squares.webp │ │ ├── avindi-desktop-512x512.png │ │ ├── lockey0-desktop-512x512.png │ │ ├── lockey1-desktop-512x512.png │ │ └── battle-squares-desktop-512x512.png │ ├── router-tree.png │ ├── draggable-boundaries.png │ ├── router-screen-layout.png │ └── draggable-attach-script.png ├── sprites │ ├── splash │ │ ├── a.png │ │ ├── l.png │ │ ├── r.png │ │ ├── s.png │ │ ├── u.png │ │ ├── y.png │ │ ├── dot.png │ │ └── boot-empty.png │ └── uralys-banner.webp └── android │ └── adaptive_icon_template.afdesign ├── .vscode └── settings.json ├── docs ├── tips │ ├── branding.md │ └── gamedev.md ├── exporting │ ├── build.md │ ├── ios.md │ ├── export.md │ ├── images.md │ └── android.md ├── gdscript │ ├── draggable-camera.md │ ├── sound.md │ ├── router.md │ ├── animations.md │ ├── popups.md │ └── interactive-area-2d.md ├── cli.md └── install.md ├── cli ├── update-translations.js ├── bundler │ ├── read-presets.js │ ├── versioning.js │ ├── switch.js │ ├── update-preset.js │ ├── export.js │ └── ini.js ├── generate-splashscreens.js ├── generate-screenshots.js ├── generate-icons.js ├── run-game.js └── cli.js ├── .gitignore ├── package.json ├── .github └── FUNDING.yml ├── license └── readme.md /fox/core/app.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bd80g23tttlc3 2 | -------------------------------------------------------------------------------- /fox/core/debug.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dq4rh5ydgyxo7 2 | -------------------------------------------------------------------------------- /fox/core/files.gd.uid: -------------------------------------------------------------------------------- 1 | uid://2ig1eejao1lc 2 | -------------------------------------------------------------------------------- /fox/core/globals.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dn483m3lxfnay 2 | -------------------------------------------------------------------------------- /fox/core/router.gd.uid: -------------------------------------------------------------------------------- 1 | uid://buv4524i1wc7x 2 | -------------------------------------------------------------------------------- /fox/core/sound.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dl3grphdqxdn4 2 | -------------------------------------------------------------------------------- /fox/libs/bundle.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cwkn2eesfowo5 2 | -------------------------------------------------------------------------------- /fox/libs/generate.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c60fauinciw8i 2 | -------------------------------------------------------------------------------- /fox/libs/gesture.gd.uid: -------------------------------------------------------------------------------- 1 | uid://sno07ysv6tja 2 | -------------------------------------------------------------------------------- /fox/libs/http.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bcipm8uyf4if5 2 | -------------------------------------------------------------------------------- /fox/libs/wait.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dbrg3mejlbxg3 2 | -------------------------------------------------------------------------------- /fox/animations/animate.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c2nlwolfd62sl 2 | -------------------------------------------------------------------------------- /fox/animations/framer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://intgbyls1skk 2 | -------------------------------------------------------------------------------- /fox/behaviours/rotation.gd.uid: -------------------------------------------------------------------------------- 1 | uid://lf6jlvs8yo1t 2 | -------------------------------------------------------------------------------- /fox/components/popup.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dcngkol06rpax 2 | -------------------------------------------------------------------------------- /fox/libs/time-tools.gd.uid: -------------------------------------------------------------------------------- 1 | uid://41qxel35mq02 2 | -------------------------------------------------------------------------------- /fox/libs/underscore.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c3bdbsmvurb3b 2 | -------------------------------------------------------------------------------- /fox/stores/appstore.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cu7rk5b6hyo7w 2 | -------------------------------------------------------------------------------- /fox/stores/playstore.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cxu3kyr61dopj 2 | -------------------------------------------------------------------------------- /fox/behaviours/dropArea2D.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dvym6oee54kvq 2 | -------------------------------------------------------------------------------- /fox/behaviours/pencil.gdshader.uid: -------------------------------------------------------------------------------- 1 | uid://bo4pj1rmcfrsd 2 | -------------------------------------------------------------------------------- /fox/components/blur.gdshader.uid: -------------------------------------------------------------------------------- 1 | uid://cn1h4le6cm6fe 2 | -------------------------------------------------------------------------------- /fox/components/screen-fader.gd.uid: -------------------------------------------------------------------------------- 1 | uid://be8qq1r8fy4bx 2 | -------------------------------------------------------------------------------- /fox/animations/intro-animation.gd.uid: -------------------------------------------------------------------------------- 1 | uid://djnatkhpe7v5s 2 | -------------------------------------------------------------------------------- /fox/animations/splash-animation.gd.uid: -------------------------------------------------------------------------------- 1 | uid://4v862tos0pey 2 | -------------------------------------------------------------------------------- /fox/behaviours/draggable-camera.gd.uid: -------------------------------------------------------------------------------- 1 | uid://0cb3buobd5v4 2 | -------------------------------------------------------------------------------- /fox/behaviours/interactiveArea2D.gd.uid: -------------------------------------------------------------------------------- 1 | uid://y2o8k24r2sj0 2 | -------------------------------------------------------------------------------- /fox/behaviours/multitouchArea.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bood0egphfhms 2 | -------------------------------------------------------------------------------- /fox/behaviours/radial-blur.gdshader.uid: -------------------------------------------------------------------------------- 1 | uid://csf3bt4iuycuh 2 | -------------------------------------------------------------------------------- /fox/behaviours/simple-blur.gdshader.uid: -------------------------------------------------------------------------------- 1 | uid://deqbvp0mjxxwq 2 | -------------------------------------------------------------------------------- /fox/components/fullscreen-loader.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d2aok60hmh3gv 2 | -------------------------------------------------------------------------------- /fox/components/gradient.gdshader.uid: -------------------------------------------------------------------------------- 1 | uid://q8tp3tibra2l 2 | -------------------------------------------------------------------------------- /fox/behaviours/spinning-sphere.gdshader.uid: -------------------------------------------------------------------------------- 1 | uid://lmkwxg663gcw 2 | -------------------------------------------------------------------------------- /fox/components/review/ask-for-review.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bsxy6s4tdxebg 2 | -------------------------------------------------------------------------------- /assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/logo.jpg -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/*.uid": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /fox/assets/splash/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/fox/assets/splash/a.png -------------------------------------------------------------------------------- /fox/assets/splash/l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/fox/assets/splash/l.png -------------------------------------------------------------------------------- /fox/assets/splash/r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/fox/assets/splash/r.png -------------------------------------------------------------------------------- /fox/assets/splash/s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/fox/assets/splash/s.png -------------------------------------------------------------------------------- /fox/assets/splash/u.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/fox/assets/splash/u.png -------------------------------------------------------------------------------- /fox/assets/splash/y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/fox/assets/splash/y.png -------------------------------------------------------------------------------- /assets/docs/cli-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/docs/cli-export.png -------------------------------------------------------------------------------- /fox/assets/gui/loader.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/fox/assets/gui/loader.webp -------------------------------------------------------------------------------- /fox/assets/splash/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/fox/assets/splash/dot.png -------------------------------------------------------------------------------- /assets/docs/games/xoozz.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/docs/games/xoozz.webp -------------------------------------------------------------------------------- /assets/docs/router-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/docs/router-tree.png -------------------------------------------------------------------------------- /assets/sprites/splash/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/sprites/splash/a.png -------------------------------------------------------------------------------- /assets/sprites/splash/l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/sprites/splash/l.png -------------------------------------------------------------------------------- /assets/sprites/splash/r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/sprites/splash/r.png -------------------------------------------------------------------------------- /assets/sprites/splash/s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/sprites/splash/s.png -------------------------------------------------------------------------------- /assets/sprites/splash/u.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/sprites/splash/u.png -------------------------------------------------------------------------------- /assets/sprites/splash/y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/sprites/splash/y.png -------------------------------------------------------------------------------- /assets/sprites/splash/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/sprites/splash/dot.png -------------------------------------------------------------------------------- /docs/tips/branding.md: -------------------------------------------------------------------------------- 1 | # branding 2 | 3 | ## screenshots 4 | 5 | use 6 | -------------------------------------------------------------------------------- /assets/sprites/uralys-banner.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/sprites/uralys-banner.webp -------------------------------------------------------------------------------- /cli/update-translations.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | -------------------------------------------------------------------------------- /fox/assets/splash/boot-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/fox/assets/splash/boot-empty.png -------------------------------------------------------------------------------- /assets/docs/draggable-boundaries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/docs/draggable-boundaries.png -------------------------------------------------------------------------------- /assets/docs/router-screen-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/docs/router-screen-layout.png -------------------------------------------------------------------------------- /assets/sprites/splash/boot-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/sprites/splash/boot-empty.png -------------------------------------------------------------------------------- /fox/assets/splash/uralys-banner.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/fox/assets/splash/uralys-banner.webp -------------------------------------------------------------------------------- /assets/docs/games/battle-squares.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/docs/games/battle-squares.webp -------------------------------------------------------------------------------- /fox/behaviours/pixelate.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="ShaderMaterial" format=3 uid="uid://cbtv54qwdffqf"] 2 | 3 | [resource] 4 | -------------------------------------------------------------------------------- /assets/docs/draggable-attach-script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/docs/draggable-attach-script.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # godot 2 | *.import 3 | export_presets.cfg 4 | 5 | # npm 6 | node_modules 7 | 8 | # osx 9 | *.DS_Store 10 | -------------------------------------------------------------------------------- /assets/android/adaptive_icon_template.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/android/adaptive_icon_template.afdesign -------------------------------------------------------------------------------- /assets/docs/games/avindi-desktop-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/docs/games/avindi-desktop-512x512.png -------------------------------------------------------------------------------- /assets/docs/games/lockey0-desktop-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/docs/games/lockey0-desktop-512x512.png -------------------------------------------------------------------------------- /assets/docs/games/lockey1-desktop-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/docs/games/lockey1-desktop-512x512.png -------------------------------------------------------------------------------- /fox/behaviours/rotation.gd: -------------------------------------------------------------------------------- 1 | extends TextureRect 2 | 3 | @export var speed = 10 4 | 5 | func _process(delta): 6 | rotation += delta * speed 7 | -------------------------------------------------------------------------------- /assets/docs/games/battle-squares-desktop-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uralys/fox/HEAD/assets/docs/games/battle-squares-desktop-512x512.png -------------------------------------------------------------------------------- /fox/env/default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=3 uid="uid://coymmi0nlykrg"] 2 | 3 | [sub_resource type="Sky" id="1"] 4 | 5 | [resource] 6 | background_mode = 2 7 | sky = SubResource("1") 8 | -------------------------------------------------------------------------------- /fox/animations/framer.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://cmt1xncsfu3xe"] 2 | 3 | [ext_resource type="Script" uid="uid://intgbyls1skk" path="res://fox/animations/framer.gd" id="1"] 4 | 5 | [node name="framer" type="Tween"] 6 | script = ExtResource("1") 7 | -------------------------------------------------------------------------------- /docs/exporting/build.md: -------------------------------------------------------------------------------- 1 | # building 2 | 3 | ## \_build folder 4 | 5 | - create a `/_build` 6 | - add `_build` to `.gitignore` 7 | - add a `_build/.gdignore` 8 | 9 | ```sh 10 | _build 11 | ├── .gdignore 12 | ├── android 13 | ├── ios 14 | ├── osx 15 | └── ... 16 | ``` 17 | -------------------------------------------------------------------------------- /fox/behaviours/dropArea2D.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://bqbex8ooxkxnm"] 2 | 3 | [ext_resource type="Script" uid="uid://dvym6oee54kvq" path="res://fox/behaviours/dropArea2D.gd" id="1_wu3mr"] 4 | 5 | [node name="dropArea" type="Area2D"] 6 | script = ExtResource("1_wu3mr") 7 | -------------------------------------------------------------------------------- /fox/behaviours/simple-blur.gdshader: -------------------------------------------------------------------------------- 1 | shader_type canvas_item; 2 | 3 | uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap; 4 | uniform float lod: hint_range(0.0, 5) = 0.0; 5 | 6 | void fragment(){ 7 | vec4 color = texture(SCREEN_TEXTURE, SCREEN_UV, lod); 8 | COLOR = color; 9 | } -------------------------------------------------------------------------------- /fox/behaviours/interactiveArea2D.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://tioclo0qghng"] 2 | 3 | [ext_resource type="Script" uid="uid://y2o8k24r2sj0" path="res://fox/behaviours/interactiveArea2D.gd" id="1_m542d"] 4 | 5 | [node name="interactiveArea2D" type="Area2D"] 6 | script = ExtResource("1_m542d") 7 | -------------------------------------------------------------------------------- /fox/behaviours/notifications.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://pwvxfbwhrtgn"] 2 | 3 | [ext_resource type="Script" uid="uid://dm8dw5s2oqnvi" path="res://addons/GodotAndroidNotificationSchedulerPlugin/NotificationScheduler.gd" id="1_aqm12"] 4 | 5 | [node name="Notifications" type="Node"] 6 | script = ExtResource("1_aqm12") 7 | -------------------------------------------------------------------------------- /fox/components/screen-fader.gd: -------------------------------------------------------------------------------- 1 | extends CanvasLayer 2 | 3 | @export var duration: float = 1 4 | 5 | func _ready(): 6 | $rect.visible = true 7 | $rect.size = get_viewport().get_visible_rect().size 8 | 9 | Animate.to($rect, { 10 | propertyPath = "modulate:a", 11 | toValue = 0, 12 | duration = duration 13 | }) 14 | -------------------------------------------------------------------------------- /fox/core/files.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # ------------------------------------------------------------------------------ 4 | 5 | func getBundles(): 6 | var file = FileAccess.open("res://fox.config.json", FileAccess.READ) 7 | var fileContent = file.get_as_text() 8 | file.close() 9 | 10 | var configJSON = JSON.parse_string(fileContent) 11 | var bundles = configJSON.bundles 12 | return bundles 13 | -------------------------------------------------------------------------------- /fox/components/screen-fader.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://dl2vjdb3s0ara"] 2 | 3 | [ext_resource type="Script" uid="uid://be8qq1r8fy4bx" path="res://fox/components/screen-fader.gd" id="1"] 4 | 5 | [node name="screenFader" type="CanvasLayer"] 6 | layer = 127 7 | script = ExtResource("1") 8 | 9 | [node name="rect" type="ColorRect" parent="."] 10 | visible = false 11 | offset_right = 164.0 12 | offset_bottom = 128.0 13 | mouse_filter = 2 14 | color = Color(0, 0, 0, 1) 15 | -------------------------------------------------------------------------------- /fox/behaviours/saturation-material.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://dhjyvdck6wxl8"] 2 | 3 | [sub_resource type="Shader" id="1"] 4 | code = "shader_type canvas_item; 5 | 6 | uniform float saturation; 7 | 8 | void fragment() { 9 | vec4 tex_color = texture(TEXTURE, UV); 10 | 11 | COLOR.rgb = mix(vec3(dot(tex_color.rgb, vec3(0.299, 0.587, 0.114))), tex_color.rgb, saturation); 12 | COLOR.a = tex_color.a; 13 | }" 14 | 15 | [resource] 16 | shader = SubResource("1") 17 | shader_parameter/saturation = 0.2 18 | -------------------------------------------------------------------------------- /fox/behaviours/multitouchArea.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://deby4crwkq0he"] 2 | 3 | [ext_resource type="Script" uid="uid://bood0egphfhms" path="res://fox/behaviours/multitouchArea.gd" id="1_pvvd6"] 4 | 5 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_wk3sr"] 6 | size = Vector2(282, 306) 7 | 8 | [node name="multitouchArea" type="Area2D"] 9 | script = ExtResource("1_pvvd6") 10 | 11 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 12 | shape = SubResource("RectangleShape2D_wk3sr") 13 | debug_color = Color(0.108357, 0.509663, 0.158947, 0.721569) 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uralys/fox", 3 | "version": "1.13.0", 4 | "type": "module", 5 | "description": "🦊 Tools and Components for Godot 4", 6 | "author": "chrisdugne", 7 | "license": "MIT", 8 | "homepage": "https://github.com/uralys/fox#readme", 9 | "devDependencies": { 10 | "chalk": "^5.4.1", 11 | "chokidar": "^4.0.3", 12 | "inquirer": "^12.6.0", 13 | "keypress": "^0.2.1", 14 | "npm-check-updates": "^18.0.1", 15 | "shelljs": "^0.9.2", 16 | "yargs": "^17.7.2" 17 | }, 18 | "scripts": { 19 | "ncu": "npm-check-updates" 20 | }, 21 | "engines": { 22 | "node": ">=22.x" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [chrisdugne] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: uralys 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /fox/behaviours/spinning-sphere.gdshader: -------------------------------------------------------------------------------- 1 | shader_type canvas_item; 2 | 3 | uniform float aspect_ratio = 2.0; 4 | uniform float rotation_speed = 0.3; 5 | uniform bool as_shadow = false; 6 | 7 | void fragment() { 8 | float px = 2.0 * (UV.x - 0.5); 9 | float py = 2.0 * (UV.y - 0.5); 10 | 11 | if (px * px + py * py > 1.0) { 12 | // Outside of "sphere" 13 | COLOR.a = 0.0; 14 | } else { 15 | px = asin(px / sqrt(1.0 - py * py)) * 2.0 / PI; 16 | py = asin(py) * 2.0 / PI; 17 | 18 | COLOR = texture(TEXTURE, vec2( 19 | 0.5 * (px + 1.0) / aspect_ratio - TIME * rotation_speed, 20 | 0.5 * (py + 1.0))); 21 | if (as_shadow) { 22 | COLOR.rgb = vec3(0.0, 0.0, 0.0); 23 | COLOR.a *= 0.9; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /fox/core/debug.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | 3 | extends Node 4 | 5 | # ------------------------------------------------------------------------------ 6 | 7 | func setup(): 8 | var _options = self.options if self.options else {} 9 | 10 | if(G.ENV == G.RELEASE): 11 | for option in _options: 12 | self[option] = false 13 | 14 | G.log('✅ debug options deactivated in production'); 15 | return 16 | 17 | var debugOptionsEnabled = false 18 | 19 | for option in _options: 20 | if(self[option]): 21 | G.log(' >', option, '[color=pink]is activated [/color]') 22 | debugOptionsEnabled = true 23 | 24 | if(not debugOptionsEnabled): 25 | G.log('👾 ✅ options deactivated, same as production'); 26 | -------------------------------------------------------------------------------- /docs/gdscript/draggable-camera.md: -------------------------------------------------------------------------------- 1 | # Draggable Camera 2 | 3 | To enable dragging a Camera2D, you can attach the [draggable-camera](../../fox/behaviours/draggable-camera.gd) script to any Camera2D Node. 4 | 5 | ## Setup 6 | 7 | Create a `Camera2D` node, and look for the [draggable-camera](../../fox/behaviours/draggable-camera.gd) script to it from the Inspector. 8 | 9 | 10 | 11 | Now, if you add a `TextureRect` as a sibling of the Camera2D, you'll be able to drag it to move the camera. 12 | 13 | ## Boundaries 14 | 15 | (Experimental) Adding a sibling `ReferenceRect` called `boundaries` allows boundaries to be set for the camera. 16 | 17 | 18 | -------------------------------------------------------------------------------- /fox/behaviours/dropArea2D.gd: -------------------------------------------------------------------------------- 1 | extends Area2D 2 | 3 | # ------------------------------------------------------------------------------ 4 | 5 | @export var acceptedType = 'default' 6 | 7 | # ------------------------------------------------------------------------------ 8 | 9 | signal dropActived 10 | signal dropDeactived 11 | signal received # triggered from interactiveArea 12 | 13 | # ------------------------------------------------------------------------------ 14 | 15 | func _ready(): 16 | connect("mouse_entered", mouse_entered) 17 | connect("mouse_exited", mouse_exited) 18 | 19 | # ------------------------------------------------------------------------------ 20 | 21 | func mouse_entered(): 22 | Gesture.verifyDroppableOnEnter(self, acceptedType) 23 | 24 | # ------------------------------------------------------------------------------ 25 | 26 | func mouse_exited(): 27 | Gesture.verifyDroppableOnExit(self, acceptedType) 28 | -------------------------------------------------------------------------------- /docs/gdscript/sound.md: -------------------------------------------------------------------------------- 1 | # Sound 2 | 3 | This core feature adds an `AudioStreamPlayer` 4 | 5 | ## setup 6 | 7 | create a `src/core/sound.gd` 8 | 9 | ```gdscript 10 | extends 'res://fox/core/sound.gd' 11 | ``` 12 | 13 | and add it as `Autoload` 14 | 15 | then you can implement the `play` like this: 16 | 17 | ```gdscript 18 | var OGG = { 19 | onButtonPress = "res://path/to/your-sound.ogg", 20 | music = "res://path/to/your-music.ogg", 21 | } 22 | 23 | func play(soundName): 24 | var assetPath =__.Get(soundName, OGG) 25 | if(assetPath): 26 | .play(assetPath) 27 | ``` 28 | 29 | ## usage 30 | 31 | Now you can call `Sound.play('music')` anywhere 32 | 33 | ## note on loop behaviour 34 | 35 | `.ogg` files will loop by default. 36 | 37 | - Select them on the `FileSystem`, 38 | - go to the `Import` tab next to the `Scene` tab 39 | - unselect `loop` 40 | - click on `Reimport` 41 | 42 | ## default sound list 43 | 44 | - `onButtonPress` 45 | -------------------------------------------------------------------------------- /fox/libs/generate.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | 3 | extends Node 4 | 5 | # ------------------------------------------------------------------------------ 6 | 7 | var ELEMENTS = load('res://assets/name-elements.json').data 8 | 9 | # ------------------------------------------------------------------------------ 10 | 11 | func uid(prefix): 12 | var _prefix = '' 13 | if (prefix): 14 | _prefix = prefix + '-' 15 | 16 | return _prefix + str(Time.get_unix_time_from_system()) + '-' + str(Time.get_ticks_msec()) + '-' + str(randi() % 900000 + 100000) 17 | 18 | # ------------------------------------------------------------------------------ 19 | 20 | func name(): 21 | if(!ELEMENTS): 22 | return 'Player' 23 | 24 | var _adjective = ELEMENTS.adjectives[randi() % ELEMENTS.adjectives.size()] 25 | var _name = ELEMENTS.names[randi() % ELEMENTS.names.size()] 26 | 27 | return _adjective + _name 28 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Uralys, Christophe Dugne-Esquevin (uralys.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /fox/behaviours/outline.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://djra12qjgima5"] 2 | 3 | [sub_resource type="Shader" id="Shader_fgayl"] 4 | code = "shader_type canvas_item; 5 | 6 | uniform vec4 line_color : source_color = vec4(1); 7 | uniform float line_thickness : hint_range(0, 30) = 1.0; 8 | 9 | void fragment() { 10 | vec2 size = TEXTURE_PIXEL_SIZE * line_thickness; 11 | 12 | float outline = texture(TEXTURE, UV + vec2(-size.x, 0)).a; 13 | outline += texture(TEXTURE, UV + vec2(0, size.y)).a; 14 | outline += texture(TEXTURE, UV + vec2(size.x, 0)).a; 15 | outline += texture(TEXTURE, UV + vec2(0, -size.y)).a; 16 | outline += texture(TEXTURE, UV + vec2(-size.x, size.y)).a; 17 | outline += texture(TEXTURE, UV + vec2(size.x, size.y)).a; 18 | outline += texture(TEXTURE, UV + vec2(-size.x, -size.y)).a; 19 | outline += texture(TEXTURE, UV + vec2(size.x, -size.y)).a; 20 | outline = min(outline, 1.0); 21 | 22 | vec4 color = texture(TEXTURE, UV); 23 | COLOR = mix(color, line_color, outline - color.a); 24 | }" 25 | 26 | [resource] 27 | shader = SubResource("Shader_fgayl") 28 | shader_parameter/line_color = Color(1, 1, 1, 0.85098) 29 | shader_parameter/line_thickness = 8.0 30 | -------------------------------------------------------------------------------- /fox/components/gradient.gdshader: -------------------------------------------------------------------------------- 1 | // added parameters to original shader: https://godotshaders.com/shader/ripple-gradient-shader-2/ 2 | shader_type canvas_item; 3 | 4 | uniform float speed: hint_range(0, 5, 0.1) = 1; 5 | uniform float colorSet: hint_range(0, 7) = 0.0; 6 | uniform float brightness: hint_range(0.1, 0.9, 0.01) = 0.5; 7 | uniform float amplitude: hint_range(0.1, 0.9, 0.01) = 0.5; 8 | uniform float frequency: hint_range(0, 125, 0.1) = 10.0; 9 | 10 | void fragment() { 11 | vec2 uv = SCREEN_UV; 12 | float currentColor = TIME * speed; 13 | 14 | if(colorSet > 0.0){ 15 | currentColor = colorSet; 16 | } 17 | 18 | float wave1 = sin(uv.x * frequency + currentColor) * amplitude; 19 | float wave2 = cos(uv.y * frequency + currentColor) * amplitude; 20 | uv += wave1 + wave2; 21 | 22 | vec3 color1 = vec3(brightness + (1.0 - brightness) * sin(currentColor), brightness + (1.0 - brightness) * cos(currentColor),brightness - (1.0 - brightness) * sin(currentColor)); 23 | vec3 color2 = vec3(brightness + (1.0 - brightness ) * cos(currentColor), brightness + (1.0 - brightness) * sin(currentColor), brightness + (1.0 - brightness) * cos(currentColor)); 24 | vec3 gradient_color = mix(color1, color2, uv.y); 25 | 26 | COLOR = vec4(gradient_color, 1.0); 27 | } -------------------------------------------------------------------------------- /cli/bundler/read-presets.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import ini from './ini.js'; 7 | 8 | // ----------------------------------------------------------------------------- 9 | 10 | export const PRESETS_CFG = 'export_presets.cfg'; 11 | 12 | // ----------------------------------------------------------------------------- 13 | 14 | const readPresets = () => { 15 | let presets; 16 | 17 | try { 18 | const presetsCFG = fs.readFileSync(path.resolve(PRESETS_CFG), 'utf8'); 19 | presets = ini.parse(presetsCFG).preset; 20 | } catch (e) { 21 | console.log(e); 22 | console.log(`\nCould not open ${path.resolve(PRESETS_CFG)}`); 23 | console.log(chalk.red.bold('🔴 failed: use Godot editor > Project > Export to define your export config.')); 24 | return; 25 | } 26 | 27 | return presets; 28 | } 29 | 30 | // ----------------------------------------------------------------------------- 31 | 32 | const writePresets = (presets) => { 33 | fs.writeFileSync(PRESETS_CFG, ini.stringify({preset: presets})); 34 | } 35 | 36 | // ----------------------------------------------------------------------------- 37 | 38 | export {readPresets, writePresets}; 39 | -------------------------------------------------------------------------------- /fox/libs/wait.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | 3 | extends Node 4 | 5 | # ------------------------------------------------------------------------------ 6 | 7 | class_name Wait 8 | 9 | # ------------------------------------------------------------------------------ 10 | 11 | static func forSomeTime(parent, timeInSec: float = 0): 12 | if(timeInSec <= 0): 13 | return {timeout = true} 14 | 15 | var _Timer = Timer.new() 16 | parent.add_child(_Timer) 17 | _Timer.start(timeInSec); 18 | return _Timer 19 | 20 | # ------------------------------------------------------------------------------ 21 | 22 | # we need the object to have a `params` Dictionary to store the timer 23 | static func withTimer(timetoWait: float, object: Variant, onTimeout: Callable): 24 | var params = __.Get('params', object) 25 | 26 | if(!params): 27 | assert(false, 'withTimeout requires the object to have a `params` Dictionary') 28 | return 29 | 30 | var timer = __.Get('timer', params) 31 | 32 | if(timer): 33 | timer.stop() 34 | else: 35 | timer = Timer.new() 36 | object.add_child(timer) 37 | object.params.timer = timer 38 | 39 | timer.timeout.connect(func(): 40 | timer.stop() 41 | onTimeout.call() 42 | ) 43 | 44 | timer.start(timetoWait) 45 | -------------------------------------------------------------------------------- /docs/exporting/ios.md: -------------------------------------------------------------------------------- 1 | # iOS 2 | 3 | ## XCode 4 | 5 | get latest version from 6 | 7 | ## build the xcode project 8 | 9 | ```sh 10 | fox export 11 | ``` 12 | 13 | - Select your iOS preset 14 | - the `.ipa` will be created, you'll be able to use it with XCode 15 | 16 | ## added auto signing option 17 | 18 | ```sh 19 | CODE_SIGNING_ALLOWED: No 20 | ``` 21 | 22 | from 23 | 24 | ## create the archive 25 | 26 | @todo migrate screenshots here from 27 | 28 | ## upload to appstore 29 | 30 | 31 | 32 | ## IAP 33 | 34 | - install godot appstore plugin from or within `ios/plugins` 35 | 36 | ```sh 37 | ios/plugins 38 | └── inappstore 39 | ├── inappstore.debug.xcframework 40 | ├── inappstore.gdip 41 | └── inappstore.release.xcframework 42 | ``` 43 | 44 | ## install and run a debug build on a device 45 | 46 | ```sh 47 | > xcrun devicectl list devices 48 | > xcrun devicectl device install app --device XXXXXXX _build/iOS/battle-squares-debug.app 49 | > xcrun devicectl device process launch --device XXXXXXX com.uralys.battlesquares 50 | ``` 51 | 52 | ```sh 53 | > brew install libimobiledevice 54 | > idevicesyslog 55 | ``` 56 | -------------------------------------------------------------------------------- /fox/components/fullscreen-loader.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | 3 | extends CanvasLayer 4 | 5 | # ------------------------------------------------------------------------------ 6 | 7 | @onready var panel = $panel 8 | @onready var circle = $panel/circle 9 | 10 | @export var lod = 3.0 11 | 12 | # ------------------------------------------------------------------------------ 13 | 14 | var removing = false 15 | var showing = false 16 | 17 | # ------------------------------------------------------------------------------ 18 | 19 | func _ready(): 20 | panel.material = panel.material.duplicate() 21 | panel.material.set_shader_parameter('lod', 0.0) 22 | removing = false 23 | showing = true 24 | Animate.show(circle) 25 | 26 | # ------------------------------------------------------------------------------ 27 | 28 | func remove(): 29 | Animate.hide(circle) 30 | removing = true 31 | showing = false 32 | 33 | # ------------------------------------------------------------------------------ 34 | 35 | func _physics_process(delta): 36 | var current = panel.material.get_shader_parameter('lod') 37 | 38 | if(showing): 39 | var newValue = min(lod, current + delta * 7) 40 | panel.material.set_shader_parameter('lod', newValue) 41 | 42 | elif(removing): 43 | var newValue = current - delta * 7 44 | panel.material.set_shader_parameter('lod', newValue) 45 | 46 | if(current <= 0): 47 | get_parent().remove_child(self) 48 | -------------------------------------------------------------------------------- /fox/components/fullscreen-loader.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://taju5q2s32ku"] 2 | 3 | [ext_resource type="Shader" uid="uid://deqbvp0mjxxwq" path="res://fox/behaviours/simple-blur.gdshader" id="1_7qld6"] 4 | [ext_resource type="Script" uid="uid://d2aok60hmh3gv" path="res://fox/components/fullscreen-loader.gd" id="1_sxau7"] 5 | [ext_resource type="Texture2D" uid="uid://bvvucvxltvypj" path="res://fox/assets/gui/loader.webp" id="3_de0gn"] 6 | [ext_resource type="Script" uid="uid://lf6jlvs8yo1t" path="res://fox/behaviours/rotation.gd" id="3_t5duq"] 7 | 8 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_umrd7"] 9 | shader = ExtResource("1_7qld6") 10 | shader_parameter/lod = 2.526 11 | 12 | [node name="loader" type="CanvasLayer"] 13 | script = ExtResource("1_sxau7") 14 | 15 | [node name="panel" type="ColorRect" parent="."] 16 | material = SubResource("ShaderMaterial_umrd7") 17 | anchors_preset = 15 18 | anchor_right = 1.0 19 | anchor_bottom = 1.0 20 | grow_horizontal = 2 21 | grow_vertical = 2 22 | 23 | [node name="circle" type="TextureRect" parent="panel"] 24 | layout_mode = 1 25 | anchors_preset = 8 26 | anchor_left = 0.5 27 | anchor_top = 0.5 28 | anchor_right = 0.5 29 | anchor_bottom = 0.5 30 | offset_left = -150.0 31 | offset_top = -149.5 32 | offset_right = 150.0 33 | offset_bottom = 149.5 34 | grow_horizontal = 2 35 | grow_vertical = 2 36 | scale = Vector2(0.3, 0.3) 37 | pivot_offset = Vector2(150, 150) 38 | texture = ExtResource("3_de0gn") 39 | script = ExtResource("3_t5duq") 40 | -------------------------------------------------------------------------------- /docs/gdscript/router.md: -------------------------------------------------------------------------------- 1 | # Router 2 | 3 | Fox router enables to switch scenes easily, and simplifies screen transitions. 4 | 5 | ## Setup 6 | 7 | To use Fox router, your top scene must be named `app`, and it must have a child scene named `scene`. 8 | 9 | To allow your screens to be full screen: 10 | 11 | - `app` and `scene` can be 2 `ReferenceRect` 12 | - use `Layout mode: Uncontrolled` 13 | - use `Anchors preset: Full Rect` 14 | 15 | 16 | 17 | --- 18 | 19 | To illustrate `scene` layout: 20 | 21 | 22 | 23 | ## Initial code 24 | 25 | - Extend `router.gd` from `fox/core/router.gd` 26 | - Implement your `openXXX` by calling `_openScene` 27 | 28 | ```gdscript 29 | extends 'res://fox/core/router.gd' 30 | 31 | var home = preload("res://src/screens/home.tscn") 32 | 33 | func openHome(options = {}): 34 | call_deferred("_openScene", home, options) 35 | ``` 36 | 37 | Now you can call `Router.openHome()` anywhere, for example from your `app.gd/_ready()` function. 38 | 39 | ## Screen transitions and options 40 | 41 | To handle passed `options` and screen transitions, you can implement `onOpen` in your screen: 42 | 43 | example here in `home.gd`: 44 | 45 | ```gdscript 46 | func onOpen(options): 47 | print('opened home with options', options) 48 | ``` 49 | 50 | calling openHome with options: 51 | 52 | ```gdscript 53 | Router.openHome({plip='plop'}) 54 | ```` 55 | -------------------------------------------------------------------------------- /fox/libs/bundle.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | 3 | extends Node 4 | 5 | # ------------------------------------------------------------------------------ 6 | 7 | class_name Bundle 8 | 9 | # ------------------------------------------------------------------------------ 10 | 11 | static func getTitle(): 12 | var title = ProjectSettings.get_setting('bundle/title') 13 | return title 14 | 15 | # ------------------------------------------------------------------------------ 16 | 17 | static func getSubtitle(): 18 | var subtitle = ProjectSettings.get_setting('bundle/subtitle') 19 | return subtitle 20 | 21 | # ------------------------------------------------------------------------------ 22 | 23 | static func getPlatform(): 24 | var platform = ProjectSettings.get_setting('bundle/platform') 25 | return platform 26 | 27 | # ------------------------------------------------------------------------------ 28 | 29 | static func getAppId(): 30 | var bundleId = ProjectSettings.get_setting('bundle/id') 31 | var bundles = Files.getBundles() 32 | return bundles[bundleId].iOS.appId 33 | 34 | # ------------------------------------------------------------------------------ 35 | 36 | static func getStoreUrl(): 37 | var platform = getPlatform() 38 | var bundleId = ProjectSettings.get_setting('bundle/id') 39 | var bundles = Files.getBundles() 40 | return bundles[bundleId][platform].storeUrl 41 | 42 | # ------------------------------------------------------------------------------ 43 | -------------------------------------------------------------------------------- /docs/gdscript/animations.md: -------------------------------------------------------------------------------- 1 | # Animations 2 | 3 | ## Animate API 4 | 5 | You can use `Animate.xxx` functions anywhere. 6 | 7 | ## doc WIP 8 | 9 | Read `/fox/animations/animate.gd` for details on options 10 | 11 | ## API 12 | 13 | - static func `from`(object, \_options) 14 | 15 | - static func `to`(object, \_options): 16 | 17 | - static func `to`(array, \_options): 18 | 19 | - static func `toAndBack`(object, \_options): 20 | 21 | - static func `show`(object, duration = 0.3, delay = 0): 22 | 23 | - static func `hide`(object, duration = 0.3, delay = 0): 24 | 25 | - static func `appear`(object, delay = 0): 26 | 27 | - static func `disappear`(object, delay = 0): 28 | 29 | - static func `bounce`(object): 30 | 31 | - static func `swing`(object, \_options): 32 | 33 | ## Examples 34 | 35 | ```gd 36 | Animate.to(yourObject, { 37 | propertyPath = 'position', 38 | toValue = Vector2(200, 200), 39 | duration = 0.5, 40 | easing = Tween.EASE_IN_OUT, 41 | delay = 0.3 42 | }) 43 | ``` 44 | 45 | ```gd 46 | # showing the car smoothly 47 | Animate.show(car) 48 | 49 | # then wait for 2 seconds 50 | await Wait.forSomeTime(car, 2).timeout 51 | 52 | # then moving the car to (200, 200) 53 | Animate.to(car, { 54 | propertyPath = 'position', 55 | toValue = Vector2(200, 200), 56 | duration = 0.5 57 | }) 58 | ``` 59 | 60 | ```gdscript 61 | Animate.to([potion, car, book], { 62 | propertyPath = "position", 63 | toValue = Vector2(0, 0), 64 | delayBetweenElements = 1, 65 | onFinished = func(): 66 | G.log('DONE'); 67 | }) 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/gdscript/popups.md: -------------------------------------------------------------------------------- 1 | # Popups 2 | 3 | You can create Popups by creating a `ReferenceRect` extending `components/popup`: 4 | 5 | ```gd 6 | extends 'res://fox/components/popup.gd' 7 | ``` 8 | 9 | if you override the `_ready` function, make sure to call `super._ready()`: 10 | 11 | ```gd 12 | func _ready(): 13 | super._ready() 14 | ``` 15 | 16 | then add a function somewhere to instantiate the popup, for example in your `Router` 17 | 18 | ```gd 19 | var ShopPopup = preload('res://shop.tscn') 20 | 21 | func openShop(): 22 | var shop = ShopPopup.instantiate() 23 | $/root/app/popups.add_child(shop) 24 | ``` 25 | 26 | ## Blur and Panel 27 | 28 | - You can add a `components/blur.tscn`, name it `blur`, to blur the background, the popup will automatically show/hide the blur. 29 | 30 | - You can add a `Panel`, name it `panel`, to automatically show/hide your content. 31 | 32 | - Inside this panel, you can add a `closeButton` with a `pressed` signal to automatically call the `close` function. 33 | 34 | example: 35 | 36 | ```gd 37 | extends 'res://fox/components/popup.gd' 38 | 39 | # ------------------------------------------------------------------------------ 40 | 41 | func _ready(): 42 | super._ready() 43 | 44 | Animate.from(panel, { 45 | propertyPath = 'position', 46 | fromValue = panel.position + Vector2(0, G.H), 47 | duration = 1, 48 | transition= Tween.TRANS_QUAD, 49 | easing = Tween.EASE_OUT 50 | }) 51 | 52 | # ------------------------------------------------------------------------------ 53 | 54 | func close(): 55 | Router.openHome() 56 | ``` 57 | -------------------------------------------------------------------------------- /fox/behaviours/radial-blur.gdshader: -------------------------------------------------------------------------------- 1 | shader_type canvas_item; 2 | 3 | uniform float value : hint_range(-6.283, 6.283, 0.1); 4 | uniform int quality : hint_range(1, 100, 1); 5 | 6 | uniform float occilation_speed : hint_range(0.0, 10.0, 0.1); 7 | 8 | uniform float scale_value : hint_range(0.0, 10.0, 0.1); 9 | 10 | uniform float xOffset : hint_range(-0.5, 0.5); 11 | uniform float yOffset : hint_range(-0.5, 0.5); 12 | 13 | vec2 rotated(vec2 pos, float rads){ 14 | 15 | pos -= vec2(0.5,0.5); 16 | 17 | float tempvecx = pos.x * cos(rads) - pos.y * sin(rads); 18 | 19 | float tempvecy = pos.x * sin(rads) + pos.y * cos(rads); 20 | 21 | vec2 finalvalue = vec2(tempvecx,tempvecy) + vec2(0.5,0.5); 22 | 23 | return finalvalue; 24 | } 25 | 26 | vec2 scale(vec2 uv, float x, float y) 27 | { 28 | mat2 scale = mat2(vec2(x, 0.0), vec2(0.0, y)); 29 | 30 | uv -= 0.5; 31 | uv = uv * scale; 32 | uv += 0.5; 33 | 34 | if (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0) 35 | return uv; 36 | 37 | return vec2(-20.0*scale_value,-20.0*scale_value); 38 | } 39 | 40 | 41 | 42 | 43 | void fragment(){ 44 | 45 | COLOR = vec4(0,0,0,0); 46 | 47 | float i = 0.0; 48 | for (int j = 0; j < quality; j++ ){ 49 | i += (value+ sin(TIME*occilation_speed)/2.0 ) /float(quality); 50 | 51 | COLOR += texture(TEXTURE,scale(rotated(UV,i)+vec2(xOffset/scale_value,yOffset/scale_value), scale_value,scale_value )); 52 | } 53 | 54 | float t = 1.0/float(quality); 55 | COLOR *= t; 56 | 57 | 58 | } 59 | 60 | 61 | void vertex(){ 62 | 63 | VERTEX.y *= scale_value; 64 | VERTEX.x *= scale_value; 65 | } -------------------------------------------------------------------------------- /docs/tips/gamedev.md: -------------------------------------------------------------------------------- 1 | # Misc tools for game development 2 | 3 | ## Godot shaders 4 | 5 | - 6 | - 7 | 8 | ## Godot scripts/examples 9 | 10 | - 11 | - 12 | - 13 addons: 13 | - transitions: 14 | 15 | Path2d: 16 | 17 | - 18 | - 19 | 20 | Build an Isometric Tower Defense: 21 | 22 | - 23 | - 24 | - 25 | 26 | ## Other godot tools 27 | 28 | - vscode extension 29 | - gdscript formatter, linter 30 | 31 | ## Audio 32 | 33 | - 34 | - 35 | 36 | ## Game UI inspiration 37 | 38 | - 39 | - 40 | 41 | ## Assets 42 | 43 | ### kenney 44 | 45 | - 46 | - --> triggers 47 | 48 | ### others 49 | 50 | - 51 | - 52 | - 53 | - 54 | - 55 | -------------------------------------------------------------------------------- /fox/animations/intro-animation.gd: -------------------------------------------------------------------------------- 1 | extends CanvasLayer 2 | 3 | # ------------------------------------------------------------------------------ 4 | 5 | signal introFinished 6 | 7 | # ------------------------------------------------------------------------------ 8 | 9 | @onready var logo = $logo 10 | @onready var letters = $letters 11 | 12 | @onready var U = $letters/u 13 | @onready var R = $letters/r 14 | @onready var A = $letters/a 15 | @onready var L = $letters/l 16 | @onready var Y = $letters/y 17 | @onready var S = $letters/s 18 | @onready var DOT = $letters/dot 19 | 20 | # ------------------------------------------------------------------------------ 21 | 22 | func _ready(): 23 | G.log('> intro animation'); 24 | G.log('-------------------------------') 25 | logo.hide() 26 | letters.hide() 27 | 28 | await Wait.forSomeTime(self, 0.25).timeout 29 | Animate.show(logo, 0.75) 30 | 31 | letters.show() 32 | Animate.show(U, 1.5, 0.15) 33 | Animate.show(R, 1.5, 0.25) 34 | Animate.show(A, 1.5, 0.35) 35 | Animate.show(L, 1.5, 0.15) 36 | Animate.show(Y, 1.5, 0.25) 37 | Animate.show(S, 1.5, 0.15) 38 | Animate.show(DOT, 1.5, 0.15) 39 | 40 | await Wait.forSomeTime(self, 2).timeout 41 | 42 | 43 | Animate.hide(logo, 0.5) 44 | Animate.hide(U, 0.5, 0.15) 45 | Animate.hide(R, 0.5, 0.25) 46 | Animate.hide(A, 0.5, 0.4) 47 | Animate.hide(L, 0.5, 0.25) 48 | Animate.hide(Y, 0.5, 0.15) 49 | Animate.hide(S, 0.5, 0.1) 50 | Animate.hide(DOT, 0.5, 0.05) 51 | 52 | await Wait.forSomeTime(self, 0.9).timeout 53 | exitIntro() 54 | 55 | # ------------------------------------------------------------------------------ 56 | 57 | func exitIntro(): 58 | introFinished.emit() 59 | get_parent().remove_child(self) 60 | queue_free() 61 | 62 | # ------------------------------------------------------------------------------ 63 | 64 | -------------------------------------------------------------------------------- /fox/default.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "core": { 3 | "title": "Your Game", 4 | "godot": "/Applications/Apps/Godot.app/Contents/MacOS/Godot", 5 | "useNotifications": false 6 | }, 7 | "generate:icons": { 8 | "input": "_release/images/", 9 | "output": "assets/generated/icons", 10 | "base": "icon-1200x1200.png", 11 | "desktop": "icon-desktop-512x512.png", 12 | "background": "adaptive-background.png", 13 | "foreground": "adaptive-foreground.png" 14 | }, 15 | "generate:splashscreens": { 16 | "input": "_release/images/base-splashscreen.png", 17 | "output": "assets/generated/splashscreens" 18 | }, 19 | "generate:screenshots": { 20 | "input": "_release/images/raw-shots", 21 | "output": "_release/images/generated-screenshots", 22 | "orientation": "landscape", 23 | "sizes": [ 24 | { "name": "iPhone_6'7", "resolution": "2796x1290" }, 25 | { "name": "iPhone_6'5", "resolution": "2778x1284" }, 26 | { "name": "iPhone_5'5", "resolution": "2208x1242" }, 27 | { "name": "iPad_12'9", "resolution": "2732x2048" }, 28 | { "name": "16'9", "resolution": "2160x1215" }, 29 | { "name": "4'3", "resolution": "1440x1080" }, 30 | { "name": "macOS", "resolution": "2560x1600" }, 31 | { "name": "webp-16'9", "resolution": "2160x1215", "extension": ".webp" } 32 | ] 33 | }, 34 | "run:editor": { 35 | "resolution": "2980x2220", 36 | "position": "0,0" 37 | }, 38 | "update-po-files": { 39 | "poFiles": "./assets/translations/*.po", 40 | "potTemplate": "./assets/translations/locales.pot" 41 | }, 42 | "run:game": { 43 | "position": "1120,20" 44 | }, 45 | "bundles": { 46 | "yourApp": { 47 | "uid": "com.your.app.id", 48 | "Android": { 49 | "storeUrl": "https://play.google.com/store/apps/details?id=com.xxx.xxx" 50 | }, 51 | "iOS": { 52 | "appId": "idxxx", 53 | "storeUrl": "https://apps.apple.com/us/app/idxxx" 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | # 🔋 experimental NodeJS CLI 2 | 3 | - to watch your files and allow to `live reload` your game. 4 | - to `export` your debug and release bundles. 5 | - to `generate` your release icons and screenshots. 6 | 7 | 8 | 9 | ## requirements 10 | 11 | To use the CLI you'll need NodeJS installed 12 | 13 | ### prepare the executable 14 | 15 | Install the dev dependencies: 16 | 17 | ```sh 18 | npm install 19 | ``` 20 | 21 | link the `fox` executable: 22 | 23 | ```sh 24 | ln -s ~/Projects/uralys/gamedev/fox/cli/cli.js /usr/local/bin/fox 25 | ``` 26 | 27 | You may have to reload your termilnal to have `fox` in your path; 28 | 29 | You can now execute fox commands from your terminal: 30 | 31 | ```sh 32 | fox 33 | ``` 34 | 35 | You can pass parameters to Godot by using them directly from the command line. 36 | 37 | See all available parameters on [Godot CLI Reference](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html#command-line-reference) 38 | 39 | Example: 40 | 41 | ```sh 42 | fox run:game --headless --debug-collisions 43 | ``` 44 | 45 | ## usage 46 | 47 | ```ini 48 | Usage: fox [options] 49 | 50 | Commands: 51 | fox run:editor open Godot Editor with your main scene 52 | 53 | fox run:game start your game to debug 54 | 55 | fox export export a bundle for one of your presets 56 | 57 | fox generate:icons generate icons, using a base 1200x1200 image 58 | 59 | fox generate:splashscreens generate splashscreens, extending a background 60 | color from a centered base image 61 | 62 | fox generate:screenshots resize all images in a folder to 2560x1600, to 63 | match store requirements 64 | ``` 65 | 66 | - more details for exporting [here](./docs/export.md) 67 | 68 | ## shortcuts 69 | 70 | You can use the following shortcuts in the terminal: 71 | 72 | - `ctrl + c` to stop the current command 73 | - `r` to reload the game (only with the `run:game` command) 74 | -------------------------------------------------------------------------------- /fox/core/globals.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # ------------------------------------------------------------------------------ 4 | 5 | const RELEASE = 'release' 6 | const DEBUG = 'debug' 7 | 8 | # ------------------------------------------------------------------------------ 9 | # Fox required globals 10 | 11 | var BUNDLE_ID 12 | var BUNDLES 13 | var ENV 14 | var PLATFORM 15 | var RECORD_PATH 16 | var VERSION 17 | var VERSION_CODE 18 | 19 | # ------------------------------------------------------------------------------ 20 | 21 | var W 22 | var H 23 | var SCREEN_CENTER 24 | 25 | # ------------------------------------------------------------------------------ 26 | 27 | func _ready(): 28 | G.BUNDLE_ID = ProjectSettings.get_setting('bundle/id') 29 | G.ENV = ProjectSettings.get_setting('bundle/env') 30 | G.PLATFORM = ProjectSettings.get_setting('bundle/platform') 31 | G.VERSION = ProjectSettings.get_setting('bundle/version') 32 | G.VERSION_CODE = ProjectSettings.get_setting('bundle/versionCode') 33 | G.RECORD_PATH = 'user://saved-data.' + G.BUNDLE_ID + '.bin' 34 | 35 | G.log('========================================') 36 | var foxVersion = ProjectSettings.get_setting('fox/version') 37 | foxVersion = foxVersion if foxVersion else '' 38 | G.log('[🦊 Fox]', foxVersion) 39 | G.log('-------------------------------') 40 | G.log('bundle/id: ' + G.BUNDLE_ID) 41 | G.log('bundle/env: ' + G.ENV) 42 | G.log('bundle/platform: ' + G.PLATFORM) 43 | 44 | # ------------------------------------------------------------------------------ 45 | 46 | func __ansi(o): 47 | return __.bbcodeToANSI(o) if o is String else o 48 | 49 | func debug(a, b=null,c=null,d=null,e=null,f=null,g=null): 50 | if(G.ENV == 'release'): return 51 | G.log('🫧 [color=magenta](debug)[/color]', a, b, c, d, e, f, g) 52 | 53 | func log(a, b=null,c=null,d=null,e=null,f=null,g=null,h=null): 54 | prints(__ansi(a), 55 | __ansi(b) if b != null else '', 56 | __ansi(c) if c != null else '', 57 | __ansi(d) if d != null else '', 58 | __ansi(e) if e != null else '', 59 | __ansi(f) if f != null else '', 60 | __ansi(g) if g != null else '', 61 | __ansi(h) if h != null else '' 62 | ) 63 | -------------------------------------------------------------------------------- /cli/generate-splashscreens.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // generates splashscreens from a base image 3 | // requires https://www.imagemagick.org/script/index.php 4 | // to enable "convert" command 5 | // OSX: brew install imagemagick 6 | // ----------------------------------------------------------------------------- 7 | 8 | import chalk from 'chalk'; 9 | import shell from 'shelljs'; 10 | 11 | // ----------------------------------------------------------------------------- 12 | 13 | const LANDSCAPE_SIZES = ['2436x1125', '2208x1242', '1024x768', '2048x1536']; 14 | 15 | const PORTRAIT_SIZES = [ 16 | '640x960', 17 | '640x1136', 18 | '750x1334', 19 | '1125x2436', 20 | '768x1024', 21 | '1536x2048', 22 | '1242x2208' 23 | ]; 24 | 25 | // ----------------------------------------------------------------------------- 26 | 27 | const convert = (inputFile, backgroundColor, outputPath) => (size) => { 28 | console.log(` ${chalk.magenta.italic(size)}`); 29 | shell.exec( 30 | `convert ${inputFile} -gravity center -background '${backgroundColor}' -extent ${size} "${outputPath}/splashscreen-${size}.png"` 31 | ); 32 | }; 33 | 34 | // ----------------------------------------------------------------------------- 35 | 36 | const generateSplashscreens = (config) => { 37 | const {input, output, backgroundColor = '#181818'} = config; 38 | console.log(`---> generating ${chalk.blue.bold('splashscreens')}...`); 39 | 40 | const applyConversion = convert(input, backgroundColor, output); 41 | 42 | console.log(` > creating ${chalk.magenta.italic('landscape')} launch screens...`); 43 | LANDSCAPE_SIZES.forEach(applyConversion); 44 | 45 | console.log(` > creating ${chalk.magenta.italic('portrait')} launch screens...`); 46 | PORTRAIT_SIZES.forEach(applyConversion); 47 | 48 | console.log( 49 | `\nCreated ${chalk.green( 50 | LANDSCAPE_SIZES.length + PORTRAIT_SIZES.length 51 | )} splashscreens ${chalk.green('successfully')}.` 52 | ); 53 | console.log(`---> ${chalk.blue.bold('output:')} ${output}`); 54 | }; 55 | 56 | // ----------------------------------------------------------------------------- 57 | 58 | export default generateSplashscreens; 59 | -------------------------------------------------------------------------------- /fox/behaviours/disintegrate-material.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://crjp8wp8672lv"] 2 | 3 | [sub_resource type="Shader" id="1"] 4 | code = "/* 5 | Shader from Godot Shaders - the free shader library. 6 | https://godotshaders.com/shader/teleport-effect 7 | 8 | This shader is under CC0 license. Feel free to use, improve and 9 | change this shader according to your needs and consider sharing 10 | the modified result on godotshaders.com. 11 | */ 12 | 13 | shader_type canvas_item; 14 | 15 | uniform float progress : hint_range(0.0, 1.0); 16 | uniform float noise_density = 60; 17 | uniform float beam_size : hint_range(0.01, 0.15); 18 | uniform vec4 color : source_color = vec4(0.0, 1.02, 1.2, 1.0); 19 | 20 | // We are generating our own noise here. You could experiment with the 21 | // built in SimplexNoise or your own noise texture for other effects. 22 | vec2 random(vec2 uv){ 23 | uv = vec2( dot(uv, vec2(127.1,311.7) ), 24 | dot(uv, vec2(269.5,183.3) ) ); 25 | return -1.0 + 2.0 * fract(sin(uv) * 43758.5453123); 26 | } 27 | 28 | float noise(vec2 uv) { 29 | vec2 uv_index = floor(uv); 30 | vec2 uv_fract = fract(uv); 31 | 32 | vec2 blur = smoothstep(0.0, 1.0, uv_fract); 33 | 34 | return mix( mix( dot( random(uv_index + vec2(0.0,0.0) ), uv_fract - vec2(0.0,0.0) ), 35 | dot( random(uv_index + vec2(1.0,0.0) ), uv_fract - vec2(1.0,0.0) ), blur.x), 36 | mix( dot( random(uv_index + vec2(0.0,1.0) ), uv_fract - vec2(0.0,1.0) ), 37 | dot( random(uv_index + vec2(1.0,1.0) ), uv_fract - vec2(1.0,1.0) ), blur.x), blur.y) * 0.5 + 0.5; 38 | } 39 | 40 | void fragment() 41 | { 42 | vec4 tex = texture(TEXTURE, UV); 43 | 44 | float noise = noise(UV * noise_density) * UV.y; 45 | 46 | float d1 = step(progress, noise); 47 | float d2 = step(progress - beam_size, noise); 48 | 49 | vec3 beam = vec3(d2 - d1) * color.rgb; 50 | 51 | tex.rgb += beam; 52 | tex.a *= d2; 53 | 54 | COLOR = tex; 55 | }" 56 | 57 | [resource] 58 | shader = SubResource("1") 59 | shader_parameter/progress = 0.0 60 | shader_parameter/noise_density = 12.0 61 | shader_parameter/beam_size = 0.071 62 | shader_parameter/color = Color(0.960784, 0.996078, 1, 1) 63 | -------------------------------------------------------------------------------- /cli/generate-screenshots.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // resize screenshots to match store requirements 3 | // requires https://www.imagemagick.org/script/index.php 4 | // to enable "convert" command 5 | // OSX: brew install imagemagick 6 | // ----------------------------------------------------------------------------- 7 | 8 | import chalk from 'chalk'; 9 | import fs from 'fs'; 10 | import path from 'path'; 11 | import shell from 'shelljs'; 12 | 13 | // ----------------------------------------------------------------------------- 14 | 15 | const generateScreenshots = (config) => { 16 | const {orientation, input, output, sizes} = config; 17 | const projectPath = path.resolve(process.cwd(), './'); 18 | 19 | console.log(`\n⚙️ veryfing folders ...`); 20 | 21 | sizes.forEach(({name}) => { 22 | const sizeFolder = `${projectPath}/${output}/${name}`; 23 | 24 | if (!fs.existsSync(sizeFolder)) { 25 | shell.mkdir('-p', sizeFolder); 26 | console.log(`✅ created ${sizeFolder}`); 27 | } 28 | }) 29 | 30 | 31 | console.log(`\n⚙️ resizing ${chalk.blue.bold('screenshots')}...`); 32 | console.log(`⚙️ orientation: ${chalk.blue.bold(orientation)}`); 33 | 34 | const files = fs.readdirSync(input); 35 | 36 | files.forEach((fileName) => { 37 | const extension = path.extname(fileName); 38 | if (!['.png', '.jpg', '.jpeg', '.webp'].includes(extension)) { 39 | return; 40 | } 41 | 42 | sizes.forEach((size) => { 43 | const resolution = orientation === 'landscape' ? size.resolution : size.resolution.split('x').reverse().join('x'); 44 | 45 | const outputFileName = `${fileName.split(extension)[0]}-${resolution}${size.extension || extension}`; 46 | const outputPath = `${output}/${size.name}/${outputFileName}`; 47 | 48 | console.log(`\n > ${chalk.magenta.italic(fileName)} | ${chalk.magenta.italic(size.name)} --> ${outputPath}`); 49 | 50 | shell.exec( 51 | `convert ${input}/${fileName} -resize ${resolution}^ -gravity center -extent ${resolution} "${outputPath}"` 52 | ); 53 | 54 | console.log(`Resized to ${resolution} ${chalk.green('successfully')}.`); 55 | }); 56 | }); 57 | }; 58 | 59 | // ----------------------------------------------------------------------------- 60 | 61 | export default generateScreenshots; 62 | -------------------------------------------------------------------------------- /fox/components/review/ask-for-review.gd: -------------------------------------------------------------------------------- 1 | extends 'res://fox/components/popup.gd' 2 | 3 | # ------------------------------------------------------------------------------ 4 | 5 | @onready var text: Label = $panel/text 6 | @onready var rateLabel: Label = $panel/rateButton/label 7 | @onready var rateButton = $panel/rateButton 8 | 9 | # ------------------------------------------------------------------------------ 10 | 11 | var rateMe 12 | var landscape = false 13 | 14 | # ------------------------------------------------------------------------------ 15 | 16 | func _ready(): 17 | text.text = tr('please rate this app') 18 | 19 | if(landscape): 20 | panel.position = Vector2((G.W - panel.size.x)/2, 80) 21 | 22 | super._ready() 23 | 24 | if Engine.has_singleton('GodotAndroidRateme'): 25 | rateMe = Engine.get_singleton('GodotAndroidRateme') 26 | rateMe.completed.connect(onRatingCompleted) 27 | rateMe.error.connect(onRatingError) # use err as string 28 | rateButton.visible = false 29 | closeButton.visible = false 30 | rateMe.show() 31 | 32 | elif Engine.has_singleton('InappReviewPlugin'): 33 | var iOSReview = $iOSReview 34 | iOSReview.connect('review_flow_launched', onRatingCompleted) 35 | iOSReview.launch_review_flow() 36 | 37 | else: 38 | rateLabel.text = tr('Rate now') 39 | rateButton.connect('pressed', showRateMe) 40 | 41 | Animate.from(panel, { 42 | propertyPath = 'position', 43 | fromValue = panel.position - Vector2(0, 300), 44 | transition = Tween.TRANS_QUAD, 45 | easing = Tween.EASE_OUT 46 | }) 47 | 48 | # ------------------------------------------------------------------------------ 49 | 50 | func showRateMe(): 51 | if(Bundle.getPlatform() == 'iOS'): 52 | OS.shell_open('itms-apps://itunes.apple.com/app/' + Bundle.getAppId() + '?action=write-review') 53 | else: 54 | OS.shell_open(Bundle.getStoreUrl()) 55 | 56 | onRatingCompleted() 57 | 58 | # ------------------------------------------------------------------------------ 59 | 60 | func onRatingCompleted(): 61 | Player.setRatingDone() 62 | close() 63 | 64 | # ------------------------------------------------------------------------------ 65 | 66 | func onRatingError(): 67 | close() 68 | 69 | # ------------------------------------------------------------------------------ 70 | 71 | func useForLandscape(): 72 | landscape = true 73 | -------------------------------------------------------------------------------- /fox/behaviours/pencil.gdshader: -------------------------------------------------------------------------------- 1 | shader_type canvas_item; 2 | 3 | uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap; 4 | 5 | uniform vec4 u_bgColor: source_color = vec4(1.0, 1.0, 1.0, 1.0); 6 | uniform float u_bgColorFactor: hint_range(0.0, 1.0) = 0.4; 7 | uniform vec4 u_patternColor: source_color = vec4(0.0, 0.0, 0.0, 1.0); 8 | 9 | uniform float u_threshold1: hint_range(0.0, 1.0) = 0.75; 10 | uniform float u_threshold2: hint_range(0.0, 1.0) = 0.50; 11 | uniform float u_threshold3: hint_range(0.0, 1.0) = 0.25; 12 | uniform float u_threshold4: hint_range(0.0, 1.0) = 0.05; 13 | 14 | uniform vec2 u_bgTiling = vec2(1.0, 1.0); 15 | uniform vec2 u_patternTiling = vec2(1.0, 1.0); 16 | 17 | uniform sampler2D u_bgTexture; 18 | uniform sampler2D u_patternTexture; 19 | 20 | const float C2_SQRT2 = 0.707106781; 21 | const mat2 ROT_45 = mat2(vec2(C2_SQRT2, -C2_SQRT2), vec2(C2_SQRT2, C2_SQRT2)); 22 | const vec4 COLOR_WHITE = vec4(1.0, 1.0, 1.0, 1.0); 23 | 24 | float getIntensity(vec3 color) 25 | { 26 | return 0.299*color.r + 0.587*color.g + 0.114*color.b; 27 | } 28 | 29 | vec4 getPatternColor(vec2 uv, float intensity) 30 | { 31 | vec2 patternUV1 = vec2(-uv.x, uv.y) * u_patternTiling; 32 | vec2 patternUV2 = uv * u_patternTiling; 33 | vec2 patternUV3 = ROT_45*(uv + vec2(0.2358, 0.9123)) * u_patternTiling; 34 | vec2 patternUV4 = (vec2(uv.x, -uv.y) + vec2(0.4123, 0.7218)) * u_patternTiling; 35 | vec4 pCol1 = texture(u_patternTexture, patternUV1); 36 | vec4 pCol2 = texture(u_patternTexture, patternUV2); 37 | vec4 pCol3 = texture(u_patternTexture, patternUV3); 38 | vec4 pCol4 = texture(u_patternTexture, patternUV4); 39 | 40 | if(intensity > u_threshold1) 41 | return vec4(1.0, 1.0, 1.0, 1.0); 42 | if(intensity > u_threshold2) 43 | return mix(pCol1, COLOR_WHITE, 0.5); 44 | if(intensity > u_threshold3) 45 | return mix(pCol1*pCol2, COLOR_WHITE, 0.3); 46 | if(intensity > u_threshold4) 47 | return mix(pCol1*pCol2*pCol3, COLOR_WHITE, 0.1); 48 | return pCol1*pCol2*pCol3*pCol4*0.8; 49 | } 50 | 51 | void fragment() 52 | { 53 | vec4 origColor = texture(SCREEN_TEXTURE, SCREEN_UV); 54 | float intensity = getIntensity(origColor.rgb); 55 | vec4 bgColor = mix(texture(u_bgTexture, UV*u_bgTiling), u_bgColor, u_bgColorFactor); 56 | vec4 patternColor = getPatternColor(UV, intensity); 57 | vec4 color = mix(u_patternColor, bgColor, getIntensity(patternColor.rgb)); 58 | COLOR = color; 59 | } -------------------------------------------------------------------------------- /docs/exporting/export.md: -------------------------------------------------------------------------------- 1 | # exporting with CLI 2 | 3 | To quick export your presets you can use: 4 | 5 | ```sh 6 | fox export 7 | ``` 8 | 9 | This will ask which preset you want to use, update it if you need to change the version for example, and run Godot export. 10 | 11 | This command cannot work out of the box, you need to set up your presets and config before to use it. 12 | 13 | Follow these requirements explained in the sections below: 14 | 15 | - prepare the presets, keys and path with Godot 16 | - define you `env` 17 | 18 | Then you're all set! You can now use `fox export` to quickly export your project, following the prompt to select your preset from the CLI. 19 | 20 | ## prepare the presets 21 | 22 | First, you need to install templates from Godot. 23 | 24 | Use `Project > Export` and be sure to generate your `export_presets.cfg` without errors from Godot Editor. 25 | 26 | The `Export path` will be generated from the preset name, `bundleId` and `env` 27 | 28 | ## define you `env` 29 | 30 | Define if your preset is for `release`, `debug` by setting it a `custom_features`. 31 | 32 | example: 33 | 34 | ```ini 35 | [preset.0] 36 | 37 | name="Android Debug" 38 | platform="Android" 39 | custom_features="env:debug" 40 | include_filter="override.cfg" 41 | ``` 42 | 43 | Then, when exporting, it will apply Godot CLI option `--export-release` or `--export-debug` depending on the `env` you've set. 44 | 45 | ## additional options 46 | 47 | ### version 48 | 49 | You may use the current version, or update it before exporting. 50 | 51 | `Fox` uses `npm version` which updates `package.json` and creates a `git tag` 52 | 53 | Then this `version` is replaced in your preset property depending on the platform. 54 | 55 | ### bundles 56 | 57 | **disclaimer**: I've experimented bundles for the different chapters in [Lockey Land](https://uralys.com/lockeyland), exported as separate applications. 58 | 59 | By default 1 app = 1 bundle --> `bundleId` is the same as the app name. 60 | 61 | You can configure `Fox` to export many apps built from a single project. 62 | 63 | Each bundle must have its `uid`, can use another icon, a subtitle attached to the main application name etc... 64 | 65 | #### example in `fox.config.json` 66 | 67 | ```json 68 | "bundles": { 69 | "app1": { 70 | "uid": "com.your.app1", 71 | "subtitle": "theme1", 72 | "Android": { 73 | "keystore/release_user": "admin-app1", 74 | } 75 | }, 76 | "app2": { 77 | "uid": "com.your.app2", 78 | "subtitle": "theme2", 79 | "Android": { 80 | "keystore/release_user": "admin-app2", 81 | } 82 | } 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /fox/components/popup.gd: -------------------------------------------------------------------------------- 1 | extends ReferenceRect 2 | 3 | # ------------------------------------------------------------------------------ 4 | 5 | signal closed 6 | 7 | # ------------------------------------------------------------------------------ 8 | 9 | @onready var blur = $blur 10 | @onready var panel = $panel 11 | 12 | @onready var closeButton = $panel/closeButton 13 | 14 | # ------------------------------------------------------------------------------ 15 | 16 | @export var blurAmount: int = 60 17 | 18 | # ------------------------------------------------------------------------------ 19 | 20 | var closing = false 21 | var showing = false 22 | 23 | var thisPopupPauseEngine = false 24 | 25 | # ------------------------------------------------------------------------------ 26 | 27 | func _ready(): 28 | if(thisPopupPauseEngine): 29 | get_tree().paused = true 30 | 31 | showing = true 32 | 33 | if(blur != null): 34 | blur.mouse_filter = Control.MOUSE_FILTER_STOP 35 | blur.material.set_shader_parameter('blur_amount', 0) 36 | 37 | if(closeButton): 38 | if(closeButton.has_node('interactiveArea2D')): 39 | closeButton.get_node('interactiveArea2D').inputPriority = -1 40 | closeButton.get_node('interactiveArea2D').connect('pressed', close) 41 | else: 42 | closeButton.connect('pressed', close) 43 | 44 | Animate.show(panel) 45 | 46 | # ------------------------------------------------------------------------------ 47 | 48 | func _physics_process(delta): 49 | var current = blur.material.get_shader_parameter('blur_amount') 50 | if(closing): 51 | var newValue = current - delta * 200 52 | 53 | blur.material.set_shader_parameter('blur_amount', newValue) 54 | 55 | if(newValue < 0): 56 | get_parent().remove_child(self) 57 | queue_free() 58 | 59 | elif(showing): 60 | var newValue = current + delta * 200 61 | 62 | if(newValue < blurAmount): 63 | blur.material.set_shader_parameter('blur_amount', newValue) 64 | else: 65 | showing = false 66 | 67 | # ------------------------------------------------------------------------------ 68 | 69 | func close(): 70 | closing = true 71 | 72 | if(__.Get('CONFIRMATION', Sound)): 73 | Sound.play(Sound.CONFIRMATION) 74 | elif(__.Get('PRESS', Sound)): 75 | Sound.play(Sound.PRESS) 76 | 77 | if(thisPopupPauseEngine): 78 | get_tree().paused = false 79 | 80 | Animate.to(panel, { 81 | propertyPath = 'modulate:a', 82 | toValue = 0, 83 | duration = 0.3 84 | }) 85 | 86 | closed.emit() 87 | 88 | # ------------------------------------------------------------------------------ 89 | -------------------------------------------------------------------------------- /fox/libs/http.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | 3 | extends Node 4 | 5 | # ------------------------------------------------------------------------------ 6 | 7 | class_name HTTP 8 | 9 | # ------------------------------------------------------------------------------ 10 | 11 | static var API_URL = ProjectSettings.get_setting('custom/api-url') 12 | static var API_KEY = ProjectSettings.get_setting('custom/api-key') 13 | 14 | # ------------------------------------------------------------------------------ 15 | 16 | static func _createHTTPRequest(caller, options): 17 | var _url = __.Get('url' ,options) 18 | var endpoint = __.GetOr('', 'endpoint' ,options) 19 | var onComplete = __.Get('onComplete', options) 20 | var onError = __.Get('onError', options) 21 | 22 | var url = _url if(_url != null) else API_URL + endpoint 23 | 24 | var http = HTTPRequest.new() 25 | caller.add_child(http) 26 | 27 | var _onComplete = func(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray): 28 | if(result != OK): 29 | G.log('❌ [b][color=pink]Error with HTTPRequest[/color][/b]', {url=url, result=result}, 'see https://docs.godotengine.org/en/stable/classes/class_httprequest.html#enumerations') 30 | if(onError): 31 | onError.call(result, response_code, headers, body) 32 | return 33 | 34 | if(onComplete): 35 | onComplete.call(result, response_code, headers, body) 36 | 37 | http.request_completed.connect(_onComplete) 38 | return {http=http, url=url} 39 | 40 | # ------------------------------------------------------------------------------ 41 | 42 | static func _performRequest(caller, options, method): 43 | var httpRequest = _createHTTPRequest(caller, options) 44 | var body = __.GetOr("", 'body', options) 45 | var http = httpRequest.http 46 | var url = httpRequest.url 47 | 48 | if(typeof(body) == TYPE_DICTIONARY): 49 | body = JSON.stringify(body) 50 | 51 | http.request( 52 | url, 53 | [ 54 | 'x-api-key:' + API_KEY, 55 | 'Content-Type: application/json' 56 | ], 57 | method, 58 | body 59 | ) 60 | 61 | # ------------------------------------------------------------------------------ 62 | 63 | static func Get(caller, options): 64 | _performRequest(caller, options, HTTPClient.METHOD_GET) 65 | 66 | # ------------------------------------------------------------------------------ 67 | 68 | static func Post(caller, options): 69 | _performRequest(caller, options, HTTPClient.METHOD_POST) 70 | 71 | # ------------------------------------------------------------------------------ 72 | 73 | static func Put(caller, options): 74 | _performRequest(caller, options, HTTPClient.METHOD_PUT) 75 | -------------------------------------------------------------------------------- /cli/generate-icons.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // generates icons from a base image 3 | // requires https://www.imagemagick.org/script/index.php 4 | // to enable "convert" command 5 | // OSX: brew install imagemagick 6 | // ----------------------------------------------------------------------------- 7 | // based on 🍒 https://github.com/chrisdugne/cherry/blob/master/prepare-icons.sh 8 | // ----------------------------------------------------------------------------- 9 | 10 | import chalk from 'chalk'; 11 | import fs from 'fs'; 12 | import shell from 'shelljs'; 13 | 14 | // ----------------------------------------------------------------------------- 15 | 16 | // https://developer.apple.com/library/archive/documentation/Xcode/Reference/xcode_ref-Asset_Catalog_Format/AppIconType.html 17 | const SIZES = [ 18 | '20x20', 19 | '29x29', 20 | '32x32', 21 | '40x40', 22 | '58x58', 23 | '60x60', 24 | '64x64', 25 | '76x76', 26 | '80x80', 27 | '87x87', 28 | '120x120', 29 | '128x128', 30 | '152x152', 31 | '167x167', 32 | '180x180', 33 | '192x192', 34 | '256x256', 35 | '512x512', 36 | '1024x1024' 37 | ]; 38 | 39 | // ----------------------------------------------------------------------------- 40 | 41 | const generateIcons = (config) => { 42 | console.log(`⚙️ generating ${chalk.blue.bold('icons')}...`); 43 | const {input, output, base, background, foreground, desktop} = config; 44 | 45 | if (!fs.existsSync(`${input}/${base}`)) { 46 | console.log( 47 | `${chalk.bold('input')} base ${chalk.red.bold('does not exist')}, please check your config` 48 | ); 49 | return null; 50 | } 51 | console.log('input:', `${input}/${base}`); 52 | 53 | SIZES.forEach((size) => { 54 | console.log(` > ${chalk.magenta.italic(size)}`); 55 | shell.exec( 56 | `convert ${input}/${base} -resize '${size}' -unsharp 1x4 "${output}/icon-${size}.png"` 57 | ); 58 | }); 59 | 60 | console.log(`\n> copying ${chalk.blue.bold('base')} icon`); 61 | shell.cp(`${input}/${base}`, `${output}/${base}`); 62 | 63 | if (background) { 64 | console.log(`> copying ${chalk.blue.bold('android adaptive')} elements`); 65 | shell.cp(`${input}/${background}`, `${output}/${background}`); 66 | shell.cp(`${input}/${foreground}`, `${output}/${foreground}`); 67 | } 68 | 69 | if (desktop) { 70 | console.log(`> copying ${chalk.blue.bold('desktop')} icon`); 71 | shell.cp(`${input}/${desktop}`, `${output}/${desktop}`); 72 | } 73 | 74 | console.log(`\n Created ${chalk.green(SIZES.length)} icons ${chalk.green('successfully')}.`); 75 | }; 76 | 77 | // ----------------------------------------------------------------------------- 78 | 79 | export default generateIcons; 80 | -------------------------------------------------------------------------------- /fox/animations/framer.gd: -------------------------------------------------------------------------------- 1 | extends Tween 2 | 3 | # ------------------------------------------------------------------------------ 4 | 5 | var nextAnimation 6 | 7 | # ------------------------------------------------------------------------------ 8 | 9 | func _ready(): 10 | connect('finished',Callable(self,'onAnimationfinished')) 11 | 12 | # ------------------------------------------------------------------------------ 13 | 14 | func animateFrames( 15 | fromFrame: int, 16 | toFrame: int, 17 | reverse: bool = false, 18 | maxNbFrames: int = 0, 19 | _totalDuration = 0.3, 20 | _duration = null, 21 | # delay = 0 22 | ): 23 | 24 | nextAnimation = null 25 | var from = fromFrame 26 | var to = toFrame 27 | var nbFrames = abs(toFrame - fromFrame) 28 | 29 | # queueing fromFrame -> 32 and 0 -> toFrame ------------- 30 | if((not reverse) and maxNbFrames and fromFrame > toFrame): 31 | to = maxNbFrames - 1 32 | nbFrames = abs(toFrame + maxNbFrames - fromFrame) 33 | nextAnimation = { 34 | from = 0, 35 | } 36 | 37 | # queueing fromFrame -> 0 and 32 -> toFrame ------------ 38 | if(reverse and maxNbFrames and toFrame > fromFrame): 39 | to = 0 40 | nbFrames = abs(toFrame + maxNbFrames - fromFrame) 41 | nextAnimation = { 42 | from = maxNbFrames - 1, 43 | } 44 | 45 | # ----------- 46 | 47 | var duration = _duration 48 | 49 | if(not _duration): 50 | if(nbFrames == 0): 51 | duration = 0 52 | else: 53 | duration = _totalDuration * abs(to - from) / nbFrames 54 | 55 | # ----------- 56 | 57 | if(nextAnimation): 58 | nextAnimation.to = toFrame 59 | nextAnimation.reverse = reverse 60 | nextAnimation.maxNbFrames = maxNbFrames 61 | nextAnimation.duration = _totalDuration - duration 62 | nextAnimation.totalDuration = _totalDuration 63 | 64 | # ----------- 65 | # 🏗️ note as of refacting with Godot4: Tween is no longer a Node 66 | # get_parent() is irrelevant; there is surely a better way to do implement this frame animation 67 | # unplugging this for now / lockey land is not working anymore with this update. 68 | 69 | # if(is_running()): 70 | # stop(get_parent(), 'frame') 71 | 72 | # # ----------- 73 | 74 | # interpolate_property( 75 | # get_parent(), 76 | # 'frame', 77 | # from, to, 78 | # duration, 79 | # Tween.TRANS_LINEAR, Tween.EASE_OUT, 80 | # delay 81 | # ) 82 | 83 | play() 84 | 85 | # ------------------------------------------------------------------------------ 86 | 87 | func onAnimationfinished(_object, _key): 88 | if(nextAnimation): 89 | animateFrames( 90 | nextAnimation.from, 91 | nextAnimation.to, 92 | nextAnimation.reverse, 93 | nextAnimation.maxNbFrames, 94 | nextAnimation.duration 95 | ) 96 | -------------------------------------------------------------------------------- /cli/run-game.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | 3 | import chalk from 'chalk'; 4 | import chokidar from 'chokidar'; 5 | import shelljs from 'shelljs'; 6 | import {spawn} from 'child_process'; 7 | 8 | import keypress from 'keypress'; 9 | 10 | // ----------------------------------------------------------------------------- 11 | 12 | let childProcess = null; 13 | 14 | // ----------------------------------------------------------------------------- 15 | 16 | const restart = (godotPath, params, config) => { 17 | shelljs.exec('clear'); 18 | 19 | if (childProcess) { 20 | childProcess.kill(); 21 | } 22 | 23 | start(godotPath, params, config); 24 | } 25 | 26 | // ----------------------------------------------------------------------------- 27 | 28 | const start = (godotPath, params, config) => { 29 | console.log('============================================================'); 30 | console.log(`🦊 ${chalk.italic('restarting Godot')}`); 31 | console.log(`⚙️ running ${chalk.blue.bold('game')}`); 32 | console.log('============================================================'); 33 | var {position, screen} = config; 34 | 35 | const parameters = [...params]; 36 | 37 | if (screen) { 38 | parameters.push(['--screen', screen]); 39 | } 40 | else{ 41 | parameters.push(['--position', position]); 42 | } 43 | 44 | childProcess = spawn(godotPath, parameters, {stdio: 'inherit'}); 45 | 46 | }; 47 | 48 | // ----------------------------------------------------------------------------- 49 | 50 | const runGame = (godotPath, params, config) => { 51 | keypress(process.stdin); 52 | 53 | process.stdin.setRawMode(true); 54 | 55 | const watcher = chokidar.watch('.', { 56 | ignored: (path, stats) => { 57 | if (!stats) return false; // Si stats est null (par exemple au démarrage), ne pas ignorer 58 | 59 | const validExtensions = ['.gd', '.tscn', '.cfg', '.json', '.yml']; 60 | const isWantedFile = validExtensions.some(ext => path.endsWith(ext)); 61 | 62 | const isInGodotFolder = path.includes('.godot/'); 63 | 64 | return stats.isFile() && (!isWantedFile || isInGodotFolder); 65 | } 66 | }); 67 | 68 | start(godotPath, params, config); 69 | 70 | watcher.on('change', (path, stats) => { 71 | restart(godotPath, params, config); 72 | }); 73 | 74 | process.stdin.on('keypress', (ch, key) => { 75 | if(!key) { 76 | return 77 | } 78 | 79 | if(key.name === 'r') { 80 | restart(godotPath, params, config); 81 | } 82 | 83 | if(key.name === 'c' && key.ctrl === true) { 84 | console.log('🦊 bye'); 85 | 86 | if (childProcess) { 87 | childProcess.kill(); 88 | } 89 | 90 | process.exit(0); 91 | } 92 | }); 93 | }; 94 | 95 | // ----------------------------------------------------------------------------- 96 | 97 | export default runGame; 98 | -------------------------------------------------------------------------------- /fox/stores/appstore.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | 3 | extends Node 4 | 5 | # ------------------------------------------------------------------------------ 6 | # plugin: https://github.com/godotengine/godot-ios-plugins/blob/master/plugins/inappstore/README.md 7 | # ------------------------------------------------------------------------------ 8 | # You must use sandbox users to test IAP: https://appstoreconnect.apple.com/access/testers 9 | # connect/logout sandbox users: Reglages > AppStore > sandbox user 10 | # ------------------------------------------------------------------------------ 11 | 12 | var appStore 13 | 14 | # ------------------------------------------------------------------------------ 15 | 16 | signal skuDetailsReceived 17 | 18 | # ------------------------------------------------------------------------------ 19 | 20 | func _ready(): 21 | if Engine.has_singleton('InAppStore'): 22 | G.log('-------------------------------') 23 | G.log('✅ AppStore starting: InAppStore was found') 24 | appStore = Engine.get_singleton('InAppStore') 25 | fetchItems() 26 | 27 | # ------------------------------------------------------------------------------ 28 | 29 | ## looping here when there is a process, to wait for StoreKit results 30 | func checkEvents(): 31 | if appStore.get_pending_event_count() > 0: 32 | var event = appStore.pop_pending_event() 33 | 34 | if event.result == 'ok': 35 | match event.type: 36 | 'product_info': 37 | G.log({event=event}); 38 | for i in event.ids.size(): 39 | var sku = event.ids[i] 40 | var price = event.localized_prices[i] 41 | var item = G.STORE[sku] 42 | item.price = price 43 | 44 | skuDetailsReceived.emit() 45 | return 46 | 47 | 'purchase': 48 | var sku = event.product_id 49 | Router.hideLoader() 50 | Player.bought(sku) 51 | return 52 | 53 | # other possible values are 'progress', 'error', 'unhandled' 54 | _: 55 | G.log({event=event}) 56 | 57 | # user cancelled or could not pay 58 | elif event.result == 'error': 59 | Router.hideLoader() 60 | return 61 | 62 | waitAWhileAndCheckEvents() 63 | 64 | # ------------------------------------------------------------------------------ 65 | 66 | func purchase(sku): 67 | G.log('purchasing ', sku); 68 | Router.showLoader() 69 | appStore.purchase({product_id = sku}) 70 | waitAWhileAndCheckEvents() 71 | 72 | # ------------------------------------------------------------------------------ 73 | 74 | func fetchItems(): 75 | var skus = G.STORE.keys() 76 | var result = appStore.request_product_info( { 'product_ids': skus } ) 77 | if result == OK: 78 | appStore.set_auto_finish_transaction(true) 79 | waitAWhileAndCheckEvents() 80 | 81 | else: 82 | G.log('🔴 [AppStore] failed requesting product info') 83 | 84 | # ------------------------------------------------------------------------------ 85 | 86 | func waitAWhileAndCheckEvents(): 87 | await Wait.forSomeTime(self, 2).timeout 88 | checkEvents() 89 | -------------------------------------------------------------------------------- /fox/behaviours/multitouchArea.gd: -------------------------------------------------------------------------------- 1 | extends Area2D 2 | 3 | @onready var collisionShape = $CollisionShape2D 4 | 5 | @export var fullscreen:bool = false 6 | 7 | # ------------------------------------------------------------------------------ 8 | 9 | signal pressed(event) 10 | signal pressing(event) 11 | signal dragging(event) 12 | signal stopPressing(event) 13 | signal longPress(latestPressEvent) 14 | 15 | # ------------------------------------------------------------------------------ 16 | 17 | @export var longPressTime: int = 500 18 | 19 | # ------------------------------------------------------------------------------ 20 | 21 | var lastPress = Time.get_ticks_msec() 22 | var latestPressEvent 23 | 24 | # ------------------------------------------------------------------------------ 25 | 26 | var _pressing = false 27 | var isLongPressing = false 28 | 29 | # ------------------------------------------------------------------------------ 30 | 31 | func _ready(): 32 | if(fullscreen): 33 | fillScreenDimensions() 34 | $/root.connect('size_changed', fillScreenDimensions) 35 | 36 | func fillScreenDimensions(): 37 | collisionShape.shape.size = Vector2(G.W, G.H) 38 | collisionShape.position = Vector2(G.W / 2.0, G.H / 2.0) 39 | 40 | # ------------------------------------------------------------------------------ 41 | 42 | func formatEvent(event): 43 | return { 44 | position = __.Get('position', event), 45 | pressed = __.Get('pressed', event), 46 | index = __.GetOr(__.GetOr(__.Get( 47 | 'button_mask', event), 48 | 'button_index', event), 49 | 'index', event) 50 | } 51 | 52 | # ------------------------------------------------------------------------------ 53 | 54 | func _input(_event): 55 | var event = formatEvent(_event) 56 | 57 | if _event is InputEventMouseButton \ 58 | and _event.button_index == MOUSE_BUTTON_LEFT: 59 | if(event.pressed): 60 | pressing.emit(event) 61 | lastPress = Time.get_ticks_msec() 62 | latestPressEvent = event 63 | else: 64 | isLongPressing = false 65 | pressed.emit(event) 66 | stopPressing.emit(event) 67 | 68 | _pressing = event.pressed 69 | 70 | elif _event is InputEventScreenTouch: 71 | if(event.pressed): 72 | pressing.emit(event) 73 | lastPress = Time.get_ticks_msec() 74 | latestPressEvent = event 75 | else: 76 | isLongPressing = false 77 | pressed.emit(event) 78 | stopPressing.emit(event) 79 | 80 | elif(_event is InputEventMouseMotion): 81 | if(_pressing): 82 | dragging.emit(event) 83 | 84 | elif(_event is InputEventScreenDrag): 85 | if(_pressing): 86 | dragging.emit(event) 87 | 88 | else: 89 | G.debug('⚡ event:', _event) 90 | 91 | # ------------------------------------------------------------------------------ 92 | 93 | func _physics_process(_delta): 94 | if _pressing: 95 | var now = Time.get_ticks_msec() 96 | var elapsedTime = now - lastPress 97 | 98 | if(not isLongPressing and elapsedTime > longPressTime): 99 | isLongPressing = true 100 | longPress.emit(latestPressEvent) 101 | -------------------------------------------------------------------------------- /cli/bundler/versioning.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | 3 | import chalk from 'chalk'; 4 | import path from 'path'; 5 | import shell from 'shelljs'; 6 | import { updateVersionInPreset } from "./update-preset.js"; 7 | import { PRESETS_CFG, writePresets } from './read-presets.js'; 8 | 9 | // ----------------------------------------------------------------------------- 10 | 11 | const getNextVersion = (currentVersion, versionLevel) => { 12 | const [major, minor, patch] = currentVersion.split('.'); 13 | 14 | switch(versionLevel) { 15 | case 'major': 16 | return `${parseInt(major) + 1}.0.0`; 17 | case 'minor': 18 | return `${major}.${parseInt(minor) + 1}.0`; 19 | case 'patch': 20 | return `${major}.${minor}.${parseInt(patch) + 1}`; 21 | } 22 | } 23 | 24 | // ----------------------------------------------------------------------------- 25 | // from https://github.com/chrisdugne/cherry/blob/master/cherry/libs/version-number.lua 26 | // ----------------------------------------------------------------------------- 27 | /* 28 | console.log('1.2.3', toVersionNumber('1.2.3')); --> 10203 29 | console.log('1.2.32', toVersionNumber('1.2.32')); --> 10232 30 | console.log('12.2.32', toVersionNumber('12.2.32')); --> 120232 31 | console.log('12.24.32', toVersionNumber('12.24.32')); --> 122432 32 | console.log('1.2', toVersionNumber('1.2')); --> 10200 33 | console.log('1', toVersionNumber('1')); --> 10000 34 | console.log(12, toVersionNumber(12)); --> 0 35 | console.log('undefined', toVersionNumber()); --> 0 36 | console.log(null, toVersionNumber(null)); --> 0 37 | console.log('whatever.not.number', toVersionNumber('whatever.not.number')); --> 0 38 | */ 39 | const toVersionNumber = (semver) => { 40 | if (!semver) return 0; 41 | if (typeof semver !== 'string') return 0; 42 | 43 | const splinters = semver.split('.'); 44 | 45 | const code = splinters.reduce((acc, splinter) => acc + splinter.padStart(2, '0'), ''); 46 | const number = parseInt(code.padEnd(6, 0)) || 0; 47 | 48 | return number; 49 | }; 50 | 51 | // ----------------------------------------------------------------------------- 52 | 53 | const increasePackageVersion = (newVersion, versionLevel) => { 54 | console.log(`⚙️ npm version ${chalk.blue.bold(versionLevel)}`); 55 | 56 | try { 57 | console.log('git add'); 58 | shell.exec(`git add ${path.resolve(PRESETS_CFG)}`); 59 | shell.exec(`git commit -m "bump presets version to ${newVersion}"`); 60 | shell.exec(`npm version ${versionLevel}`); 61 | } catch (e) { 62 | console.log(e); 63 | console.log(chalk.red.bold('🔴 failed during versioning, check "git status"')); 64 | return; 65 | } 66 | }; 67 | 68 | // ----------------------------------------------------------------------------- 69 | 70 | const increasePresetsVersion = (newVersion, presets) => { 71 | Object.keys(presets).forEach(num => { 72 | updateVersionInPreset(presets[num], newVersion); 73 | }) 74 | 75 | writePresets(presets); 76 | }; 77 | 78 | // ----------------------------------------------------------------------------- 79 | 80 | export { 81 | getNextVersion, 82 | toVersionNumber, 83 | increasePackageVersion, 84 | increasePresetsVersion 85 | }; 86 | -------------------------------------------------------------------------------- /docs/gdscript/interactive-area-2d.md: -------------------------------------------------------------------------------- 1 | # Interactive Area2D 2 | 3 | This is an `Area2D` able to listen for mouse / touch events. 4 | The Node where this interactive `Area2D` is attached can be touched, dragged, used as a drop area. 5 | 6 | An `interactiveArea2D` uses [Gesture](../../fox/libs/gesture.gd) behind the hood. 7 | 8 | 9 | ## Setup 10 | 11 | - Attach the [interactiveArea2D](../../fox/behaviours/interactiveArea2D.tscn) **Scene** as child to your Node. 12 | 13 | - Add a `CollisionShape2D` to the `interactiveArea` Node and set its shape e.g(`RectangleShape2D`). 14 | 15 | - Set your shape size and position. 16 | 17 | **important note**: be sure you have no `Control` in the parent tree, it could intercept the events. Or You can also set `mouse: ignore` to the `Control` to avoid it to intercept the events. 18 | 19 | Read the doc about [Godot Input Events](https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html#how-does-it-work) 20 | 21 | ## Simple dragArea 22 | 23 | To drag an image around, create a `Sprite2D` and attach the **scene** `interactiveArea2D` to it. 24 | 25 | Then connect the `dragged` signal using this . 26 | 27 | ```gdscript 28 | @onready var sprite2D = $sprite2D 29 | @onready var dragArea = $sprite2D/interactiveArea2D 30 | 31 | func _ready(): 32 | dragArea.prepareDraggable({ 33 | draggable = sprite2D 34 | }) 35 | ``` 36 | 37 | ## Boundaries, for maps for example 38 | 39 | You can add boundaries to limit the dragging to the window size. 40 | To do so, you need to position your Sprite2D at `(0,0)` 41 | 42 | Then just use your sprite to define the boundaries size. 43 | 44 | ```gdscript 45 | dragArea.prepareDraggable({ 46 | draggable = sprite2D, 47 | useBoundaries = sprite2D 48 | }) 49 | ``` 50 | 51 | You may want to define the boundaries using another Rectangle instead: change `useBoundaries = yourNode`. 52 | 53 | ## API 54 | 55 | Declare the `area` in your script to allow connecting listeners: 56 | 57 | ```gdscript 58 | @onready var interactiveArea2D = $interactiveArea2D 59 | ``` 60 | 61 | Now you can attach listeners to the `touchable` and `draggable` areas: 62 | 63 | ```gdscript 64 | func _ready(): 65 | interactiveArea2D.connect('dragged', _dragged) 66 | interactiveArea2D.connect('startedDragging', _startedDragging) 67 | interactiveArea2D.connect('press', pressed) 68 | 69 | # -------------------------------------- 70 | 71 | func _dragged(_position): 72 | G.log('dragged', name, _position); 73 | 74 | func _startedDragging(): 75 | G.log('started dragging', name); 76 | 77 | func pressed(): 78 | G.log('pressed on', name); 79 | ``` 80 | 81 | ## Dragging 82 | 83 | To drag your Node needs few more settings: you need to tell it what Node is to be dragged, and what parent it's dragged on, to apply the parent scale to the new positions. 84 | 85 | ```gdscript 86 | interactiveArea2D.draggable = self 87 | interactiveArea2D.parentReference = get_parent() 88 | ``` 89 | 90 | By default the dragging starts immediately but you can ask to start after a `longPress` 91 | 92 | ```gdscript 93 | interactiveArea2D.afterLongPress = true 94 | ``` 95 | 96 | Also, you may have scaled a parent. If so, you need to tell the `interactiveArea2D` what scale to apply to calculate the dragged position. 97 | 98 | ```gdscript 99 | var parent = $your/scaled/parent 100 | interactiveArea2D.zoom = parent.scale.x 101 | ``` 102 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Fox 2 | 3 | [![License](https://img.shields.io/badge/License-MIT-green.svg?colorB=3cc712)](license) [![version](https://img.shields.io/github/package-json/v/uralys/fox)](https://github.com/uralys/fox/tags) 4 | 5 | 🦊 Fox provides tooling while developing with Godot Engine. 6 | 7 |

8 | 9 | ## Scenes and scripts 10 | 11 | With Fox, you can use `Scenes`, `Resources`, scripts and static functions to build your app. 12 | 13 | As an example, this code will move 3 nodes to the same position, with a delay of 1 second between each animation. Finally it fill print 'DONE' in the console. 14 | 15 | ```gdscript 16 | Animate.to([potion, car, book], { 17 | propertyPath = "position", 18 | toValue = Vector2(0, 0), 19 | delayBetweenElements = 1, 20 | onFinished = func(): 21 | G.log('DONE'); 22 | }) 23 | ``` 24 | 25 | This other one sends a body to a REST API, handles and logs the result while showing a loader: 26 | 27 | ```gd 28 | Router.showLoader() 29 | 30 | HTTP.Post(self, { 31 | endpoint = "/score", 32 | body = {playerId = "FieryFox", score = 100}, 33 | onError = func(_result, _response_code, _headers, _body): 34 | handleScoreFailure() 35 | Router.hideLoader() 36 | , 37 | onComplete = func(_result, _response_code, _headers, body): 38 | var _body = body.get_string_from_utf8() 39 | var newRecord = __.GetOr(false, 'newRecord', _body) 40 | G.debug( 41 | '✅ [b][color=green]successfully posted score[/color][/b]', 42 | {newRecord = newRecord} 43 | ) 44 | Router.hideLoader() 45 | }) 46 | ``` 47 | 48 | ## Documentation 49 | 50 | Few documentation links (find more in the [docs](./docs)): 51 | 52 | - [Installing](./docs/install.md) Fox to use in your Godot app 53 | 54 | Coding: 55 | 56 | - Using the [Router](./docs/gdscript/router.md) 57 | - Using [Animation](./docs/gdscript/animations.md) Tween helpers 58 | - Using `Touchable` and `Draggable` Nodes with an [interactiveArea2D](./docs/gdscript/interactive-area-2d.md) behaviour on any Node 59 | - Using [Popups](./docs/gdscript/popups.md) 60 | - Using [DraggableCamera](./docs/gdscript/draggable-camera.md) 61 | - Using [Sound](./docs/gdscript/sound.md) 62 | - static functions inspired by [Underscore](/fox/libs/underscore.gd) 63 | 64 | Exporting: 65 | 66 | - [Installing the CLI](./docs/cli.md) 67 | - Info about [Android](./docs/exporting/android.md) settings and building 68 | - Info about [iOS](./docs/exporting/ios.md) settings and building 69 | 70 | ## Games created with Fox 71 | 72 | xoozz 73 | battle-squares 74 | avindi 75 | lockeyland 76 | lockeyland 77 | battle-squares 78 | -------------------------------------------------------------------------------- /fox/core/app.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | 3 | extends CanvasLayer 4 | 5 | # ------------------------------------------------------------------------------ 6 | 7 | var IntroAnimation = preload('res://fox/animations/intro-animation.tscn') 8 | var NotificationsScheduler = preload('res://fox/behaviours/notifications.tscn') 9 | 10 | # ------------------------------------------------------------------------------ 11 | 12 | # your app must call super._ready() to initialize the game using: 13 | # ```func _ready(): 14 | # super._ready() 15 | # loadApp() 16 | # ``` 17 | func _ready(): 18 | DEBUG.setup() 19 | createScreenReference() 20 | prepareNotifications() 21 | randomize() # https://docs.godotengine.org/en/latest/tutorials/math/random_number_generation.html#the-randomize-method 22 | 23 | # ------------------------------------------------------------------------------ 24 | 25 | func startIntroAnimation(): 26 | if(DEBUG.NO_INTRO_ANIMATION): 27 | return 28 | 29 | var intro = IntroAnimation.instantiate() 30 | add_child(intro) 31 | 32 | # may await intro.introFinished 33 | return intro 34 | 35 | # ------------------------------------------------------------------------------ 36 | 37 | func createScreenReference(): 38 | var screenReference = ReferenceRect.new() 39 | screenReference.name = 'screenReference' 40 | 41 | screenReference.position = Vector2(0, 0) 42 | screenReference.mouse_filter = Control.MOUSE_FILTER_IGNORE 43 | screenReference.anchors_preset = Control.PRESET_FULL_RECT 44 | screenReference.anchor_right = Control.PRESET_FULL_RECT 45 | screenReference.anchor_right = 1.0 46 | screenReference.anchor_bottom = 1.0 47 | screenReference.grow_horizontal = Control.GROW_DIRECTION_BOTH 48 | screenReference.grow_vertical = Control.GROW_DIRECTION_BOTH 49 | 50 | $hud.add_child(screenReference) 51 | recordScreenDimensions() 52 | $/root.connect('size_changed', recordScreenDimensions) 53 | 54 | func recordScreenDimensions(): 55 | var screenReference = $/root/app/hud/screenReference 56 | G.W = screenReference.get_rect().size.x 57 | G.H = screenReference.get_rect().size.y 58 | G.SCREEN_CENTER = Vector2(G.W /2.0, G.H /2.0) 59 | 60 | # ------------------------------------------------------------------------------ 61 | 62 | var scheduler 63 | 64 | func prepareNotifications(): 65 | var useNotifications = ProjectSettings.get_setting('custom/useNotifications') 66 | if(not useNotifications): 67 | return 68 | 69 | scheduler = NotificationsScheduler.instantiate() 70 | scheduler.init() 71 | $/root.add_child.call_deferred(scheduler) 72 | 73 | call_deferred('_configureScheduler') 74 | 75 | # ------------------------------------------------------------------------------ 76 | 77 | func _configureScheduler(): 78 | var h = scheduler.has_post_notifications_permission() 79 | G.log('has_post_notifications_permission:', h); 80 | 81 | # if(not h): 82 | # scheduler.request_post_notifications_permission() 83 | 84 | # var chanId = 'CHAN_BATTLE_ID' 85 | # scheduler.create_notification_channel(chanId, "My Channel Name", "My channel description") 86 | 87 | # var my_notification_data = NotificationData.new() 88 | # my_notification_data.set_id(1)\ 89 | # .set_channel_id(chanId)\ 90 | # .set_title("Youhou!")\ 91 | # .set_content("Time to gift")\ 92 | # .set_small_icon_name("notification_icon") 93 | 94 | # G.log('scheduling', {my_notification_data=my_notification_data} ); 95 | # scheduler.schedule(my_notification_data, 7) 96 | -------------------------------------------------------------------------------- /docs/exporting/images.md: -------------------------------------------------------------------------------- 1 | # Images generation for release 2 | 3 | - create a `_release/images` folder 4 | - add a `.gdignore` file to `_release` 5 | 6 | ## Icons 7 | 8 | use `fox/assets/android/adaptive_icon_template.afdesign` at your convenience, to generate these files: 9 | 10 | - android adaptive: use `adaptive` artboard, hide parts for foreground/background, export 1000x1000 11 | - icon 1200x1200: use `ios` artboard 12 | - icon 512x512: use `ios` artboard 13 | - icon desktop 512x512: use `desktop` artboard 14 | 15 | export using 16 | 17 | ```sh 18 | fox generate:icons 19 | ``` 20 | 21 | ### iOS 22 | 23 | once exported you can fill the paths in the iOS export section: 24 | 25 | ```ini 26 | icons/iphone_120x120="res://assets/generated/icons/icon-120x120.png" 27 | icons/iphone_180x180="res://assets/generated/icons/icon-180x180.png" 28 | icons/ipad_76x76="res://assets/generated/icons/icon-76x76.png" 29 | icons/ipad_152x152="res://assets/generated/icons/icon-152x152.png" 30 | icons/ipad_167x167="res://assets/generated/icons/icon-167x167.png" 31 | icons/app_store_1024x1024="res://assets/generated/icons/icon-1024x1024.png" 32 | icons/spotlight_40x40="res://assets/generated/icons/icon-40x40.png" 33 | icons/spotlight_80x80="res://assets/generated/icons/icon-80x80.png" 34 | icons/settings_58x58="res://assets/generated/icons/icon-58x58.png" 35 | icons/settings_87x87="res://assets/generated/icons/icon-87x87.png" 36 | icons/notification_40x40="res://assets/generated/icons/icon-40x40.png" 37 | icons/notification_60x60="res://assets/generated/icons/icon-60x60.png" 38 | ``` 39 | 40 | ### android 41 | 42 | more info for android: 43 | 44 | ## Splashscreens 45 | 46 | - generate a `_release/images/base-splashscreen.png` and extends its dimension using 47 | 48 | ```sh 49 | fox generate:splashscreens 50 | ``` 51 | 52 | once it's done you can fill the paths in the iOS export section: 53 | 54 | ```ini 55 | landscape_launch_screens/iphone_2436x1125="res://assets/generated/splashscreens/splashscreen-2436x1125.png" 56 | landscape_launch_screens/iphone_2208x1242="res://assets/generated/splashscreens/splashscreen-2208x1242.png" 57 | landscape_launch_screens/ipad_1024x768="res://assets/generated/splashscreens/splashscreen-1024x768.png" 58 | landscape_launch_screens/ipad_2048x1536="res://assets/generated/splashscreens/splashscreen-2048x1536.png" 59 | portrait_launch_screens/iphone_640x960="res://assets/generated/splashscreens/splashscreen-640x960.png" 60 | portrait_launch_screens/iphone_640x1136="res://assets/generated/splashscreens/splashscreen-640x1136.png" 61 | portrait_launch_screens/iphone_750x1334="res://assets/generated/splashscreens/splashscreen-750x1334.png" 62 | portrait_launch_screens/iphone_1125x2436="res://assets/generated/splashscreens/splashscreen-1125x2436.png" 63 | portrait_launch_screens/ipad_768x1024="res://assets/generated/splashscreens/splashscreen-768x1024.png" 64 | portrait_launch_screens/ipad_1536x2048="res://assets/generated/splashscreens/splashscreen-1536x2048.png" 65 | portrait_launch_screens/iphone_1242x2208="res://assets/generated/splashscreens/splashscreen-1242x2208.png" 66 | ``` 67 | 68 | ## Screenshots 69 | 70 | Take screenshots from your app, then use this command to generate all resized resolutions: 71 | 72 | ```sh 73 | fox generate:screenshots 74 | ``` 75 | 76 | Default orientation is `landscape`, if you need `portrait` add this in your `fox.config.json`: 77 | 78 | ```json 79 | "generate:screenshots": { 80 | "orientation": "portrait" 81 | } 82 | ``` 83 | -------------------------------------------------------------------------------- /fox/animations/splash-animation.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # ------------------------------------------------------------------------------ 4 | 5 | signal splashFinished 6 | 7 | # ------------------------------------------------------------------------------ 8 | 9 | const STEP_DURATION = 0.75 10 | var blurring = false 11 | 12 | # ------------------------------------------------------------------------------ 13 | 14 | @onready var blur = $blur 15 | @onready var logo = $logo 16 | @onready var letters = $letters 17 | 18 | @onready var U = $letters/u 19 | @onready var R = $letters/r 20 | @onready var A = $letters/a 21 | @onready var L = $letters/l 22 | @onready var Y = $letters/y 23 | @onready var S = $letters/s 24 | @onready var DOT = $letters/dot 25 | 26 | # ------------------------------------------------------------------------------ 27 | 28 | # Called when the node enters the scene tree for the first time. 29 | func _ready(): 30 | G.log('> splashScreen'); 31 | G.log('-------------------------------') 32 | var appearDuration = 0.75 33 | var appearDelay = 0.2 34 | 35 | letters.hide() 36 | Animate.show(logo, 2) 37 | 38 | blur.material.set_shader_parameter('blur_amount', 0) 39 | 40 | await Wait.forSomeTime(self, 0.6).timeout 41 | letters.show() 42 | blurring = true 43 | 44 | Animate.show(U, appearDuration, appearDelay) 45 | Animate.show(R, appearDuration, appearDelay) 46 | Animate.show(A, appearDuration, appearDelay) 47 | Animate.show(L, appearDuration, appearDelay) 48 | Animate.show(Y, appearDuration, appearDelay) 49 | Animate.show(S, appearDuration, appearDelay) 50 | Animate.show(DOT, appearDuration, appearDelay) 51 | 52 | var delay = appearDuration + appearDelay + 0.1 53 | await Wait.forSomeTime(self, delay).timeout 54 | 55 | # ------------------- UR 56 | 57 | Animate.hide(U, STEP_DURATION, .6) 58 | Animate.hide(R, STEP_DURATION, .2) 59 | 60 | # ------------------- A 61 | 62 | Animate.to(A, { 63 | propertyPath = 'position', 64 | toValue = Vector2( 65 | A.get_parent().size.x * 0.5, 66 | A.get_parent().size.y * 0.5 67 | ), 68 | duration = STEP_DURATION + 1, 69 | easing = Tween.EASE_IN_OUT, 70 | delay = 0.3 71 | }) 72 | 73 | Animate.to(A, { 74 | propertyPath = 'scale', 75 | toValue = A.scale * 3, 76 | duration = STEP_DURATION + 1, 77 | easing = Tween.EASE_IN_OUT, 78 | delay = 0.3, 79 | signalToWait = 'scaled' 80 | }) 81 | 82 | # ------------------- LYS. 83 | 84 | Animate.hide(L, STEP_DURATION, 0.5) 85 | Animate.hide(Y, STEP_DURATION, .3) 86 | Animate.hide(S, STEP_DURATION, 0.7) 87 | Animate.hide(DOT, STEP_DURATION, .8) 88 | 89 | Animate.to(logo, { 90 | propertyPath = 'scale', 91 | toValue = Vector2(0.3, 0.3), 92 | duration = 4, 93 | easing = Tween.EASE_IN 94 | }) 95 | 96 | await Signal(A, 'scaled') 97 | Animate.hide(A, 1.2, 0.3) 98 | Animate.hide(logo, 2) 99 | await Wait.forSomeTime(self, 1).timeout 100 | exitSplashAnimation() 101 | 102 | # ------------------------------------------------------------------------------ 103 | 104 | func _physics_process(delta): 105 | if(blurring): 106 | var current = blur.material.get_shader_parameter('blur_amount') 107 | var newValue = current - delta * 15 108 | blur.material.set_shader_parameter('blur_amount', newValue) 109 | 110 | # ------------------------------------------------------------------------------ 111 | 112 | func exitSplashAnimation(): 113 | splashFinished.emit() 114 | get_parent().remove_child(self) 115 | queue_free() 116 | -------------------------------------------------------------------------------- /fox/core/sound.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | 3 | extends Node 4 | 5 | # ------------------------------------------------------------------------------ 6 | 7 | var ___node 8 | var _verbose = true 9 | 10 | # ------------------------------------------------------------------------------ 11 | 12 | var CURRENT_MUSIC_CURSOR = -1 13 | var CURRENT_MUSIC 14 | var MUSIC_ON 15 | var SOUNDS_ON 16 | 17 | var OGG 18 | 19 | # ------------------------------------------------------------------------------ 20 | 21 | var BUTTON_PRESS = "onButtonPress" 22 | 23 | # ------------------------------------------------------------------------------ 24 | 25 | func init(musicOn = true, soundsOn = true): 26 | OGG = self.oggFiles 27 | MUSIC_ON = musicOn 28 | SOUNDS_ON = soundsOn 29 | 30 | ___node = Node.new() 31 | ___node.process_mode = PROCESS_MODE_ALWAYS 32 | $'/root/app'.add_child(___node) 33 | 34 | # ------------------------------------------------------------------------------ 35 | 36 | func playMusicsInLoop(options): 37 | var delay = __.GetOr(0, 'delay', options) 38 | if(delay > 0): 39 | await Wait.forSomeTime(___node, delay).timeout 40 | 41 | CURRENT_MUSIC_CURSOR = (CURRENT_MUSIC_CURSOR+1) % Sound.MUSICS.size() 42 | var musicName = Sound.MUSICS[CURRENT_MUSIC_CURSOR] 43 | await playMusic(musicName) 44 | 45 | # CURRENT_MUSIC.seek(145) # to debug .ogg encoding 46 | 47 | if(CURRENT_MUSIC): 48 | CURRENT_MUSIC.connect('finished', func(): 49 | stopMusic() 50 | playMusicsInLoop(options) 51 | ) 52 | 53 | # ------------------------------------------------------------------------------ 54 | 55 | func playMusic(musicName, delay = 0): 56 | CURRENT_MUSIC = await _play(musicName, delay) 57 | _refreshMusicVolume() 58 | 59 | # ------------------------------------------------------------------------------ 60 | 61 | func play(soundName, delay = 0): 62 | if(SOUNDS_ON): 63 | _play(soundName, delay) 64 | 65 | # ------------------------------------------------------------------------------ 66 | 67 | func stopMusic(): 68 | if(not CURRENT_MUSIC): 69 | return 70 | 71 | CURRENT_MUSIC.stop() 72 | ___node.remove_child(CURRENT_MUSIC) 73 | CURRENT_MUSIC.queue_free() 74 | CURRENT_MUSIC = null 75 | 76 | # ------------------------------------------------------------------------------ 77 | 78 | func _refreshMusicVolume(): 79 | if(not CURRENT_MUSIC): 80 | return 81 | 82 | var volume = 0 if(MUSIC_ON) else -100 83 | CURRENT_MUSIC.set_volume_db(volume) 84 | 85 | func toggleSounds(): 86 | SOUNDS_ON = not SOUNDS_ON 87 | 88 | func toggleMusic(): 89 | MUSIC_ON = not MUSIC_ON 90 | _refreshMusicVolume() 91 | 92 | func isSoundsOn(): 93 | return SOUNDS_ON 94 | 95 | func isMusicOn(): 96 | return MUSIC_ON 97 | 98 | # ------------------------------------------------------------------------------ 99 | 100 | func _play(soundName, delay = 0): 101 | if(delay > 0): 102 | await Wait.forSomeTime(___node, delay).timeout 103 | 104 | if(_verbose):G.debug('[Sound] playing', soundName, 'with delay', delay) 105 | 106 | var assetPath =__.Get(soundName, OGG) 107 | if(assetPath): 108 | if(DEBUG.SOUND_OFF): 109 | G.debug('🎵 >> DEBUG.SOUND_OFF [', soundName, ']'); 110 | else: 111 | return _playStream(assetPath) 112 | else: 113 | if(_verbose):G.debug('[Sound] ❌ sound [', soundName, '] has no super.ogg'); 114 | 115 | # ------------------------------------------------------------------------------ 116 | 117 | func _playStream(path): 118 | var sound = AudioStreamPlayer.new() 119 | sound.process_mode = PROCESS_MODE_ALWAYS 120 | 121 | var stream = load(path) 122 | sound.stream = stream 123 | ___node.add_child(sound) 124 | 125 | sound.play() 126 | 127 | return sound 128 | 129 | # ------------------------------------------------------------------------------ 130 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # 📦 Installing Fox 2 | 3 | ## starting from scratch 4 | 5 | ### 1 - New Godot Project 6 | 7 | Start by opening Godot Editor and create a new project with `Godot > New Project > Create folder >` `your-game` 8 | 9 | Then > `Select Current Folder` 10 | 11 | Edit your project settings and `Create & Edit` 12 | 13 | ### 2 - Clone this repo next to `your-game` 14 | 15 | ```sh 16 | git clone https://github.com/uralys/fox 17 | ``` 18 | 19 | To keep same paths and `res://`, symlink godot elements in the `/fox` folder like this: 20 | 21 | ```sh 22 | cd your-game 23 | ln -s ../fox/fox fox 24 | ``` 25 | 26 | ### 4 - Declare your main Scene 27 | 28 | #### create the main script 29 | 30 | Create a `src` folder and Create a new scene with Godot Editor, you can name it `app.tscn`. 31 | 32 | Then add attach a `app.gd` script to this scene. 33 | 34 | You can remove the default code and replace with: 35 | 36 | ```gdscript 37 | extends 'res://fox/core/app.gd' 38 | 39 | func _ready(): 40 | finalizeFoxSetup() 41 | print(G.BUNDLE_ID + ' is running!') 42 | ``` 43 | 44 | Note: `finalizeFoxSetup()` is mandatory to setup Fox core nodes and settings. 45 | 46 | #### set as main scene 47 | 48 | Finally, right click on your `app.tscn` to `Set as Main Scene` 49 | 50 | Or edit manually your `project.godot` to declare: 51 | 52 | ```ini 53 | [application] 54 | run/main_scene="res://src/app.tscn" 55 | ``` 56 | 57 | #### create mandatory nodes 58 | 59 | You must setup a few nodes in your main scene: 60 | 61 | by default: 62 | 63 | - `app` should be a `CanvasLayer` 64 | - `app/scene` should also be a `Node2D` 65 | - `app/hud` should be a `CanvasLayer` 66 | 67 | To change these defaults, edit the `fox/core` "extends XXX" 68 | 69 | ```sh 70 | app 71 | ├── scene 72 | └── hud 73 | ``` 74 | 75 | ### 4 - Declare Fox default config 76 | 77 | Now you need to setup Fox default paths within the `project.godot` `[autoload]` section. 78 | 79 | ```ini 80 | [autoload] 81 | 82 | G="*res://fox/core/globals.gd" 83 | DEBUG="*res://fox/core/debug.gd" 84 | Gesture="*res://fox/libs/gesture.gd" 85 | ``` 86 | 87 | and set few default options 88 | 89 | ```ini 90 | [bundle] 91 | 92 | id="your-game" 93 | version="0.0.1" 94 | versionCode=1 95 | platform="xxx" 96 | env="debug" 97 | ``` 98 | 99 | ### Let's craft! 100 | 101 | At this point, you should have something like this: 102 | 103 | ```sh 104 | . 105 | ├── fox 106 | └── your-game 107 | ├──.godot 108 | ├── fox -> ../fox/fox 109 | ├── fox.config.json 110 | ├── icon.svg 111 | ├── project.godot 112 | └── src 113 | ├── app.gd 114 | └── app.tscn 115 | ``` 116 | 117 | You can have a look at your startup app: 118 | 119 | ```sh 120 | fox run:start 121 | ``` 122 | 123 | and now let's start your editor and enjoy developing! 124 | 125 | ```sh 126 | fox run:editor 127 | ``` 128 | 129 | ## 🏹 extending default Fox Nodes 130 | 131 | To extend a Fox default Node, you can do like with did with the main scene: Extend the Node from you script. 132 | 133 | For example, to extend Globals and add your own: 134 | 135 | Create a `globals.gd` 136 | 137 | ```gdscript 138 | extends 'res://fox/core/main.gd' 139 | ``` 140 | 141 | And replace the autoload in `project.godot` with yours: 142 | 143 | ```ini 144 | [autoload] 145 | G="*res://src/globals.gd" 146 | ``` 147 | 148 | To better use Fox core, screens and components, you can organise your project like this: 149 | 150 | ```sh 151 | . 152 | ├── fox 153 | └── your-game 154 | ├── assets 155 | │   ├── map.png 156 | │   └── logo.svg 157 | ├── fox -> ../fox/fox 158 | ├── fox.config.json 159 | ├── project.godot 160 | ├── readme.md 161 | └── src 162 | ├── main.gd 163 | ├── main.tscn 164 | ├── player.gd 165 | ├── router.gd 166 | └── screens 167 | ├── home.tscn 168 | └── home.gd 169 | ``` 170 | 171 | 🚀 You can continue by extending the [Router](./router.md) to add your first screens. 172 | -------------------------------------------------------------------------------- /fox/animations/intro-animation.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=12 format=3 uid="uid://cdceudy16q3st"] 2 | 3 | [ext_resource type="Script" uid="uid://djnatkhpe7v5s" path="res://fox/animations/intro-animation.gd" id="1_ubxrs"] 4 | [ext_resource type="Texture2D" uid="uid://cbim7dv1kofo4" path="res://fox/assets/splash/uralys-banner.webp" id="2_01e2g"] 5 | [ext_resource type="Texture2D" uid="uid://cg7fbtk88iqkf" path="res://fox/assets/splash/u.png" id="3_w0kbj"] 6 | [ext_resource type="Texture2D" uid="uid://dsbll0xbc4qu8" path="res://fox/assets/splash/r.png" id="4_4qfkm"] 7 | [ext_resource type="Texture2D" uid="uid://dv47yowudwflp" path="res://fox/assets/splash/a.png" id="5_pmoyp"] 8 | [ext_resource type="Texture2D" uid="uid://m57u8kqidkga" path="res://fox/assets/splash/l.png" id="6_pyk4c"] 9 | [ext_resource type="Texture2D" uid="uid://daj0n7uiyq5fj" path="res://fox/assets/splash/y.png" id="7_jsixp"] 10 | [ext_resource type="Texture2D" uid="uid://c6u3p83wmrca4" path="res://fox/assets/splash/s.png" id="8_x6n67"] 11 | [ext_resource type="Texture2D" uid="uid://cbcdpk2whiio" path="res://fox/assets/splash/dot.png" id="9_logd5"] 12 | 13 | [sub_resource type="Gradient" id="Gradient_xy75g"] 14 | offsets = PackedFloat32Array(0) 15 | colors = PackedColorArray(0.0208125, 0.051448, 0.0946116, 1) 16 | 17 | [sub_resource type="GradientTexture2D" id="GradientTexture2D_i0gxd"] 18 | gradient = SubResource("Gradient_xy75g") 19 | fill = 1 20 | fill_from = Vector2(0.509174, 0.577982) 21 | fill_to = Vector2(0.922018, 0.0894495) 22 | 23 | [node name="intro" type="CanvasLayer"] 24 | layer = 128 25 | script = ExtResource("1_ubxrs") 26 | 27 | [node name="bg" type="TextureRect" parent="."] 28 | anchors_preset = 15 29 | anchor_right = 1.0 30 | anchor_bottom = 1.0 31 | grow_horizontal = 2 32 | grow_vertical = 2 33 | pivot_offset = Vector2(966, 557) 34 | texture = SubResource("GradientTexture2D_i0gxd") 35 | expand_mode = 1 36 | metadata/_edit_lock_ = true 37 | 38 | [node name="logo" type="TextureRect" parent="."] 39 | anchors_preset = 8 40 | anchor_left = 0.5 41 | anchor_top = 0.5 42 | anchor_right = 0.5 43 | anchor_bottom = 0.5 44 | offset_left = -301.0 45 | offset_top = -281.0 46 | offset_right = 299.0 47 | offset_bottom = 57.0 48 | grow_horizontal = 2 49 | grow_vertical = 2 50 | pivot_offset = Vector2(296, 198) 51 | texture = ExtResource("2_01e2g") 52 | expand_mode = 1 53 | 54 | [node name="letters" type="ReferenceRect" parent="."] 55 | modulate = Color(0.876843, 0.430274, 0.300823, 1) 56 | anchors_preset = 8 57 | anchor_left = 0.5 58 | anchor_top = 0.5 59 | anchor_right = 0.5 60 | anchor_bottom = 0.5 61 | offset_left = -393.0 62 | offset_top = 18.0 63 | offset_right = 386.0 64 | offset_bottom = 171.0 65 | grow_horizontal = 2 66 | grow_vertical = 2 67 | border_width = 0.0 68 | 69 | [node name="u" type="Sprite2D" parent="letters"] 70 | position = Vector2(143.996, 65.532) 71 | scale = Vector2(0.490155, 0.490155) 72 | texture = ExtResource("3_w0kbj") 73 | 74 | [node name="r" type="Sprite2D" parent="letters"] 75 | position = Vector2(267.996, 63.5319) 76 | scale = Vector2(0.500939, 0.507) 77 | texture = ExtResource("4_4qfkm") 78 | 79 | [node name="a" type="Sprite2D" parent="letters"] 80 | position = Vector2(390, 64.9999) 81 | scale = Vector2(0.131745, 0.132874) 82 | texture = ExtResource("5_pmoyp") 83 | 84 | [node name="l" type="Sprite2D" parent="letters"] 85 | position = Vector2(497, 64.9999) 86 | scale = Vector2(0.507002, 0.507002) 87 | texture = ExtResource("6_pyk4c") 88 | 89 | [node name="y" type="Sprite2D" parent="letters"] 90 | position = Vector2(576.152, 66.0321) 91 | scale = Vector2(0.474107, 0.474107) 92 | texture = ExtResource("7_jsixp") 93 | 94 | [node name="s" type="Sprite2D" parent="letters"] 95 | position = Vector2(675.152, 66.032) 96 | scale = Vector2(0.461651, 0.461651) 97 | texture = ExtResource("8_x6n67") 98 | 99 | [node name="dot" type="Sprite2D" parent="letters"] 100 | position = Vector2(742.152, 90.0321) 101 | scale = Vector2(0.325746, 0.325746) 102 | texture = ExtResource("9_logd5") 103 | -------------------------------------------------------------------------------- /fox/core/router.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | 3 | extends Node 4 | 5 | # ------------------------------------------------------------------------------ 6 | 7 | var ScreenFader = preload("res://fox/components/screen-fader.tscn") 8 | var FullscreenLoader = preload("res://fox/components/fullscreen-loader.tscn") 9 | var SettingsPopup = preload('res://src/popups/settings.tscn') 10 | var LanguagesPopup = preload('res://src/popups/languages.tscn') 11 | 12 | # ------------------------------------------------------------------------------ 13 | 14 | var resourceLoader 15 | var _loadedResources = {} 16 | var fullscreenLoader 17 | 18 | signal loaded 19 | 20 | # ------------------------------------------------------------------------------ 21 | 22 | var currentScene = null 23 | 24 | # ------------------------------------------------------------------------------ 25 | 26 | func getCurrentSceneName(): 27 | return str(currentScene.name) 28 | 29 | func getCurrentScene(): 30 | return currentScene 31 | 32 | # ------------------------------------------------------------------------------ 33 | 34 | func openDefault(): 35 | G.log('openDefault() can be overriden to open your default screen when an error occurs.') 36 | 37 | # ------------------------------------------------------------------------------ 38 | 39 | func onOpenScene(): 40 | return 0 41 | 42 | # ------------------------------------------------------------------------------ 43 | 44 | func openScene(scene, options = {}): 45 | call_deferred("_openScene", scene, options) 46 | 47 | func _openScene(scene, options = {}): 48 | var previousSceneName = 'none' 49 | 50 | if(currentScene): 51 | previousSceneName = str(currentScene.name) 52 | G.log('[🦊 Router]> leaving', previousSceneName, '> ---------') 53 | 54 | if(currentScene.has_method('onLeave')): 55 | currentScene.onLeave(options) 56 | 57 | var timeToWaitOnOpenScene = onOpenScene() 58 | await Wait.forSomeTime($/root, timeToWaitOnOpenScene).timeout 59 | 60 | $'/root/app/scene'.remove_child(currentScene) 61 | currentScene.queue_free() 62 | 63 | currentScene = scene.instantiate() 64 | $'/root/app/scene'.add_child(currentScene) 65 | 66 | if(__.Get('onOpen',options) != null): 67 | options.onOpen.call() 68 | 69 | if(currentScene.has_method('onOpen')): 70 | currentScene.onOpen(options) 71 | 72 | G.log('[🦊 Router]> ---------- entered:', str(currentScene.name)) 73 | 74 | # ------------------------------------------------------------------------------ 75 | 76 | func startLoadingResource(path): 77 | resourceLoader = ResourceLoader.load_threaded_request(path) 78 | _loadedResources.__loading = path 79 | 80 | if resourceLoader == null: # Check for errors. 81 | openDefault() 82 | 83 | func finishedLoadingResource(): 84 | var resource = resourceLoader.get_resource() 85 | _loadedResources[_loadedResources.__loading] = resource 86 | _loadedResources.__loading = null 87 | resourceLoader = null 88 | loaded.emit() 89 | return resource 90 | 91 | func getLoadingProgress(): 92 | if resourceLoader == null: 93 | return 1 94 | return float(resourceLoader.get_stage()) / resourceLoader.get_stage_count() 95 | 96 | func getLoadedResource(path): 97 | if(_loadedResources.has(path)): 98 | return _loadedResources[path] 99 | return null 100 | 101 | # ------------------------------------------------------------------------------ 102 | 103 | func useScreenFader(duration:float = 0.75): 104 | var fader = ScreenFader.instantiate() 105 | fader.duration = duration 106 | getCurrentScene().add_child(fader) 107 | 108 | # ------------------------------------------------------------------------------ 109 | 110 | func showLoader(): 111 | fullscreenLoader = FullscreenLoader.instantiate() 112 | $/root/app.add_child(fullscreenLoader) 113 | 114 | func hideLoader(): 115 | if(fullscreenLoader): 116 | fullscreenLoader.remove() 117 | fullscreenLoader.queue_free() 118 | fullscreenLoader = null 119 | 120 | # ------------------------------------------------------------------------------ 121 | 122 | func openSettings(): 123 | var settings = SettingsPopup.instantiate() 124 | $/root/app/popups.add_child(settings) 125 | settings.show() 126 | 127 | func openLanguages(options = {}): 128 | var languages = LanguagesPopup.instantiate() 129 | languages.onClose = __.Get('onClose', options) 130 | languages.welcome = __.Get('welcome', options) 131 | 132 | $/root/app/popups.add_child(languages) 133 | languages.show() 134 | -------------------------------------------------------------------------------- /fox/components/pool.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://dxh3inc2gyell"] 2 | 3 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_x6aht"] 4 | corner_radius_top_left = 220 5 | corner_radius_top_right = 220 6 | corner_radius_bottom_right = 186 7 | corner_radius_bottom_left = 186 8 | 9 | [sub_resource type="Shader" id="Shader_i0x2f"] 10 | code = "shader_type canvas_item; 11 | 12 | #define iResolution 1.0/SCREEN_PIXEL_SIZE 13 | #define iTime TIME 14 | #define fragColor COLOR 15 | 16 | uniform float uv_scale : hint_range(0.0, 10.0, 0.1) = 1.0; 17 | uniform float color_alpha : hint_range(0.0, 1.0, 0.1) = 1.0; 18 | 19 | vec2 hash( vec2 p ) // replace this by something better 20 | { 21 | p = vec2( dot(p,vec2(127.1,311.7)), dot(p,vec2(269.5,183.3)) ); 22 | return -1.0 + 2.0*fract(sin(p)*43758.5453123); 23 | } 24 | 25 | float noise( in vec2 p ) 26 | { 27 | const float K1 = 0.366025404; // (sqrt(3)-1)/2; 28 | const float K2 = 0.211324865; // (3-sqrt(3))/6; 29 | 30 | vec2 i = floor( p + (p.x+p.y)*K1 ); 31 | vec2 a = p - i + (i.x+i.y)*K2; 32 | float m = step(a.y,a.x); 33 | vec2 o = vec2(m,1.0-m); 34 | vec2 b = a - o + K2; 35 | vec2 c = a - 1.0 + 2.0*K2; 36 | vec3 h = max( 0.5-vec3(dot(a,a), dot(b,b), dot(c,c) ), 0.0 ); 37 | vec3 n = h*h*h*h*vec3( dot(a,hash(i+0.0)), dot(b,hash(i+o)), dot(c,hash(i+1.0))); 38 | return dot( n, vec3(70.0) ); 39 | } 40 | 41 | #define MAX_WAVES 4 42 | #define SUPERPOSITION 4 43 | #define TAU 6.28318 44 | #define PHI 1.618 45 | 46 | // closed form normal would increase performance a lot 47 | float height(vec2 p, float t) { 48 | float acc = 0.0; 49 | for (int i = 0; i < MAX_WAVES; i++) { 50 | for (int j = 0; j < SUPERPOSITION; j++) { 51 | int seed = i + 5*j; 52 | //float theta = TAU * noise(vec2(0.01 * t, 10.0*float(i))); 53 | float theta = TAU * PHI * 10.0*float(seed); 54 | float up = cos(theta) * p.x - sin(theta) * p.y; 55 | float vp = sin(theta) * p.x + cos(theta) * p.y; 56 | //float initial_phase = TAU * noise(vec2(0.0, 2.0*float(i))) + cos(vp); 57 | float initial_phase = TAU * PHI * float(seed); 58 | //float k = pow(2.0, float(i)*0.1) + 0.5; 59 | //float k = pow(2.0, 1.0 + 0.4*float(i)); 60 | float k = pow(2.0, float(i)); 61 | //float k = float(i+1); 62 | float phase = initial_phase + up*k + cos(vp) + 3.0*t + 0.5*k*t; 63 | // its kinda like choose your artifacts, probably use noise for vp 64 | float A = cos(phase)/(k*k); 65 | acc += A; 66 | }} 67 | return acc; 68 | } 69 | vec4 hn_fdm(vec2 p, float t) { 70 | float h = height(p, t); 71 | vec2 vx = vec2(0.1, 0.0); 72 | vec2 vy = vec2(0.0, 0.1); 73 | float hx = height(p+vx, t); 74 | float hy = height(p+vy, t); 75 | float dx = (hx - h); 76 | float dy = (hy - h); 77 | // vec3 norm = normalize(vec3(-dx, -dy, dx+dy)); 78 | // vec3 norm = normalize(vec3(-dx/vx.x, -dy/vy.y, 1.0)); 79 | 80 | vec3 v1 = normalize(vec3(vx.x, 0.0, dx)); 81 | vec3 v2 = normalize(vec3(0.0, vy.y, dy)); 82 | vec3 norm = cross(v1, v2); 83 | 84 | return vec4(norm, h); 85 | } 86 | 87 | void fragment() 88 | { 89 | vec2 uv = UV; 90 | vec2 uv_screen = (uv - 0.5) * uv_scale; 91 | 92 | vec4 nh = hn_fdm(uv_screen* 10.0, iTime * 1.0 * 0.3); 93 | float h = nh.w; 94 | vec3 norm = nh.xyz; 95 | vec3 sun_dir = normalize(vec3(-0.2, 0.4, 1.0)); 96 | 97 | vec4 water_colour = vec4(0.2, 0.4, 0.6, 1.0); 98 | vec4 foam_colour = vec4(0.8, 0.9, 1.0, 1.0); 99 | vec4 sky_colour = vec4(0.2, 0.6, 0.8, 1.0); 100 | vec4 specular_colour = vec4(1.0, 1.0, 1.0, 1.0); 101 | 102 | //fragColor = vec4(norm.xyz, 1.0); return; 103 | 104 | if (dot(sun_dir, norm) > 0.98) { 105 | fragColor = specular_colour; 106 | } else { 107 | fragColor = mix(water_colour, sky_colour, dot(norm, normalize(vec3(0.0, 0.2, 1.0)))); 108 | } 109 | fragColor.a = color_alpha; 110 | } 111 | " 112 | 113 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_dbgb7"] 114 | shader = SubResource("Shader_i0x2f") 115 | shader_parameter/uv_scale = 0.4 116 | shader_parameter/color_alpha = 1.0 117 | 118 | [node name="pool" type="Panel"] 119 | clip_children = 1 120 | anchors_preset = 8 121 | anchor_left = 0.5 122 | anchor_top = 0.5 123 | anchor_right = 0.5 124 | anchor_bottom = 0.5 125 | offset_left = 2.0 126 | offset_top = -217.0 127 | offset_right = 81.0 128 | offset_bottom = -181.0 129 | grow_horizontal = 2 130 | grow_vertical = 2 131 | theme_override_styles/panel = SubResource("StyleBoxFlat_x6aht") 132 | 133 | [node name="water" type="ColorRect" parent="."] 134 | material = SubResource("ShaderMaterial_dbgb7") 135 | layout_mode = 1 136 | anchors_preset = 15 137 | anchor_right = 1.0 138 | anchor_bottom = 1.0 139 | grow_horizontal = 2 140 | grow_vertical = 2 141 | -------------------------------------------------------------------------------- /docs/exporting/android.md: -------------------------------------------------------------------------------- 1 | # Android 2 | 3 | Exporting for Android: 4 | 5 | - configure to build with gradle, and install the build template: `Project > Install android build template`. It will generate a root `android` folder. 6 | 7 | ## permissions 8 | 9 | To use HTTP calls to your backend you need to add the following permissions to your android template in `export_presets.cfg`: 10 | 11 | ```ini 12 | permissions/internet=true 13 | ``` 14 | 15 | Otherwise, network communication of any kind will be blocked by the Android OS. (from this [warning in the docs](https://docs.godotengine.org/en/stable/tutorials/networking/http_request_class.html#http-requests-in-godot)) 16 | 17 | ## debug key 18 | 19 | generate debug key for godot: 20 | 21 | ```sh 22 | > keytool -keyalg RSA -genkeypair -v -alias YOUR_ALIAS -keystore android.debug.keystore -validity 9999 -deststoretype pkcs12 -keypass YOUR_PASSWORD 23 | ``` 24 | 25 | replace in the preset options: 26 | 27 | ```ini 28 | keystore/debug_user="YOUR_ALIAS" 29 | keystore/debug_password="YOUR_PASSWORD" 30 | ``` 31 | 32 | ## icons 33 | 34 | In `assets/android` you'll find the adaptive icons template from 35 | 36 | ## apk for debug 37 | 38 | ### build apk from cli 39 | 40 | ```sh 41 | > /Applications/Apps/Godot.app/Contents/MacOS/Godot --export-debug "Android Debug" --headless 42 | ``` 43 | 44 | ### manifest from apk 45 | 46 | ```sh 47 | > aapt dump badging _build/android/lockeyland.apk 48 | ``` 49 | 50 | ```sh 51 | > which aapt 52 | aapt: aliased to ~/Library/Android/sdk/build-tools/32.0.0/aapt2 53 | ``` 54 | 55 | ## aab for releases 56 | 57 | ### build aab from cli 58 | 59 | ```sh 60 | > /Applications/Apps/Godot.app/Contents/MacOS/Godot --export-debug "Android Release" --headless 61 | ``` 62 | 63 | ### manifest from aab 64 | 65 | ```sh 66 | > brew install bundletool 67 | ``` 68 | 69 | ```sh 70 | > bundletool dump manifest --bundle _build/android/lockeyland.aab --xpath /manifest/@android:versionName 71 | 1.0.0 72 | ``` 73 | 74 | ```sh 75 | > bundletool dump manifest --bundle _build/android/lockeyland.aab --xpath /manifest/@android:versionCode 76 | 10000 77 | ``` 78 | 79 | ## install playstore on emulators 80 | 81 | all steps: 82 | 83 | 1 - download `Phonesky.apk` 84 | 2 - `emulator @pixel-api-21 -writable-system` 85 | 3 - `adb remount` 86 | 4 - `adb push ~/path/to/Phonesky.apk /system/priv-app/` 87 | 5 - `adb shell stop && adb shell start` 88 | 89 | ## intall apk on emulator 90 | 91 | 1 - `emulator @pixel-api-21 -no-snapshot -writable-system` 92 | 2 - `fox export` an android debug preset 93 | 3 - `adb uninstall com.uralys.xxx` 94 | 4 - `adb install -r ~/path/to/your.apk` 95 | 5 - `adb logcat -s godot` 96 | 97 | --> repeat from `2` to `5` on every test 98 | 99 | ## IAP 100 | 101 | ### setting up android plugins 102 | 103 | - To keep your plugins whenever you update (and erase the `android` folder), you can install them next to your game folder, and symlink to the `android/plugins` folder each time you reinstall the latest android template. 104 | 105 | - install android plugins next to `/fox` and `/yourgame` 106 | 107 | for example in my case: 108 | 109 | ```sh 110 | ~/Projects/uralys/gamedev/ 111 | └── fox 112 | └── yourgame 113 | └── godot.android-plugins 114 | ├── GodotGooglePlayBilling.x.x.x.release.aar 115 | └── GodotGooglePlayBilling.gdap 116 | ``` 117 | 118 | ```sh 119 | > ln -s ~/Projects/uralys/gamedev/godot.android-plugins android/plugins 120 | > ln -s ~/Projects/uralys/gamedev/godot.addons addons 121 | ``` 122 | 123 | note: currently using `godot-lib.4.1.3.stable.template_release.aar` and building `assembleRelease` from Android Studio 124 | 125 | ### testing IAP 126 | 127 | to test android IAP: 128 | 129 | - `fox export > select an android release preset` to generate the `.aab` 130 | - create an intern release 131 | - add a product 132 | - add a tester and send invite link to internal Play Store 133 | - connect to PlayStore with this tester account 134 | - Accept the invitation through the link 135 | 136 | then the test account can see the SKU even from `debug.apk` generated with `fox export > android debug preset` manually installed with `adb`. 137 | 138 | API reference and examples: 139 | 140 | ## Notifications: 141 | 142 | - install Android plugin 143 | - add `useNotifications: true` to foxConfig.core 144 | 145 | When Fox initializes, it will check if the app `useNotifications`. If so, it will instantiate a `NotificationScheduler` singleton and add it to the `root` node. 146 | 147 | To generate an icon, you may use tools like 148 | 149 | The generated icons should be placed in the `android/build/res/drawable*` folders. 150 | -------------------------------------------------------------------------------- /fox/libs/gesture.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | 3 | extends Node 4 | 5 | # ------------------------------------------------------------------------------ 6 | 7 | var state = { 8 | dragArea = null, 9 | draggable = null, 10 | droppable = null, 11 | DRAGGING_DATA = null, 12 | PRESS_EVENTS = [] 13 | } 14 | 15 | # ------------------------------------------------------------------------------ 16 | 17 | func findPressEvent(touchable): 18 | var pressEvents = state.PRESS_EVENTS.filter(func(_event): 19 | return _event.touchable == touchable 20 | ) 21 | 22 | if(pressEvents.size() > 0): 23 | return pressEvents[0] 24 | 25 | # ------------------------------------------------------------------------------ 26 | 27 | func addPressedItem(pressEvent): 28 | state.PRESS_EVENTS.append(pressEvent) 29 | state.PRESS_EVENTS.sort_custom(func(evA, evB): 30 | if(evA.zIndex > evA.zIndex): return true 31 | return evA.touchDistance < evB.touchDistance 32 | ) 33 | 34 | # ------------------------------------------------------------------------------ 35 | 36 | func removePressedItem(touchable): 37 | # another touchable has already claimed acceptation 38 | if(state.PRESS_EVENTS.size() == 0): 39 | return 40 | 41 | var pressEvent = findPressEvent(touchable) 42 | 43 | if(pressEvent): 44 | state.PRESS_EVENTS.erase(pressEvent) 45 | 46 | # ------------------------------------------------------------------------------ 47 | 48 | func shouldConcedePriority(touchable): 49 | if(state.PRESS_EVENTS.size() > 0): 50 | for pressEvent in state.PRESS_EVENTS: 51 | var priority = __.GetOr(10000, 'touchable.inputPriority', pressEvent) 52 | if(priority < touchable.inputPriority): 53 | return true 54 | 55 | return false 56 | 57 | # ------------------------------------------------------------------------------ 58 | 59 | func acceptTouchable(touchable): 60 | # another touchable has already been accepted 61 | if(state.PRESS_EVENTS.size() == 0): 62 | return false 63 | 64 | # PRESS_EVENTS are sorted as soon as they are added during addPressedItem 65 | var isAccepted = state.PRESS_EVENTS[0].touchable == touchable 66 | 67 | if(isAccepted): 68 | state.PRESS_EVENTS = [state.PRESS_EVENTS[0]] 69 | return true 70 | 71 | return false 72 | 73 | # ------------------------------------------------------------------------------ 74 | 75 | func isDragging(): 76 | return state.DRAGGING_DATA != null 77 | 78 | func getDraggingData(): 79 | return state.DRAGGING_DATA 80 | 81 | func currentDragArea(): 82 | return __.Get('DRAGGING_DATA.dragArea', state) 83 | 84 | func currentDraggable(): 85 | return __.Get('DRAGGING_DATA.draggable', state) 86 | 87 | func currentDroppable(): 88 | return __.Get('DRAGGING_DATA.droppable', state) 89 | 90 | # ------------------------------------------------------------------------------ 91 | 92 | func startDragging(dragArea, draggable, additionalDragData): 93 | state.DRAGGING_DATA = { 94 | dragArea = dragArea, 95 | draggable = draggable 96 | } 97 | 98 | if(additionalDragData): 99 | for key in additionalDragData.keys(): 100 | var value = additionalDragData[key] 101 | __.Set(value, key, state.DRAGGING_DATA) 102 | 103 | # ------------------------------------------------------------------------------ 104 | 105 | func verifyDroppableOnEnter(_droppable, acceptedType: String): 106 | var dragArea = currentDragArea() 107 | 108 | if(not dragArea): 109 | return 110 | 111 | if(dragArea.type == acceptedType): 112 | state.DRAGGING_DATA.droppable = _droppable 113 | dragArea.emit_signal('foundDroppable', _droppable) 114 | 115 | _droppable.emit_signal('dropActived') 116 | 117 | # ------------------------------------------------------------------------------ 118 | 119 | func verifyDroppableOnExit(_droppable, acceptedType: String): 120 | var dragArea = currentDragArea() 121 | if(not dragArea): 122 | return 123 | 124 | if(dragArea.type == acceptedType): 125 | state.DRAGGING_DATA.droppable = null 126 | dragArea.emit_signal('leftDroppable', _droppable) 127 | _droppable.emit_signal('dropDeactived') 128 | 129 | # ------------------------------------------------------------------------------ 130 | 131 | func handleDraggingEnd(): 132 | var dragArea = currentDragArea() 133 | var draggable = currentDraggable() 134 | var droppable = currentDroppable() 135 | 136 | if(droppable): 137 | droppable.emit_signal('received', state.DRAGGING_DATA, draggable.position) 138 | dragArea.emit_signal('droppedOnDroppable', state.DRAGGING_DATA, draggable.position) 139 | else: 140 | dragArea.emit_signal('droppedIntheWild', draggable.position) 141 | 142 | state.DRAGGING_DATA = null 143 | 144 | # ------------------------------------------------------------------------------ 145 | 146 | func switchDraggingTo(newDraggable, parentReference = null): 147 | var droppable = currentDroppable() 148 | var dragArea = currentDragArea() 149 | dragArea.resetInteraction() 150 | 151 | var newDragArea = newDraggable.get_node('interactiveArea2D') 152 | 153 | newDragArea.prepareDraggable({ 154 | draggable = newDraggable, 155 | type = dragArea.type, 156 | parentReference = parentReference 157 | }) 158 | 159 | newDragArea.manualStartDragging() 160 | 161 | state.DRAGGING_DATA.droppable = droppable 162 | -------------------------------------------------------------------------------- /cli/bundler/switch.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import inquirer from 'inquirer'; 6 | 7 | // ----------------------------------------------------------------------------- 8 | 9 | import ini from './ini.js'; 10 | import { toVersionNumber } from './versioning.js'; 11 | import { getSubtitle, getTitle } from './export.js'; 12 | 13 | // ----------------------------------------------------------------------------- 14 | 15 | const OVERRIDE_CFG = './override.cfg'; 16 | const ENV = ['debug', 'staging', 'release']; 17 | 18 | // ----------------------------------------------------------------------------- 19 | 20 | const extractEnv = (preset) => { 21 | const _env = preset.custom_features.split(',').find((feature) => feature.includes('env:')); 22 | 23 | if (!_env) { 24 | console.warn(`\n🔴 env:${chalk.red('export_presets.cfg must be edited')}`); 25 | console.warn(`Missing 'env' in custom_features: "${preset.custom_features}"`); 26 | console.warn(`add "env:debug" or "env:release" within the ${chalk.blueBright('custom_features')} list`); 27 | return; 28 | } 29 | 30 | const env = _env.split('env:')[1]; 31 | 32 | if (!ENV.includes(env)) { 33 | console.warn(`env:${chalk.yellow(env)} is not supported, use one of [${ENV}]`); 34 | return; 35 | } 36 | 37 | return env; 38 | }; 39 | 40 | // ----------------------------------------------------------------------------- 41 | 42 | const inquireParams = async (bundles, presets) => { 43 | const bundleIds = Object.keys(bundles); 44 | const singleBundleId = bundleIds.length > 1 ? null : bundleIds[0]; 45 | 46 | const questions = [ 47 | { 48 | message: 'preset', 49 | name: 'presetNum', 50 | type: 'list', 51 | choices: Object.keys(presets).map((num) => ({ 52 | name: presets[num].name, 53 | value: num 54 | })) 55 | } 56 | ]; 57 | 58 | if (!singleBundleId) { 59 | questions.push({ 60 | message: 'bundle', 61 | name: 'bundleId', 62 | type: 'list', 63 | choices: bundleIds 64 | }); 65 | } 66 | 67 | const answers = await inquirer.prompt(questions); 68 | const { bundleId = singleBundleId, presetNum } = answers; 69 | const preset = presets[presetNum]; 70 | const bundle = bundles[bundleId]; 71 | 72 | return { bundleId, bundle, preset }; 73 | }; 74 | 75 | // ----------------------------------------------------------------------------- 76 | 77 | const switchBundle = async (settings, presets) => { 78 | const { core, bundles } = settings; 79 | console.log(`⚙️ switching to another ${chalk.blue.bold('bundle')}...`); 80 | 81 | if (!bundles) { 82 | console.log('\nmissing bundles in fox.config.json'); 83 | console.log(chalk.red.bold('🔴 failed: you can use default config for bundles')); 84 | return; 85 | } 86 | 87 | if (!presets) { 88 | console.log(chalk.red.bold('🔴 failed: missing presets')); 89 | return; 90 | } 91 | 92 | const { bundleId, preset } = await inquireParams(bundles, presets); 93 | 94 | // --------- 95 | 96 | const env = extractEnv(preset); 97 | 98 | if (!env) { 99 | console.log(chalk.red.bold('🔴 failed: could not find env')); 100 | return; 101 | } 102 | 103 | console.log(`⚙️ env: ${env}`); 104 | 105 | // --------- 106 | 107 | const override = { bundle: {}, fox: {}, custom: {} }; 108 | const appPackageJSON = JSON.parse(fs.readFileSync('./package.json', 'utf8')); 109 | const foxPackageJSON = JSON.parse(fs.readFileSync('../fox/package.json', 'utf8')); 110 | 111 | const subtitle = getSubtitle(bundles[bundleId]) 112 | 113 | override.fox.version = foxPackageJSON.version; 114 | override.bundle.id = bundleId; 115 | override.bundle.title = getTitle(core); 116 | override.bundle.version = appPackageJSON.version; 117 | override.bundle.versionCode = toVersionNumber(appPackageJSON.version); 118 | override.bundle.platform = preset.platform; 119 | override.bundle.env = env; 120 | 121 | if (subtitle) { 122 | override.bundle.subtitle = getSubtitle(bundles[bundleId]); 123 | } 124 | 125 | if (core.useNotifications !== undefined) { 126 | override.custom.useNotifications = core.useNotifications; 127 | } 128 | 129 | // --------- 130 | 131 | let overrideByEnv; 132 | 133 | try { 134 | overrideByEnv = ini.parse(fs.readFileSync(`./override.${env}.cfg`, 'utf8')); 135 | } catch (e) { 136 | overrideByEnv = {}; 137 | } 138 | 139 | // --------- 140 | 141 | let secretByEnv; 142 | 143 | try { 144 | secretByEnv = ini.parse(fs.readFileSync(`./secret.${env}.cfg`, 'utf8')); 145 | } catch (e) { 146 | secretByEnv = {}; 147 | } 148 | 149 | // --------- 150 | 151 | override.custom = { 152 | ...override.custom, 153 | ...overrideByEnv, 154 | ...secretByEnv 155 | }; 156 | 157 | // --------- 158 | 159 | Object.keys(override.bundle).forEach((key) => { 160 | console.log(` ${chalk.green.bold('[bundle]')} ${key} = ${override.bundle[key]}`); 161 | }) 162 | 163 | Object.keys(override.custom).forEach((key) => { 164 | console.log(` ${chalk.magenta.bold('[custom]')} ${key} = ${key.includes('secret') ? 'xxx' : override.custom[key]}`); 165 | }) 166 | 167 | // --------- 168 | 169 | fs.writeFileSync(OVERRIDE_CFG, ini.stringify(override)); 170 | 171 | return { bundleId, preset, env }; 172 | }; 173 | 174 | // ----------------------------------------------------------------------------- 175 | 176 | export default switchBundle; 177 | -------------------------------------------------------------------------------- /cli/bundler/update-preset.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | 3 | import chalk from 'chalk'; 4 | import { androidExtension, getApplicationName } from './export.js'; 5 | import { toVersionNumber } from './versioning.js'; 6 | 7 | // ----------------------------------------------------------------------------- 8 | 9 | const MAC_OSX = 'Mac OSX'; 10 | const IOS = 'iOS'; 11 | const ANDROID = 'Android'; 12 | 13 | // ----------------------------------------------------------------------------- 14 | 15 | const updateOptions = (preset, key, value) => { 16 | preset.options[key] = value; 17 | console.log(` - ${chalk.bold(key)}=${chalk.yellow.italic(value)}`); 18 | }; 19 | 20 | const updateMain = (preset, key, value) => { 21 | preset[key] = value; 22 | console.log(` - ${chalk.bold(key)}=${chalk.yellow.italic(value)}`); 23 | }; 24 | 25 | // ----------------------------------------------------------------------------- 26 | 27 | const updateIcons = (preset, bundleId) => { 28 | Object.keys(preset.options).forEach((key) => { 29 | if (key.includes('icon')) { 30 | const newIcon = preset.options[key].replace( 31 | /generated\/.+\/icons/g, 32 | `generated/${bundleId}/icons` 33 | ); 34 | 35 | updateOptions(preset, key, newIcon); 36 | } 37 | }); 38 | }; 39 | 40 | // ----------------------------------------------------------------------------- 41 | 42 | const updateAndroidPreset = (env, preset, bundle, bundleId, applicationName, bundleName) => { 43 | console.log(`main:`); 44 | updateMain(preset, 'export_path', `_build/android/${bundleName}${androidExtension(env)}`); 45 | console.log(`options:`); 46 | 47 | updateOptions(preset, 'package/name', applicationName); 48 | 49 | const packageUIDKey = 'package/unique_name'; 50 | const packageUID = (bundle[ANDROID] && bundle[ANDROID][packageUIDKey]) || bundle.uid; 51 | updateOptions(preset, packageUIDKey, packageUID); 52 | 53 | if (env === 'release' && bundle[ANDROID]['keystore/release_user']) { 54 | updateOptions(preset, 'keystore/release_user', bundle[ANDROID]['keystore/release_user']); 55 | } 56 | 57 | updateIcons(preset, bundleId); 58 | }; 59 | 60 | // ----------------------------------------------------------------------------- 61 | 62 | const updateIOSPreset = (env, preset, bundle, bundleId, applicationName, bundleName) => { 63 | console.log(`main:`); 64 | updateMain(preset, 'export_path', `_build/iOS/${bundleName}.ipa`); 65 | console.log(`options:`); 66 | 67 | updateOptions(preset, 'application/name', applicationName); 68 | 69 | const packageUID = (bundle[IOS] && bundle[IOS]['application/bundle_identifier']) || bundle.uid; 70 | updateOptions(preset, 'application/bundle_identifier', packageUID); 71 | 72 | updateIcons(preset, bundleId); 73 | }; 74 | 75 | // ----------------------------------------------------------------------------- 76 | 77 | const updateMacOSPreset = (env, preset, bundle, bundleId, applicationName) => { 78 | console.log(`main:`); 79 | updateMain(preset, 'export_path', `_build/macOS/${bundleName}`); 80 | console.log(`options:`); 81 | 82 | updateOptions(preset, 'application/name', applicationName); 83 | 84 | const packageUID = (bundle[IOS] && bundle[IOS]['application/bundle_identifier']) || bundle.uid; 85 | updateOptions(preset, 'application/bundle_identifier', packageUID); 86 | 87 | updateIcons(preset, bundleId); 88 | }; 89 | 90 | // ----------------------------------------------------------------------------- 91 | 92 | export const updateVersionInPreset = (preset, newVersion) => { 93 | const {platform, name} = preset; 94 | console.log('> updating version for', name); 95 | 96 | switch (platform) { 97 | case ANDROID: 98 | updateOptions(preset, 'version/code', toVersionNumber(newVersion)); 99 | updateOptions(preset, 'version/name', newVersion); 100 | break 101 | case IOS: 102 | case MAC_OSX: 103 | updateOptions(preset, 'application/short_version', newVersion); 104 | updateOptions(preset, 'application/version', newVersion); 105 | break 106 | } 107 | }; 108 | 109 | // ----------------------------------------------------------------------------- 110 | 111 | const updatePreset = (bundleId, env, coreConfig, preset, bundle) => { 112 | const {platform} = preset; 113 | console.log('⚙️ updating the preset:'); 114 | 115 | const _applicationName = getApplicationName(coreConfig, bundle); 116 | 117 | const applicationName = `${_applicationName}${env === 'release' ? '' : `(${env})`}`; 118 | const bundleName = `${bundleId}${env === 'release' ? '' : `-${env}`}`; 119 | 120 | switch (platform) { 121 | case ANDROID: 122 | updateAndroidPreset(env, preset, bundle, bundleId, applicationName, bundleName); 123 | break; 124 | case IOS: 125 | updateIOSPreset(env, preset, bundle, bundleId, applicationName, bundleName); 126 | break; 127 | case MAC_OSX: 128 | updateMacOSPreset(env, preset, bundle, bundleId, applicationName, bundleName); 129 | break; 130 | default: 131 | console.log(`\n> platform ${platform} has no preset specificity.`); 132 | console.log(`> applying default preset options:`); 133 | updateOptions(preset, 'application/name', applicationName); 134 | } 135 | 136 | console.log('✅ preset successfully updated.'); 137 | 138 | return { 139 | applicationName, 140 | bundleName 141 | }; 142 | }; 143 | 144 | // ----------------------------------------------------------------------------- 145 | 146 | export default updatePreset; 147 | -------------------------------------------------------------------------------- /fox/components/blur.gdshader: -------------------------------------------------------------------------------- 1 | shader_type canvas_item; 2 | 3 | uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap; 4 | // Xor's gausian blur function 5 | // Link: https://xorshaders.weebly.com/tutorials/blur-shaders-5-part-2 6 | // Defaults from: https://www.shadertoy.com/view/Xltfzj 7 | // 8 | // BLUR BLURRINESS (Default 8.0) 9 | // BLUR ITERATIONS (Default 16.0 - More is better but slower) 10 | // BLUR QUALITY (Default 4.0 - More is better but slower) 11 | // 12 | // Desc.: Don't have the best performance but will run on almost 13 | // anything, although, if developing for mobile, is better to use 14 | // 'texture_nodevgaussian(...) instead'. 15 | vec4 texture_xorgaussian(sampler2D tex, vec2 uv, vec2 pixel_size, float blurriness, int iterations, int quality){ 16 | float pi = 6.28; 17 | 18 | vec2 radius = blurriness / (1.0 / pixel_size).xy; 19 | vec4 blurred_tex = texture(tex, uv); 20 | 21 | for(float d = 0.0; d < pi; d += pi / float(iterations)){ 22 | for( float i = 1.0 / float(quality); i <= 1.0; i += 1.0 / float(quality) ){ 23 | vec2 directions = uv + vec2(cos(d), sin(d)) * radius * i; 24 | blurred_tex += texture(tex, directions); 25 | } 26 | } 27 | blurred_tex /= float(quality) * float(iterations) + 1.0; 28 | 29 | return blurred_tex; 30 | } 31 | 32 | // Experience-Monks' fast gaussian blur function 33 | // Link: https://github.com/Experience-Monks/glsl-fast-gaussian-blur/ 34 | // 35 | // BLUR ITERATIONS (Default 16.0 - More is better but slower) 36 | // BLUR DIRECTION (Direction in which the blur is apllied, use vec2(1, 0) for first pass and vec2(0, 1) for second pass) 37 | // 38 | // Desc.: ACTUALLY PRETTY SLOW but still pretty good for custom cinematic 39 | // bloom effects, since this needs render 2 passes 40 | vec4 texture_monksgaussian_multipass(sampler2D tex, vec2 uv, vec2 pixel_size, int iterations, vec2 direction){ 41 | vec4 blurred_tex = vec4(0.0); 42 | vec2 resolution = 1.0 / pixel_size; 43 | 44 | for (int i=0; i < iterations; i++){ 45 | float size = float(iterations - i); 46 | 47 | vec2 off1 = vec2(1.3846153846) * (direction * size); 48 | vec2 off2 = vec2(3.2307692308) * (direction * size); 49 | 50 | blurred_tex += texture(tex, uv) * 0.2270270270; 51 | blurred_tex += texture(tex, uv + (off1 / resolution)) * 0.3162162162; 52 | blurred_tex += texture(tex, uv - (off1 / resolution)) * 0.3162162162; 53 | blurred_tex += texture(tex, uv + (off2 / resolution)) * 0.0702702703; 54 | blurred_tex += texture(tex, uv - (off2 / resolution)) * 0.0702702703; 55 | } 56 | 57 | blurred_tex /= float(iterations) + 1.0; 58 | 59 | return blurred_tex; 60 | } 61 | 62 | // u/_NoDev_'s gaussian blur function 63 | // Discussion Link: https://www.reddit.com/r/godot/comments/klgfo9/help_with_shaders_in_gles2/ 64 | // Code Link: https://postimg.cc/7JDJw80d 65 | // 66 | // BLUR BLURRINESS (Default 8.0 - More is better but slower) 67 | // BLUR RADIUS (Default 1.5) 68 | // BLUR DIRECTION (Direction in which the blur is apllied, use vec2(1, 0) for first pass and vec2(0, 1) for second pass) 69 | // 70 | // Desc.: Really fast and GOOD FOR MOST CASES, but might NOT RUN IN THE WEB! 71 | // use 'texture_xorgaussian' instead if you found any issues. 72 | vec4 texture_nodevgaussian_singlepass(sampler2D tex, vec2 uv, vec2 pixel_size, float blurriness, float radius){ 73 | float pi = 3.14; 74 | float n = 0.0015; 75 | 76 | vec4 blurred_tex = vec4(0); 77 | 78 | float weight; 79 | for (float i = -blurriness; i <= blurriness; i++){ 80 | float d = i / pi; 81 | vec2 anchor = vec2(cos(d), sin(d)) * radius * i; 82 | vec2 directions = uv + pixel_size * anchor; 83 | blurred_tex += texture(tex, directions) * n; 84 | if (i <= 0.0) {n += 0.0015; } 85 | if (i > 0.0) {n -= 0.0015; } 86 | weight += n; 87 | } 88 | 89 | float norm = 1.0 / weight; 90 | blurred_tex *= norm; 91 | return blurred_tex; 92 | } 93 | vec4 texture_nodevgaussian_multipass(sampler2D tex, vec2 uv, vec2 pixel_size, float blurriness, vec2 direction){ 94 | float n = 0.0015; 95 | 96 | vec4 blurred_tex = vec4(0); 97 | 98 | float weight; 99 | for (float i = -blurriness; i <= blurriness; i++){ 100 | vec2 directions = uv + pixel_size * (direction * i); 101 | blurred_tex += texture(tex, directions) * n; 102 | if (i <= 0.0) {n += 0.0015; } 103 | if (i > 0.0) {n -= 0.0015; } 104 | weight += n; 105 | } 106 | 107 | float norm = 1.0 / weight; 108 | blurred_tex *= norm; 109 | return blurred_tex; 110 | } 111 | 112 | // -- EXAMPLE CODE -- // 113 | uniform int blur_type = 0; 114 | uniform int blur_amount = 16; 115 | uniform float blur_radius = 1; 116 | uniform vec2 blur_direction = vec2(1, 1); 117 | void fragment(){ 118 | vec2 uv = FRAGCOORD.xy / (1.0 / SCREEN_PIXEL_SIZE).xy; 119 | 120 | if (blur_type == 0) 121 | { 122 | vec4 xorgaussian = texture_xorgaussian(SCREEN_TEXTURE, uv, SCREEN_PIXEL_SIZE, float(blur_amount), 16, 4); 123 | COLOR = xorgaussian; 124 | } 125 | else if (blur_type == 1) 126 | { 127 | vec4 monksgaussian_multipass = texture_monksgaussian_multipass(SCREEN_TEXTURE, uv, SCREEN_PIXEL_SIZE, blur_amount, blur_direction); 128 | COLOR = monksgaussian_multipass; 129 | } 130 | else if (blur_type == 2) 131 | { 132 | vec4 nodevgaussian_singlepass = texture_nodevgaussian_singlepass(SCREEN_TEXTURE, uv, SCREEN_PIXEL_SIZE, float(blur_amount), blur_radius); 133 | COLOR = nodevgaussian_singlepass; 134 | } 135 | else if (blur_type == 3) 136 | { 137 | vec4 nodevgaussian_multipass = texture_nodevgaussian_multipass(SCREEN_TEXTURE, uv, SCREEN_PIXEL_SIZE, float(blur_amount), blur_direction); 138 | COLOR = nodevgaussian_multipass; 139 | } 140 | else 141 | { 142 | COLOR = texture(SCREEN_TEXTURE, uv); 143 | } 144 | } -------------------------------------------------------------------------------- /fox/libs/time-tools.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | 3 | extends Node 4 | 5 | # ------------------------------------------------------------------------------ 6 | 7 | class_name TimeTools 8 | 9 | # ------------------------------------------------------------------------------ 10 | 11 | # returns a yyyymmdd number: e.g: 20240820 12 | static func dateTimeToYYYYMMDDNumber(datetime: Dictionary): 13 | var yyyymmdd = ( 14 | str(datetime.year) 15 | + str(datetime.month).pad_zeros(2) 16 | + str(datetime.day).pad_zeros(2) 17 | ) 18 | 19 | return int(yyyymmdd) 20 | 21 | # ------------------------------------------------------------------------------ 22 | 23 | # returns a yyyymm number: e.g: 202408 24 | static func dateTimeToYYYYMMNumber(datetime: Dictionary): 25 | var yyyymm = ( 26 | str(datetime.year) 27 | + str(datetime.month).pad_zeros(2) 28 | ) 29 | 30 | return int(yyyymm) 31 | 32 | # ------------------------------------------------------------------------------ 33 | 34 | # e.g: 2024-03-13 10:29 35 | static func dateTimeToReadableDate(datetime: Dictionary): 36 | var readableDate = ( 37 | str(datetime.year) + '-' 38 | + str(datetime.month).pad_zeros(2)+ '-' 39 | + str(datetime.day).pad_zeros(2) + ' ' 40 | + str(datetime.hour).pad_zeros(2) + ':' 41 | + str(datetime.minute).pad_zeros(2) 42 | ) 43 | 44 | return readableDate 45 | 46 | # ------------------------------------------------------------------------------ 47 | 48 | # returns today as yyyymmdd number: e.g: 20240820 49 | static func getDeviceTodayNumUTC(): 50 | var unixTimeUTCSec = Time.get_unix_time_from_system() 51 | var datetimeUTC = Time.get_datetime_dict_from_unix_time(unixTimeUTCSec) 52 | var todayNum = dateTimeToYYYYMMDDNumber(datetimeUTC) 53 | return todayNum 54 | 55 | # ------------------------------------------------------------------------------ 56 | 57 | static func getTimeRemainingForSeason(): 58 | var timestampUTCSec = Time.get_unix_time_from_system() 59 | var datetime = Time.get_datetime_dict_from_unix_time(timestampUTCSec) 60 | 61 | datetime.month += 1 62 | if datetime.month > 12: 63 | datetime.month = 1 64 | datetime.year += 1 65 | 66 | datetime.day = 1 67 | datetime.hour = 0 68 | datetime.minute = 0 69 | datetime.second = 0 70 | 71 | var nextSeasonStartTimestamp = Time.get_unix_time_from_datetime_dict(datetime) 72 | var diffSec = int(floor(nextSeasonStartTimestamp - timestampUTCSec)) 73 | 74 | @warning_ignore('INTEGER_DIVISION') 75 | var days = int(diffSec / (60 * 60 * 24)) 76 | 77 | return { 78 | nbDays = days, 79 | timeBeforeMidnight = getTimeRemainingForToday() 80 | } 81 | 82 | # ------------------------------------------------------------------------------ 83 | 84 | static func nbDaysInMonth(month, year): 85 | if month in [1, 3, 5, 7, 8, 10, 12]: 86 | return 31 87 | elif month in [4, 6, 9, 11]: 88 | return 30 89 | elif month == 2: 90 | if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0): 91 | return 29 92 | else: 93 | return 28 94 | else: 95 | return 0 96 | 97 | # ------------------------------------------------------------------------------ 98 | 99 | static func getTimeRemainingForThisWeek(): 100 | var timestampUTCSec = Time.get_unix_time_from_system() 101 | var datetime = Time.get_datetime_dict_from_unix_time(timestampUTCSec) 102 | 103 | var current_day_of_week = datetime.weekday # Jour de la semaine actuel (0 est dimanche) 104 | 105 | var days_until_next_monday = (7 - current_day_of_week + 1) % 7 106 | if days_until_next_monday == 0: 107 | days_until_next_monday = 7 # Si aujourd'hui est lundi, le prochain lundi est dans 7 jours 108 | 109 | 110 | var nextMonday = datetime 111 | nextMonday.day += days_until_next_monday 112 | if nextMonday.day > nbDaysInMonth(nextMonday.month, nextMonday.year): 113 | nextMonday.day = nextMonday.day - nbDaysInMonth(nextMonday.month, nextMonday.year) 114 | nextMonday.month += 1 115 | if nextMonday.month > 12: 116 | nextMonday.month = 1 117 | nextMonday.year += 1 118 | 119 | nextMonday.hour = 0 120 | nextMonday.minute = 0 121 | nextMonday.second = 0 122 | 123 | var nextMondayStartTimestamp = Time.get_unix_time_from_datetime_dict(nextMonday) 124 | var diffSec = int(floor(nextMondayStartTimestamp - timestampUTCSec)) 125 | 126 | @warning_ignore('INTEGER_DIVISION') 127 | var days = int(diffSec / (60 * 60 * 24)) 128 | 129 | return { 130 | nbDays = days, 131 | timeBeforeMidnight = getTimeRemainingForToday() 132 | } 133 | 134 | # ------------------------------------------------------------------------------ 135 | 136 | static func getTimeRemainingForToday(): 137 | var timestampUTCSec = Time.get_unix_time_from_system() 138 | var datetime = Time.get_datetime_dict_from_unix_time(timestampUTCSec) 139 | 140 | datetime.day += 1 141 | if datetime.day > nbDaysInMonth(datetime.month, datetime.year): 142 | datetime.day = 1 143 | datetime.month += 1 144 | if datetime.month > 12: 145 | datetime.month = 1 146 | datetime.year += 1 147 | 148 | datetime.hour = 0 149 | datetime.minute = 0 150 | datetime.second = 0 151 | 152 | var nextDayStartTimestamp = Time.get_unix_time_from_datetime_dict(datetime) 153 | var diffSec = int(floor(nextDayStartTimestamp - timestampUTCSec)) 154 | 155 | @warning_ignore('INTEGER_DIVISION') 156 | var hours = int((diffSec % (60 * 60 * 24)) / (60 * 60)) 157 | @warning_ignore('INTEGER_DIVISION') 158 | var minutes = int((diffSec % (60 * 60)) / 60) 159 | var seconds = int(diffSec) % 60 160 | 161 | return (str(hours).pad_zeros(2) + ':' 162 | + str(minutes).pad_zeros(2) + ':' 163 | + str(seconds).pad_zeros(2)) 164 | -------------------------------------------------------------------------------- /fox/components/blur.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://7qqtdv7ky8f5"] 2 | 3 | [sub_resource type="Shader" id="Shader_v1heq"] 4 | code = "shader_type canvas_item; 5 | 6 | uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap; 7 | // Xor's gausian blur function 8 | // Link: https://xorshaders.weebly.com/tutorials/blur-shaders-5-part-2 9 | // Defaults from: https://www.shadertoy.com/view/Xltfzj 10 | // 11 | // BLUR BLURRINESS (Default 8.0) 12 | // BLUR ITERATIONS (Default 16.0 - More is better but slower) 13 | // BLUR QUALITY (Default 4.0 - More is better but slower) 14 | // 15 | // Desc.: Don't have the best performance but will run on almost 16 | // anything, although, if developing for mobile, is better to use 17 | // 'texture_nodevgaussian(...) instead'. 18 | vec4 texture_xorgaussian(sampler2D tex, vec2 uv, vec2 pixel_size, float blurriness, int iterations, int quality){ 19 | float pi = 6.28; 20 | 21 | vec2 radius = blurriness / (1.0 / pixel_size).xy; 22 | vec4 blurred_tex = texture(tex, uv); 23 | 24 | for(float d = 0.0; d < pi; d += pi / float(iterations)){ 25 | for( float i = 1.0 / float(quality); i <= 1.0; i += 1.0 / float(quality) ){ 26 | vec2 directions = uv + vec2(cos(d), sin(d)) * radius * i; 27 | blurred_tex += texture(tex, directions); 28 | } 29 | } 30 | blurred_tex /= float(quality) * float(iterations) + 1.0; 31 | 32 | return blurred_tex; 33 | } 34 | 35 | // Experience-Monks' fast gaussian blur function 36 | // Link: https://github.com/Experience-Monks/glsl-fast-gaussian-blur/ 37 | // 38 | // BLUR ITERATIONS (Default 16.0 - More is better but slower) 39 | // BLUR DIRECTION (Direction in which the blur is apllied, use vec2(1, 0) for first pass and vec2(0, 1) for second pass) 40 | // 41 | // Desc.: ACTUALLY PRETTY SLOW but still pretty good for custom cinematic 42 | // bloom effects, since this needs render 2 passes 43 | vec4 texture_monksgaussian_multipass(sampler2D tex, vec2 uv, vec2 pixel_size, int iterations, vec2 direction){ 44 | vec4 blurred_tex = vec4(0.0); 45 | vec2 resolution = 1.0 / pixel_size; 46 | 47 | for (int i=0; i < iterations; i++){ 48 | float size = float(iterations - i); 49 | 50 | vec2 off1 = vec2(1.3846153846) * (direction * size); 51 | vec2 off2 = vec2(3.2307692308) * (direction * size); 52 | 53 | blurred_tex += texture(tex, uv) * 0.2270270270; 54 | blurred_tex += texture(tex, uv + (off1 / resolution)) * 0.3162162162; 55 | blurred_tex += texture(tex, uv - (off1 / resolution)) * 0.3162162162; 56 | blurred_tex += texture(tex, uv + (off2 / resolution)) * 0.0702702703; 57 | blurred_tex += texture(tex, uv - (off2 / resolution)) * 0.0702702703; 58 | } 59 | 60 | blurred_tex /= float(iterations) + 1.0; 61 | 62 | return blurred_tex; 63 | } 64 | 65 | // u/_NoDev_'s gaussian blur function 66 | // Discussion Link: https://www.reddit.com/r/godot/comments/klgfo9/help_with_shaders_in_gles2/ 67 | // Code Link: https://postimg.cc/7JDJw80d 68 | // 69 | // BLUR BLURRINESS (Default 8.0 - More is better but slower) 70 | // BLUR RADIUS (Default 1.5) 71 | // BLUR DIRECTION (Direction in which the blur is apllied, use vec2(1, 0) for first pass and vec2(0, 1) for second pass) 72 | // 73 | // Desc.: Really fast and GOOD FOR MOST CASES, but might NOT RUN IN THE WEB! 74 | // use 'texture_xorgaussian' instead if you found any issues. 75 | vec4 texture_nodevgaussian_singlepass(sampler2D tex, vec2 uv, vec2 pixel_size, float blurriness, float radius){ 76 | float pi = 3.14; 77 | float n = 0.0015; 78 | 79 | vec4 blurred_tex = vec4(0); 80 | 81 | float weight; 82 | for (float i = -blurriness; i <= blurriness; i++){ 83 | float d = i / pi; 84 | vec2 anchor = vec2(cos(d), sin(d)) * radius * i; 85 | vec2 directions = uv + pixel_size * anchor; 86 | blurred_tex += texture(tex, directions) * n; 87 | if (i <= 0.0) {n += 0.0015; } 88 | if (i > 0.0) {n -= 0.0015; } 89 | weight += n; 90 | } 91 | 92 | float norm = 1.0 / weight; 93 | blurred_tex *= norm; 94 | return blurred_tex; 95 | } 96 | vec4 texture_nodevgaussian_multipass(sampler2D tex, vec2 uv, vec2 pixel_size, float blurriness, vec2 direction){ 97 | float n = 0.0015; 98 | 99 | vec4 blurred_tex = vec4(0); 100 | 101 | float weight; 102 | for (float i = -blurriness; i <= blurriness; i++){ 103 | vec2 directions = uv + pixel_size * (direction * i); 104 | blurred_tex += texture(tex, directions) * n; 105 | if (i <= 0.0) {n += 0.0015; } 106 | if (i > 0.0) {n -= 0.0015; } 107 | weight += n; 108 | } 109 | 110 | float norm = 1.0 / weight; 111 | blurred_tex *= norm; 112 | return blurred_tex; 113 | } 114 | 115 | // -- EXAMPLE CODE -- // 116 | uniform int blur_type = 0; 117 | uniform int blur_amount = 16; 118 | uniform float blur_radius = 1; 119 | uniform vec2 blur_direction = vec2(1, 1); 120 | void fragment(){ 121 | vec2 uv = FRAGCOORD.xy / (1.0 / SCREEN_PIXEL_SIZE).xy; 122 | 123 | if (blur_type == 0) 124 | { 125 | vec4 xorgaussian = texture_xorgaussian(SCREEN_TEXTURE, uv, SCREEN_PIXEL_SIZE, float(blur_amount), 16, 4); 126 | COLOR = xorgaussian; 127 | } 128 | else if (blur_type == 1) 129 | { 130 | vec4 monksgaussian_multipass = texture_monksgaussian_multipass(SCREEN_TEXTURE, uv, SCREEN_PIXEL_SIZE, blur_amount, blur_direction); 131 | COLOR = monksgaussian_multipass; 132 | } 133 | else if (blur_type == 2) 134 | { 135 | vec4 nodevgaussian_singlepass = texture_nodevgaussian_singlepass(SCREEN_TEXTURE, uv, SCREEN_PIXEL_SIZE, float(blur_amount), blur_radius); 136 | COLOR = nodevgaussian_singlepass; 137 | } 138 | else if (blur_type == 3) 139 | { 140 | vec4 nodevgaussian_multipass = texture_nodevgaussian_multipass(SCREEN_TEXTURE, uv, SCREEN_PIXEL_SIZE, float(blur_amount), blur_direction); 141 | COLOR = nodevgaussian_multipass; 142 | } 143 | else 144 | { 145 | COLOR = texture(SCREEN_TEXTURE, uv); 146 | } 147 | }" 148 | 149 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_23ij8"] 150 | resource_local_to_scene = true 151 | shader = SubResource("Shader_v1heq") 152 | shader_parameter/blur_type = 0 153 | shader_parameter/blur_amount = 27 154 | shader_parameter/blur_radius = 1.0 155 | shader_parameter/blur_direction = Vector2(1, 1) 156 | 157 | [node name="blur" type="ColorRect"] 158 | material = SubResource("ShaderMaterial_23ij8") 159 | anchors_preset = 15 160 | anchor_right = 1.0 161 | anchor_bottom = 1.0 162 | mouse_filter = 2 163 | color = Color(0.258824, 0.258824, 0.258824, 1) 164 | -------------------------------------------------------------------------------- /cli/bundler/export.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import inquirer from 'inquirer'; 7 | import shell from 'shelljs'; 8 | import {spawn} from 'child_process'; 9 | 10 | // ----------------------------------------------------------------------------- 11 | 12 | import updatePreset from './update-preset.js'; 13 | import switchBundle from './switch.js'; 14 | import {readPresets, writePresets} from './read-presets.js'; 15 | import { getNextVersion, increasePackageVersion, increasePresetsVersion } from './versioning.js'; 16 | 17 | // ----------------------------------------------------------------------------- 18 | 19 | const INCREASE_SEMVER_LEVELS = ['patch', 'minor', 'major']; 20 | 21 | // ----------------------------------------------------------------------------- 22 | 23 | export const androidExtension = (env) => env === 'release' ? '.aab' : '.apk' 24 | 25 | export const getApplicationName = (coreConfig, bundle) => { 26 | const {title} = coreConfig; 27 | const {subtitle} = bundle; 28 | return subtitle ? `${title}: ${subtitle}` : title; 29 | } 30 | 31 | export const getTitle = (coreConfig) => coreConfig.title; 32 | export const getSubtitle = (bundle) => { 33 | const {subtitle} = bundle; 34 | return subtitle; 35 | } 36 | 37 | // ----------------------------------------------------------------------------- 38 | 39 | const inquireVersioning = async (currentVersion) => { 40 | const questions = [ 41 | { 42 | message: 'version', 43 | name: 'versionLevel', 44 | type: 'list', 45 | choices: [`${currentVersion}`, ...INCREASE_SEMVER_LEVELS] 46 | } 47 | ]; 48 | 49 | const answers = await inquirer.prompt(questions); 50 | const {versionLevel} = answers; 51 | return {versionLevel}; 52 | }; 53 | 54 | // ----------------------------------------------------------------------------- 55 | 56 | const verifyBuildFolder = () => { 57 | const buildFolder = path.resolve(process.cwd(), './_build'); 58 | 59 | if (!fs.existsSync(buildFolder)) { 60 | shell.mkdir('-p', buildFolder); 61 | console.log('✅ created _build folder'); 62 | } 63 | }; 64 | 65 | // ----------------------------------------------------------------------------- 66 | 67 | const unzipIPA = (bundleName) => { 68 | console.log(`⚙️ Unzipping ${bundleName}.app...`); 69 | 70 | const absolutePath = `${path.resolve(process.cwd())}/_build/iOS` 71 | shell.exec(`tar -xf ${absolutePath}/${bundleName}.ipa -C _build/iOS`) 72 | shell.rm('-rf', `${absolutePath}/${bundleName}.app`) 73 | shell.exec(`mv ${absolutePath}/Payload/${bundleName}.app ${absolutePath}/${bundleName}.app`) 74 | shell.rm('-rf', `${absolutePath}/Payload`) 75 | 76 | console.log(`\n${chalk.blue.bold(`_build/iOS/${bundleName}.app`)} is ready for your device, read https://github.com/uralys/fox/blob/master/docs/exporting/ios.md#install-and-run-a-debug-build-on-a-device`); 77 | console.log(`xcrun devicectl device install app _build/iOS/${bundleName}.app --device XXX`); 78 | }; 79 | 80 | // ----------------------------------------------------------------------------- 81 | 82 | const exportBundle = async (settings) => { 83 | const {core: coreConfig, bundles} = settings; 84 | console.log(`⚙️ exporting a ${chalk.blue.bold('bundle')}...`); 85 | 86 | if (!bundles) { 87 | console.log('\nmissing bundles in fox.config.json'); 88 | console.log(chalk.red.bold('🔴 failed')); 89 | return; 90 | } 91 | 92 | // --------- 93 | 94 | verifyBuildFolder(); 95 | 96 | // --------- 97 | 98 | const packageJSON = JSON.parse(fs.readFileSync('./package.json', 'utf8')); 99 | const currentVersion = packageJSON.version; 100 | 101 | // --------- 102 | 103 | const presets = readPresets(); 104 | if (!presets) { 105 | console.log(chalk.red.bold('🔴 failed during reading presets.')); 106 | return; 107 | } 108 | 109 | // --------- 110 | 111 | const {versionLevel} = await inquireVersioning(currentVersion); 112 | const upgrading = versionLevel !== currentVersion; 113 | 114 | let newVersion = currentVersion 115 | 116 | if(upgrading) { 117 | newVersion = getNextVersion(currentVersion, versionLevel) 118 | increasePresetsVersion(newVersion, presets) 119 | increasePackageVersion(newVersion, versionLevel) 120 | } 121 | 122 | // --------- 123 | 124 | const bundleSettings = await switchBundle(settings, presets); 125 | if (!bundleSettings) { 126 | console.log(chalk.red.bold('🔴 failed during bundle settings preparation.')); 127 | return; 128 | } 129 | 130 | const {bundleId, preset, env} = bundleSettings; 131 | 132 | // --------- 133 | 134 | const bundleInfo = `${chalk.blue.bold(bundleId)} (${chalk.blue.bold( 135 | newVersion 136 | )}) for ${chalk.blue.bold(preset.name)}`; 137 | 138 | console.log(`\n⚙️ Ready to bundle ${bundleInfo}`); 139 | 140 | const {applicationName, bundleName} = updatePreset( 141 | bundleId, 142 | env, 143 | coreConfig, 144 | preset, 145 | bundles[bundleId], 146 | newVersion 147 | ); 148 | 149 | writePresets(presets); 150 | 151 | // --------- 152 | 153 | const exportType = `--export-${env === 'release' ? 'release' : 'debug'}`; 154 | console.log(`\n⚙️ Exporting with ${exportType}...`); 155 | 156 | const bundler = spawn(coreConfig.godot, [exportType, preset.name, '--headless'], { 157 | stdio: [process.stdin, process.stdout, process.stderr] 158 | }); 159 | 160 | bundler.on('close', () => { 161 | console.log(`\n${chalk.green.bold(applicationName)}`); 162 | console.log(`✅ Exported ${bundleInfo} successfully!`); 163 | 164 | if (preset.platform === 'iOS') { 165 | if(env === 'debug' || env === 'staging') { 166 | unzipIPA(bundleName); 167 | } 168 | 169 | console.log(`\n${chalk.blue.bold(`_build/iOS/${bundleName}.xcodeproj`)} is ready to be used with XCode`); 170 | } 171 | 172 | if (preset.platform === 'Android') { 173 | console.log(`\n${chalk.blue.bold(`_build/android/${bundleName}${androidExtension(env)}`)} is ready`); 174 | console.log(`adb install -r _build/android/${bundleName}${androidExtension(env)}`); 175 | } 176 | }); 177 | }; 178 | 179 | // ----------------------------------------------------------------------------- 180 | 181 | export default exportBundle; 182 | -------------------------------------------------------------------------------- /fox/behaviours/interactiveArea2D.gd: -------------------------------------------------------------------------------- 1 | extends Area2D 2 | 3 | # ------------------------------------------------------------------------------ 4 | 5 | var draggable 6 | var parentReference 7 | var additionalDragData 8 | var params = {} 9 | var mouseStartPosition 10 | var screenStartPosition 11 | var useBoundaries ## usually the draggable itself to use its size 12 | 13 | # ------------------------------------------------------------------------------ 14 | 15 | @export var inputPriority: int = 0 # the lower the more priority 16 | @export var minDragTime: int = 20 17 | @export var minPressTime: int = 150 18 | @export var longPressTime: int = 500 19 | 20 | @export var dragAfterLongPress: bool = false 21 | @export var useManualDragStart: bool = false 22 | @export var type = 'default' 23 | 24 | # ------------------------------------------------------------------------------ 25 | 26 | var _dragging = false 27 | var _pressing = false 28 | 29 | var isPressing = false 30 | var isLongPressing = false 31 | var lastPress = Time.get_ticks_msec() 32 | 33 | # ------------------------------------------------------------------------------ 34 | 35 | signal pressed 36 | signal pressing 37 | signal stopPressing 38 | signal longPress 39 | 40 | signal startedDragging 41 | 42 | @warning_ignore("UNUSED_SIGNAL") 43 | signal droppedOnDroppable 44 | 45 | @warning_ignore("UNUSED_SIGNAL") 46 | signal droppedIntheWild 47 | 48 | @warning_ignore("UNUSED_SIGNAL") 49 | signal foundDroppable 50 | 51 | @warning_ignore("UNUSED_SIGNAL") 52 | signal leftDroppable 53 | 54 | # ------------------------------------------------------------------------------ 55 | 56 | func _ready(): 57 | connect("input_event", onInput) 58 | 59 | # ------------------------------------------------------------------------------ 60 | 61 | func _physics_process(_delta): 62 | if _pressing: 63 | if(Gesture.shouldConcedePriority(self)): 64 | resetInteraction() 65 | return 66 | 67 | if(Gesture.isDragging() and Gesture.currentDraggable() != draggable): 68 | resetInteraction() 69 | return 70 | 71 | var now = Time.get_ticks_msec() 72 | var elapsedTime = now - lastPress 73 | var mousePosition = get_global_mouse_position() 74 | 75 | var mouseDiff = mousePosition - mouseStartPosition 76 | var minMouseDragTresholdReached = (mouseDiff).length() > 3 77 | 78 | if(draggable \ 79 | and not _dragging 80 | and minMouseDragTresholdReached \ 81 | and not dragAfterLongPress \ 82 | and not useManualDragStart \ 83 | and elapsedTime > minDragTime): 84 | _startDragging() 85 | 86 | if(not _dragging and not isPressing and elapsedTime > minPressTime): 87 | var _accepted = Gesture.acceptTouchable(self) 88 | if(_accepted): 89 | isPressing = true 90 | pressing.emit() 91 | else: 92 | resetInteraction() 93 | 94 | if(not _dragging and not isLongPressing and elapsedTime > longPressTime): 95 | var _accepted = Gesture.acceptTouchable(self) 96 | if(_accepted): 97 | isLongPressing = true 98 | longPress.emit() 99 | 100 | if(draggable \ 101 | and minMouseDragTresholdReached \ 102 | and dragAfterLongPress \ 103 | and not useManualDragStart): 104 | _startDragging() 105 | 106 | else: 107 | resetInteraction() 108 | 109 | 110 | if(_dragging): 111 | var zoom = parentReference.scale.x if parentReference else 1 112 | var newPosition = mouseDiff / zoom + screenStartPosition 113 | 114 | if(useBoundaries): 115 | var draggableWidth = useBoundaries.get_rect().size.x * draggable.scale.x 116 | var draggableHeight = useBoundaries.get_rect().size.y * draggable.scale.y 117 | 118 | var xMin = G.W - draggableWidth/2 119 | var xMax = draggableWidth/2 120 | var yMin = G.H - draggableHeight/2 121 | var yMax = draggableHeight/2 122 | 123 | newPosition.x = min(max(newPosition.x, xMin), xMax) 124 | newPosition.y = min(max(newPosition.y, yMin), yMax) 125 | 126 | draggable.position = lerp(draggable.position, newPosition, 25 * _delta) 127 | 128 | # ------------------------------------------------------------------------------ 129 | 130 | func onInput(_viewport, event, _shape_idx): 131 | # ---------- mouse down ---------- 132 | if event is InputEventMouseButton \ 133 | and event.button_index == MOUSE_BUTTON_LEFT \ 134 | and event.pressed: 135 | lastPress = Time.get_ticks_msec() 136 | mouseStartPosition = get_global_mouse_position() 137 | _pressing = true 138 | 139 | var globalPosition = get_parent().global_position 140 | var touchDistance = (mouseStartPosition - globalPosition).length() 141 | 142 | var pressEvent = { 143 | zIndex = get_parent().z_index, 144 | touchable = self, 145 | from = get_parent(), 146 | touchDistance = touchDistance 147 | } 148 | 149 | Gesture.addPressedItem(pressEvent) 150 | 151 | # ------------------------------------------------------------------------------ 152 | 153 | func _startDragging(): 154 | manualStartDragging() 155 | startedDragging.emit() 156 | 157 | # ------------------------------------------------------------------------------ 158 | 159 | func resetInteraction(): 160 | if(isPressing): 161 | stopPressing.emit() 162 | 163 | _dragging = false 164 | _pressing = false 165 | 166 | isPressing = false 167 | isLongPressing = false 168 | 169 | Gesture.removePressedItem(self) 170 | 171 | # ------------------------------------------------------------------------------ 172 | 173 | func _unhandled_input(event): 174 | if _pressing \ 175 | and event is InputEventMouseButton \ 176 | and event.button_index == MOUSE_BUTTON_LEFT \ 177 | and !event.pressed: 178 | 179 | if(_dragging): 180 | Gesture.handleDraggingEnd() 181 | else: 182 | var _accepted = Gesture.acceptTouchable(self) 183 | if(_accepted): 184 | pressed.emit() 185 | 186 | resetInteraction() 187 | 188 | # ------------------------------------------------------------------------------ 189 | 190 | func manualStartDragging(): 191 | if(not draggable): 192 | G.log('[color=pink]You must set your draggable object before to use dragging. Use prepareDraggable()[/color]') 193 | return 194 | 195 | _pressing = true 196 | _dragging = true 197 | mouseStartPosition = get_global_mouse_position() 198 | 199 | Gesture.startDragging(self, draggable, additionalDragData) 200 | 201 | screenStartPosition = draggable.position 202 | 203 | # ------------------------------------------------------------------------------ 204 | 205 | func prepareDraggable(_options): 206 | draggable = __.Get('draggable', _options) 207 | 208 | if(!draggable): 209 | G.log('[color=pink]You must pass your draggable within the options: prepareDraggable({draggable=item})[/color]') 210 | return 211 | 212 | type = __.GetOr('default', 'type', _options) 213 | parentReference = __.Get('parentReference', _options) 214 | useBoundaries = __.Get('useBoundaries', _options) 215 | useManualDragStart = __.GetOr(false, 'useManualDragStart', _options) 216 | 217 | # ------------------------------------------------------------------------------ 218 | 219 | func resetDraggingPosition(): 220 | screenStartPosition = draggable.position 221 | 222 | func resetDraggable(): 223 | draggable = null 224 | -------------------------------------------------------------------------------- /fox/libs/underscore.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | 3 | extends Node 4 | 5 | # ------------------------------------------------------------------------------ 6 | 7 | class_name __ 8 | 9 | # ------------------------------------------------------------------------------ 10 | 11 | static func Get(path, obj): 12 | if(obj == null): 13 | return null 14 | 15 | if(path == null): 16 | return obj 17 | 18 | if(typeof(path) != TYPE_STRING): 19 | G.log('❌ [b][color=pink] __.Get(path, obj): path must be of TYPE_STRING[/color][/b] '); 20 | return null 21 | 22 | if(typeof(obj) != TYPE_DICTIONARY and typeof(obj) != TYPE_OBJECT): 23 | G.log('❌ [b][color=pink] __.Get(path, obj): obj must be either a TYPE_OBJECT or a TYPE_DICTIONARY[/color][/b] '); 24 | G.log('found: ', {obj=obj}); 25 | return null 26 | 27 | # --- 28 | 29 | var res = obj 30 | var fields = Array(path.split('.')) 31 | 32 | # --- 33 | 34 | while(fields.size() > 0): 35 | var field = fields.pop_front() 36 | res = res.get(field) 37 | if(res == null): 38 | return null 39 | 40 | return res 41 | 42 | # ------------------------------------------------------------------------------ 43 | 44 | static func GetOr(defaultValue, path, obj): 45 | var res = Get(path, obj) 46 | return res if res != null else defaultValue 47 | 48 | # ------------------------------------------------------------------------------ 49 | 50 | static func Set(value, path, obj): 51 | if(not path): 52 | return obj 53 | 54 | if(typeof(obj) == TYPE_OBJECT): 55 | obj.set_indexed(path, value) 56 | else: 57 | if '.' in path: 58 | var fields = Array(path.split('.')) 59 | var first = fields.pop_front() 60 | Set(value, ".".join(fields), obj[first]) 61 | else: 62 | obj[path] = value 63 | 64 | # ------------------------------------------------------------------------------ 65 | 66 | # example: __.useColor('#A1553E') 67 | static func useColor(colorHex: String) -> Color: 68 | var cleanHex = colorHex.replace("#", "") 69 | var r = 0; var b = 0; var g = 0; var a = 255; 70 | 71 | if cleanHex.length() == 3: 72 | cleanHex = "%s%s%s%s%s%s" % [cleanHex[0], cleanHex[0], cleanHex[1], cleanHex[1], cleanHex[2], cleanHex[2]] 73 | 74 | elif cleanHex.length() == 4: 75 | cleanHex = "%s%s%s%s%s%s" % [cleanHex[0], cleanHex[0], cleanHex[1], cleanHex[1], cleanHex[2], cleanHex[2] , cleanHex[3], cleanHex[3]] 76 | 77 | elif cleanHex.length() == 6: 78 | r = cleanHex.substr(0, 2).hex_to_int() 79 | g = cleanHex.substr(2, 2).hex_to_int() 80 | b = cleanHex.substr(4, 2).hex_to_int() 81 | 82 | elif cleanHex.length() == 8: 83 | r = cleanHex.substr(0, 2).hex_to_int() 84 | g = cleanHex.substr(2, 2).hex_to_int() 85 | b = cleanHex.substr(4, 2).hex_to_int() 86 | a = cleanHex.substr(6, 2).hex_to_int() 87 | 88 | else: 89 | G.log('❌ [b][color=pink]check your color.[/color][/b] ', {colorHex=colorHex}); 90 | return Color(r, b, g) # Retourne du noir en cas d'erreur. 91 | 92 | var red = r / 255.0 93 | var green = g / 255.0 94 | var blue = b / 255.0 95 | var alpha = a / 255.0 96 | 97 | return Color(red, green, blue, alpha) 98 | 99 | # ------------------------------------------------------------------------------ 100 | 101 | # from https://github.com/Calinou/godot-bbcode-to-ansi 102 | static func bbcodeToANSI(bbcode: String) -> String: 103 | var res = (bbcode 104 | # Bold. 105 | .replace("[b]", "\u001b[1m") 106 | .replace("[/b]", "\u001b[22m") 107 | # Italic. 108 | .replace("[i]", "\u001b[3m") 109 | .replace("[/i]", "\u001b[23m") 110 | # Underline. 111 | .replace("[u]", "\u001b[4m") 112 | .replace("[/u]", "\u001b[24m") 113 | # Strikethrough. 114 | .replace("[s]", "\u001b[9m") 115 | .replace("[/s]", "\u001b[29m") 116 | # Indentation (looks equivalent to 4 spaces). 117 | .replace("[indent]", " ") 118 | .replace("[/indent]", "") 119 | 120 | # Code. 121 | # Terminal fonts are already fixed-width, so use faint coloring to distinguish it 122 | # from normal text. 123 | .replace("[code]", "\u001b[2m") 124 | .replace("[/code]", "\u001b[22m") 125 | 126 | # Without knowing the terminal width, we can't fully emulate [center] and [right] behavior. 127 | # This is only an approximation that doesn't take the terminal width into account. 128 | .replace("[center]", "\n\t\t\t") 129 | .replace("[/center]", "") 130 | .replace("[right]", "\n\t\t\t\t\t\t") 131 | .replace("[/right]", "") 132 | 133 | # URL (link). 134 | # Only unnamed URLs can be universally supported in terminals (by letting the terminal 135 | # recognize it as-is). As of April 2022, support for named URLs is still in progress 136 | # for many popular terminals. 137 | .replace("[url]", "") 138 | .replace("[/url]", "") 139 | 140 | # Text color. 141 | .replace("[color=black]", "\u001b[30m") 142 | .replace("[color=red]", "\u001b[91m") 143 | .replace("[color=green]", "\u001b[92m") 144 | .replace("[color=lime]", "\u001b[92m") 145 | .replace("[color=yellow]", "\u001b[93m") 146 | .replace("[color=blue]", "\u001b[94m") 147 | .replace("[color=magenta]", "\u001b[95m") 148 | .replace("[color=pink]", "\u001b[38;5;218m") 149 | .replace("[color=purple]", "\u001b[38;5;98m") 150 | .replace("[color=cyan]", "\u001b[96m") 151 | .replace("[color=white]", "\u001b[97m") 152 | .replace("[color=orange]", "\u001b[38;5;208m") 153 | .replace("[color=gray]", "\u001b[90m") 154 | .replace("[/color]", "\u001b[39m") 155 | 156 | # Background color (highlighting text). 157 | .replace("[bgcolor=black]", "\u001b[40m") 158 | .replace("[bgcolor=red]", "\u001b[101m") 159 | .replace("[bgcolor=green]", "\u001b[102m") 160 | .replace("[bgcolor=lime]", "\u001b[102m") 161 | .replace("[bgcolor=yellow]", "\u001b[103m") 162 | .replace("[bgcolor=blue]", "\u001b[104m") 163 | .replace("[bgcolor=magenta]", "\u001b[105m") 164 | .replace("[bgcolor=pink]", "\u001b[48;5;218m") 165 | .replace("[bgcolor=purple]", "\u001b[48;5;98m") 166 | .replace("[bgcolor=cyan]", "\u001b[106m") 167 | .replace("[bgcolor=white]", "\u001b[107m") 168 | .replace("[bgcolor=orange]", "\u001b[48;5;208m") 169 | .replace("[bgcolor=gray]", "\u001b[100m") 170 | .replace("[/bgcolor]", "\u001b[49m") 171 | 172 | # Foreground color (redacting text). 173 | # Emulated by using the same color for both foreground and background. 174 | .replace("[fgcolor=black]", "\u001b[30;40m") 175 | .replace("[fgcolor=red]", "\u001b[91;101m") 176 | .replace("[fgcolor=green]", "\u001b[92;102m") 177 | .replace("[fgcolor=lime]", "\u001b[92;102m") 178 | .replace("[fgcolor=yellow]", "\u001b[93;103m") 179 | .replace("[fgcolor=blue]", "\u001b[94;104m") 180 | .replace("[fgcolor=magenta]", "\u001b[95;105m") 181 | .replace("[fgcolor=pink]", "\u001b[38;5;218;48;5;218m") 182 | .replace("[fgcolor=purple]", "\u001b[38;5;98;48;5;98m") 183 | .replace("[fgcolor=cyan]", "\u001b[96;106m") 184 | .replace("[fgcolor=white]", "\u001b[97;107m") 185 | .replace("[fgcolor=orange]", "\u001b[38;5;208;48;5;208m") 186 | .replace("[fgcolor=gray]", "\u001b[90;100m") 187 | .replace("[/fgcolor]", "\u001b[39;49m") 188 | ) 189 | 190 | return res 191 | -------------------------------------------------------------------------------- /cli/bundler/ini.js: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | The ISC License 3 | 4 | Copyright (c) Isaac Z. Schlueter and Contributors 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 16 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | ------------------------------------------------------------------------------*/ 18 | 19 | /* 20 | Note: the `safe()` function is not working as expected with Godot .cfg files 21 | I had to use a specific one to write string values properly. 22 | hence this `safeGodotValue()`: 23 | */ 24 | const safeGodotValue = (val) => { 25 | if (typeof val !== 'string' || isNumber(val) || isGodotObject(val)) { 26 | return val; 27 | } 28 | 29 | return JSON.stringify(val); 30 | }; 31 | 32 | const isNumber = (val) => val.length > 0 && !isNaN(val); 33 | const isGodotObject = (val) => 34 | ['PoolStringArray', 'Color'].reduce((acc, objectType) => acc || val.includes(objectType), false); 35 | 36 | // ----------------------------------------------------------------------------- 37 | // everything else is from https://github.com/npm/ini/ 38 | // ----------------------------------------------------------------------------- 39 | 40 | const {hasOwnProperty} = Object.prototype; 41 | 42 | const eol = typeof process !== 'undefined' && process.platform === 'win32' ? '\r\n' : '\n'; 43 | 44 | const encode = (obj, opt) => { 45 | const children = []; 46 | let out = ''; 47 | 48 | if (typeof opt === 'string') { 49 | opt = { 50 | section: opt, 51 | whitespace: false 52 | }; 53 | } else { 54 | opt = opt || Object.create(null); 55 | opt.whitespace = opt.whitespace === true; 56 | } 57 | 58 | const separator = opt.whitespace ? ' = ' : '='; 59 | 60 | for (const k of Object.keys(obj)) { 61 | const val = obj[k]; 62 | if (val && Array.isArray(val)) { 63 | for (const item of val) out += safe(k + '[]') + separator + safe(item) + '\n'; 64 | } else if (val && typeof val === 'object') children.push(k); 65 | else out += safe(k) + separator + safeGodotValue(val) + eol; 66 | } 67 | 68 | if (opt.section && out.length) out = '[' + safe(opt.section) + ']' + eol + '\n' + out; 69 | 70 | for (const k of children) { 71 | const nk = dotSplit(k).join('\\.'); 72 | const section = (opt.section ? opt.section + '.' : '') + nk; 73 | const {whitespace} = opt; 74 | const child = encode(obj[k], { 75 | section, 76 | whitespace 77 | }); 78 | if (out.length && child.length) out += eol; 79 | 80 | out += child; 81 | } 82 | 83 | return out; 84 | }; 85 | 86 | const dotSplit = (str) => 87 | str 88 | .replace(/\1/g, '\u0002LITERAL\\1LITERAL\u0002') 89 | .replace(/\\\./g, '\u0001') 90 | .split(/\./) 91 | .map((part) => part.replace(/\1/g, '\\.').replace(/\2LITERAL\\1LITERAL\2/g, '\u0001')); 92 | 93 | const decode = (str) => { 94 | const out = Object.create(null); 95 | let p = out; 96 | let section = null; 97 | // section |key = value 98 | const re = /^\[([^\]]*)\]$|^([^=]+)(=(.*))?$/i; 99 | const lines = str.split(/[\r\n]+/g); 100 | 101 | for (const line of lines) { 102 | if (!line || line.match(/^\s*[;#]/)) continue; 103 | const match = line.match(re); 104 | if (!match) continue; 105 | if (match[1] !== undefined) { 106 | section = unsafe(match[1]); 107 | if (section === '__proto__') { 108 | // not allowed 109 | // keep parsing the section, but don't attach it. 110 | p = Object.create(null); 111 | continue; 112 | } 113 | p = out[section] = out[section] || Object.create(null); 114 | continue; 115 | } 116 | const keyRaw = unsafe(match[2]); 117 | const isArray = keyRaw.length > 2 && keyRaw.slice(-2) === '[]'; 118 | const key = isArray ? keyRaw.slice(0, -2) : keyRaw; 119 | if (key === '__proto__') continue; 120 | const valueRaw = match[3] ? unsafe(match[4]) : true; 121 | const value = 122 | valueRaw === 'true' || valueRaw === 'false' || valueRaw === 'null' 123 | ? JSON.parse(valueRaw) 124 | : valueRaw; 125 | 126 | // Convert keys with '[]' suffix to an array 127 | if (isArray) { 128 | if (!hasOwnProperty.call(p, key)) p[key] = []; 129 | else if (!Array.isArray(p[key])) p[key] = [p[key]]; 130 | } 131 | 132 | // safeguard against resetting a previously defined 133 | // array by accidentally forgetting the brackets 134 | if (Array.isArray(p[key])) p[key].push(value); 135 | else p[key] = value; 136 | } 137 | 138 | // {a:{y:1},"a.b":{x:2}} --> {a:{y:1,b:{x:2}}} 139 | // use a filter to return the keys that have to be deleted. 140 | const remove = []; 141 | for (const k of Object.keys(out)) { 142 | if (!hasOwnProperty.call(out, k) || typeof out[k] !== 'object' || Array.isArray(out[k])) 143 | continue; 144 | 145 | // see if the parent section is also an object. 146 | // if so, add it to that, and mark this one for deletion 147 | const parts = dotSplit(k); 148 | let p = out; 149 | const l = parts.pop(); 150 | const nl = l.replace(/\\\./g, '.'); 151 | for (const part of parts) { 152 | if (part === '__proto__') continue; 153 | if (!hasOwnProperty.call(p, part) || typeof p[part] !== 'object') 154 | p[part] = Object.create(null); 155 | p = p[part]; 156 | } 157 | if (p === out && nl === l) continue; 158 | 159 | p[nl] = out[k]; 160 | remove.push(k); 161 | } 162 | for (const del of remove) delete out[del]; 163 | 164 | return out; 165 | }; 166 | 167 | const isQuoted = (val) => 168 | (val.charAt(0) === '"' && val.slice(-1) === '"') || 169 | (val.charAt(0) === "'" && val.slice(-1) === "'"); 170 | 171 | const safe = (val) => 172 | typeof val !== 'string' || 173 | val.match(/[=\r\n]/) || 174 | val.match(/^\[/) || 175 | (val.length > 1 && isQuoted(val)) || 176 | val !== val.trim() 177 | ? JSON.stringify(val) 178 | : val.replace(/;/g, '\\;').replace(/#/g, '\\#'); 179 | 180 | const unsafe = (val, doUnesc) => { 181 | val = (val || '').trim(); 182 | if (isQuoted(val)) { 183 | // remove the single quotes before calling JSON.parse 184 | if (val.charAt(0) === "'") val = val.substr(1, val.length - 2); 185 | 186 | try { 187 | val = JSON.parse(val); 188 | } catch (_) {} 189 | } else { 190 | // walk the val to find the first not-escaped ; character 191 | let esc = false; 192 | let unesc = ''; 193 | for (let i = 0, l = val.length; i < l; i++) { 194 | const c = val.charAt(i); 195 | if (esc) { 196 | if ('\\;#'.indexOf(c) !== -1) unesc += c; 197 | else unesc += '\\' + c; 198 | 199 | esc = false; 200 | } else if (';#'.indexOf(c) !== -1) break; 201 | else if (c === '\\') esc = true; 202 | else unesc += c; 203 | } 204 | if (esc) unesc += '\\'; 205 | 206 | return unesc.trim(); 207 | } 208 | return val; 209 | }; 210 | 211 | // ----------------------------------------------------------------------------- 212 | 213 | export default { 214 | parse: decode, 215 | decode, 216 | stringify: encode, 217 | encode, 218 | safe, 219 | unsafe 220 | }; 221 | -------------------------------------------------------------------------------- /fox/behaviours/draggable-camera.gd: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # tips from 3 | # https://github.com/Ombarus/SolarRogue/blob/master/scripts/CamControl.gd 4 | # https://www.youtube.com/watch?v=duDk9ICkKWI 5 | # ------------------------------------------------------------------------------ 6 | 7 | extends CanvasLayer 8 | 9 | # ------------------------------------------------------------------------------ 10 | 11 | @onready var camera = $camera 12 | @onready var boundaries = $boundaries 13 | 14 | # ------------------------------------------------------------------------------ 15 | 16 | signal startPressing 17 | signal startDragging 18 | signal stopDragging 19 | signal draggingCamera 20 | 21 | # ------------------------------------------------------------------------------ 22 | 23 | @export var pan_smooth: float = -3 24 | @export var dragDelay: float = 80 25 | 26 | # ------------------------------------------------------------------------------ 27 | 28 | var ZOOM = 2.5 29 | 30 | var mouse_start_pos 31 | var screen_start_position 32 | 33 | var tweening = false 34 | var pressing = false 35 | var dragging = false 36 | var smoothing = false 37 | var moving = false 38 | var startPressingTime = 0 39 | var startPressingPosition 40 | 41 | # ------------------------------------------------------------------------------ 42 | 43 | var draggingVelocity := Vector2(0,0) 44 | var _last_cam_pos := Vector2(0,0) 45 | 46 | # ------------------------------------------------------------------------------ 47 | 48 | ## it's not possible to create a local behaviour because a fullscreen Control 49 | ## for the camera would intercept all input events 50 | func _input(event): 51 | if event is InputEventMouseButton: 52 | # ------- mouse down 53 | if event.is_pressed(): 54 | var now = Time.get_ticks_msec() 55 | startPressingTime = now 56 | startPressingPosition = event.position 57 | tweening = false 58 | smoothing = false 59 | pressing = true 60 | startPressing.emit() 61 | 62 | # ------- mouse up 63 | else: 64 | pressing = false 65 | startPressingTime = 0 66 | startPressingPosition = null 67 | 68 | if(Gesture.isDragging()): 69 | return 70 | 71 | if(dragging): 72 | dragging = false 73 | stopDragging.emit() 74 | 75 | if(boundaries): 76 | var outOfBoundaries = checkBoundaries({reposition=true}) 77 | if(not outOfBoundaries): 78 | smoothing = true 79 | 80 | # ------- mouse mmotion 81 | elif event is InputEventMouseMotion: 82 | # updates position only when global dragging is occuring 83 | if(Gesture.isDragging()): 84 | return 85 | 86 | if(startPressingTime > 0): 87 | var now = Time.get_ticks_msec() 88 | if(now - startPressingTime > dragDelay): 89 | var startDiff = startPressingPosition - event.position 90 | if(startDiff.length() < 50): 91 | return 92 | 93 | if(not dragging): 94 | dragging = true 95 | mouse_start_pos = event.position 96 | screen_start_position = camera.position 97 | startDragging.emit() 98 | 99 | var mouseDiff = mouse_start_pos - event.position 100 | camera.position = mouseDiff / ZOOM + screen_start_position 101 | draggingCamera.emit() 102 | get_viewport().set_input_as_handled() 103 | 104 | # ------------------------------------------------------------------------------ 105 | 106 | func _process(delta): 107 | if delta <= 0: 108 | return 109 | 110 | if dragging: 111 | update_vel(delta) 112 | elif smoothing: 113 | smooth(delta) 114 | 115 | var diff = _last_cam_pos - camera.position 116 | moving = diff.length() > 5 117 | _last_cam_pos = camera.position 118 | 119 | if(not moving and not pressing): 120 | smoothing = false 121 | tweening = false 122 | 123 | if(boundaries): 124 | checkBoundaries({reposition=true}) 125 | 126 | # ------------------------------------------------------------------------------ 127 | 128 | func update_vel(delta : float): 129 | var move = _last_cam_pos - camera.position 130 | var move_speed:Vector2 = move / delta 131 | 132 | draggingVelocity = (draggingVelocity + move_speed ) / 2.0 133 | draggingVelocity.x = clamp(draggingVelocity.x, -10000, 10000) 134 | draggingVelocity.y = clamp(draggingVelocity.y, -10000, 10000) 135 | 136 | # ------------------------------------------------------------------------------ 137 | 138 | func toPosition(from: Vector2, to : Vector2, duration: float = 1): 139 | if(smoothing or tweening or dragging): 140 | return 141 | 142 | tweening = true 143 | 144 | var tween = create_tween() 145 | 146 | camera.position = from 147 | tween.tween_property(camera, "position", to, duration).connect("finished", func(): 148 | tweening = false 149 | ) 150 | 151 | # ------------------------------------------------------------------------------ 152 | 153 | func smooth(delta : float): 154 | if(tweening): 155 | return 156 | 157 | # cancel smoothing if going out of boundaries 158 | if(boundaries): 159 | var outOfBoundaries = checkBoundaries({offset=400}) 160 | if(outOfBoundaries): 161 | smoothing = false 162 | return 163 | 164 | var l = draggingVelocity.length() 165 | var move_frame = 10 * exp(pan_smooth * ((log(l/10) / pan_smooth)+delta)) 166 | draggingVelocity = draggingVelocity.normalized() * move_frame 167 | camera.position -= draggingVelocity * delta 168 | 169 | # ------------------------------------------------------------------------------ 170 | 171 | func checkBoundaries(options = {}): 172 | var reposition = options.reposition if options.has('reposition') else false 173 | var _offset = options.offset if options.has('offset') else 0 174 | 175 | var boundariesLeft = boundaries.position.x - _offset 176 | var boundariesRight = boundaries.position.x + boundaries.size[0] + _offset 177 | var boundariesTop = boundaries.position.y - _offset 178 | var boundariesBottom = boundaries.position.y + boundaries.size[1] + _offset 179 | 180 | var outOnLeft = camera.position.x < boundariesLeft 181 | var outOnRight = camera.position.x > boundariesRight 182 | var outOnTop = camera.position.y < boundariesTop 183 | var outOnBottom = camera.position.y > boundariesBottom 184 | 185 | if outOnLeft or outOnRight or outOnBottom or outOnTop: 186 | if(reposition): 187 | var newX = camera.position.x 188 | var newY = camera.position.y 189 | var innerMargin = 10 # not to have accurate equalities 190 | 191 | if outOnLeft: 192 | newX = boundariesLeft + innerMargin 193 | if outOnRight: 194 | newX = boundariesRight - innerMargin 195 | if outOnTop: 196 | newY = boundariesTop + innerMargin 197 | if outOnBottom: 198 | newY = boundariesBottom - innerMargin 199 | 200 | if(newX != camera.position.x or newY != camera.position.y): 201 | toPosition(camera.position, Vector2(newX, newY), 0.5) 202 | 203 | return true 204 | 205 | # ============================================================================== 206 | # API 207 | # ============================================================================== 208 | 209 | func setPosition(_position: Vector2): 210 | camera.position = _position 211 | 212 | func setZoom(_zoom: float): 213 | ZOOM = _zoom 214 | camera.zoom = Vector2(_zoom, _zoom) 215 | 216 | func zoom(): 217 | camera.zoom = Vector2(ZOOM * 0.7, ZOOM * 0.7) 218 | 219 | Animate.to(camera, { 220 | propertyPath = 'zoom', 221 | toValue = Vector2(ZOOM, ZOOM), 222 | duration = 1, 223 | easing = Tween.EASE_OUT 224 | }) 225 | 226 | func focusCameraOn(_position): 227 | Animate.to(camera, { 228 | propertyPath = 'position', 229 | toValue = _position, 230 | duration = 1, 231 | easing = Tween.EASE_OUT 232 | }) 233 | -------------------------------------------------------------------------------- /cli/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --no-warnings 2 | // ----------------------------------------------------------------------------- 3 | 4 | import chalk from 'chalk'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import shell from 'shelljs'; 8 | import { spawn } from 'child_process'; 9 | import yargsFactory from 'yargs'; 10 | 11 | import pkg from '../package.json' with { type: 'json' }; 12 | 13 | // ----------------------------------------------------------------------------- 14 | 15 | import generateIcons from './generate-icons.js'; 16 | import generateSplashscreens from './generate-splashscreens.js'; 17 | import generateScreenshots from './generate-screenshots.js'; 18 | import exportBundle from './bundler/export.js'; 19 | import { readPresets } from './bundler/read-presets.js'; 20 | import switchBundle from './bundler/switch.js'; 21 | import runGame from './run-game.js'; 22 | 23 | // ----------------------------------------------------------------------------- 24 | 25 | const EXPORT = 'export'; 26 | const SWITCH = 'switch'; 27 | 28 | const GENERATE_ICONS = 'generate:icons'; 29 | const GENERATE_SPLASHSCREENS = 'generate:splashscreens'; 30 | const GENERATE_SCREENSHOTS = 'generate:screenshots'; 31 | const UPDATE_PO_FILES = 'update-po-files'; 32 | 33 | const RUN_EDITOR = 'run:editor'; 34 | const RUN_GAME = 'run:game'; 35 | 36 | // ----------------------------------------------------------------------------- 37 | 38 | const commands = [ 39 | EXPORT, 40 | SWITCH, 41 | GENERATE_ICONS, 42 | GENERATE_SCREENSHOTS, 43 | GENERATE_SPLASHSCREENS, 44 | UPDATE_PO_FILES, 45 | RUN_EDITOR, 46 | RUN_GAME 47 | ]; 48 | 49 | const commandMessage = `choose a command above, example:\n${chalk.italic(`fox ${RUN_EDITOR}`)}`; 50 | 51 | // ----------------------------------------------------------------------------- 52 | 53 | const DEFAULT_CONFIG_FILE = 'fox/default.config.json'; 54 | const CONFIG_FILE = 'fox.config.json'; 55 | 56 | // ----------------------------------------------------------------------------- 57 | 58 | const getSettings = async (command, defaultConfig) => { 59 | let config; 60 | const configPath = path.resolve(process.cwd(), `./${CONFIG_FILE}`); 61 | 62 | try { 63 | console.log(`⚙️ reading ${chalk.blue.bold(CONFIG_FILE)}`); 64 | config = (await import(configPath, { with: { type: "json" } })).default; 65 | 66 | if (!config[command] && defaultConfig[command]) { 67 | console.log(chalk.green(`Using default config for command "${command}"`)); 68 | config = defaultConfig; 69 | } 70 | } catch (e) { 71 | console.log(`could not find ${chalk.blue.bold(CONFIG_FILE)} > ${chalk.green(`Using default config for command "${command}"`)}`); 72 | config = defaultConfig; 73 | return; 74 | } 75 | 76 | return { 77 | config: { ...defaultConfig[command], ...config[command] }, 78 | core: { ...defaultConfig.core, ...config.core }, 79 | bundles: config.bundles 80 | }; 81 | }; 82 | 83 | // ----------------------------------------------------------------------------- 84 | 85 | const verifyConfig = (config, defaultConfig) => { 86 | const requirements = Object.keys(defaultConfig); 87 | 88 | requirements.forEach((requirement) => { 89 | const value = config[requirement]; 90 | if (!value) { 91 | const message = `${chalk.red.bold(`${requirement} not provided`)} in your config.`; 92 | console.log(message); 93 | throw new Error(message); 94 | } 95 | }); 96 | 97 | if (config.output) { 98 | const projectPath = path.resolve(process.cwd(), './'); 99 | const output = `${projectPath}/${config.output}`; 100 | 101 | console.log(`⚙️ ${chalk.blue.bold('verifying output path')}`); 102 | console.log(output); 103 | 104 | if (!fs.existsSync(output)) { 105 | shell.mkdir('-p', output); 106 | console.log('✅ created output.'); 107 | } 108 | } 109 | }; 110 | 111 | // ----------------------------------------------------------------------------- 112 | 113 | const cli = async (yargs, params) => { 114 | const defaultConfigPath = path.resolve(process.cwd(), `${DEFAULT_CONFIG_FILE}`); 115 | 116 | let defaultConfig; 117 | 118 | try { 119 | defaultConfig = (await import(defaultConfigPath, { with: { type: "json" } })).default; 120 | } catch (e) { 121 | console.log( 122 | chalk.red.bold('🔴 failed:'), 123 | chalk.blue.bold(process.cwd()), 124 | 'is not a project using Fox' 125 | ); 126 | return; 127 | } 128 | 129 | // -------- 130 | 131 | const command = yargs.argv._[0]; 132 | 133 | if (!commands.includes(command)) { 134 | yargs.showHelp(); 135 | return; 136 | } 137 | 138 | // -------- 139 | 140 | console.log(chalk.bold.green(`Fox CLI v${pkg.version}`)); 141 | console.log(`🦊 ${chalk.italic('> command')} ${chalk.cyan(command)}`); 142 | const settings = await getSettings(command, defaultConfig); 143 | 144 | if (!settings) { 145 | return; 146 | } 147 | 148 | const { core, config, bundles } = settings; 149 | 150 | // -------- Godot commands 151 | 152 | switch (command) { 153 | case RUN_EDITOR: { 154 | console.log('----------------------------'); 155 | console.log(`🦊 ${chalk.italic('opening Godot editor')}`); 156 | const { resolution, position } = config; 157 | 158 | // '-e' runs editor 159 | const editorProcess = spawn( 160 | core.godot, 161 | ['-e', '--windowed', '--resolution', resolution, '--position', position], 162 | { stdio: [process.stdin, process.stdout, process.stderr] } 163 | ); 164 | 165 | editorProcess.on('close', () => { 166 | console.log(`🦊 ${chalk.italic('bye!')}`); 167 | }); 168 | 169 | return; 170 | } 171 | case RUN_GAME: { 172 | runGame(core.godot, params, config); 173 | return; 174 | } 175 | case EXPORT: { 176 | exportBundle(settings); 177 | return; 178 | } 179 | case SWITCH: { 180 | const presets = readPresets(); 181 | switchBundle(settings, presets); 182 | return; 183 | } 184 | } 185 | 186 | // -------- IO commands 187 | 188 | verifyConfig(config, defaultConfig[command]); 189 | 190 | switch (command) { 191 | case GENERATE_ICONS: { 192 | generateIcons(config); 193 | break; 194 | } 195 | case GENERATE_SPLASHSCREENS: { 196 | generateSplashscreens(config); 197 | break; 198 | } 199 | case UPDATE_PO_FILES: { 200 | const { poFiles, potTemplate } = config; 201 | console.log(`⚙️ using ${chalk.blue.bold('msgmerge')} on your .po files`); 202 | shell.exec(`for file in ${poFiles}; do echo \${file} ; msgmerge --backup=off --update \${file} ${potTemplate}; done`); 203 | break; 204 | } 205 | case GENERATE_SCREENSHOTS: { 206 | generateScreenshots(config); 207 | break; 208 | } 209 | default: { 210 | console.log(command); 211 | console.log(chalk.red.bold('🔴 not handled')); 212 | } 213 | } 214 | 215 | return true; 216 | }; 217 | 218 | // ----------------------------------------------------------------------------- 219 | 220 | const execute = async () => { 221 | const params = process.argv.slice(3); 222 | 223 | const yargs = yargsFactory(process.argv.splice(2)) 224 | .usage('Usage: fox [options]') 225 | .command(RUN_EDITOR, 'open Godot Editor with your main scene') 226 | .command(RUN_GAME, 'start your game locally') 227 | .command(EXPORT, 'export a bundle for one of your presets') 228 | .command(SWITCH, 'switch from a bundle to another (write in override.cfg)') 229 | .command(UPDATE_PO_FILES, 'calls msgmerge on all .po files in your project -- experimental setup for avindi') 230 | .command(GENERATE_ICONS, 'generate icons, using a base 1200x1200 image') 231 | .command( 232 | GENERATE_SPLASHSCREENS, 233 | 'generate splashscreens, extending a background color from a centered base image' 234 | ) 235 | .command( 236 | GENERATE_SCREENSHOTS, 237 | 'resize all images in a folder to 2560x1600, to match store requirements' 238 | ) 239 | .demandCommand(1, 1, commandMessage, commandMessage) 240 | .help('h') 241 | .version(pkg.version) 242 | .alias('version', 'v').epilog(`${chalk.bold.green(`🦊 Fox CLI v${pkg.version}`)} 243 | Documentation: https://github.com/uralys/fox 244 | Icons, splashscreens and screenshots commands require ImageMagick https://imagemagick.org/index.php`); 245 | 246 | // ----------------------------------------------------------------------------- 247 | 248 | try { 249 | const result = await cli(yargs, params); 250 | if (result) { 251 | console.log(`🦊 ${chalk.italic('done.')}`); 252 | } 253 | } catch (e) { 254 | console.log(e); 255 | console.log(chalk.red.bold('🔴 failed')); 256 | } 257 | } 258 | 259 | execute() 260 | --------------------------------------------------------------------------------