├── 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://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 |
73 |
74 |
75 |
76 |
77 |
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 |
--------------------------------------------------------------------------------