Tutorial improvements
This commit is contained in:
@@ -158,7 +158,7 @@ openlink = Open Link
|
|||||||
copylink = Copy Link
|
copylink = Copy Link
|
||||||
back = Back
|
back = Back
|
||||||
quit.confirm = Are you sure you want to quit?
|
quit.confirm = Are you sure you want to quit?
|
||||||
quit.confirm.tutorial = Are you sure you want to quit the tutorial?
|
quit.confirm.tutorial = Are you sure you know what you're doing?\nThe tutorial can be re-taken in[accent] Settings->Game->Re-Take Tutorial.[]
|
||||||
loading = [accent]Loading...
|
loading = [accent]Loading...
|
||||||
saving = [accent]Saving...
|
saving = [accent]Saving...
|
||||||
wave = [accent]Wave {0}
|
wave = [accent]Wave {0}
|
||||||
@@ -310,6 +310,7 @@ ping = Ping: {0}ms
|
|||||||
language.restart = Please restart your game for the language settings to take effect.
|
language.restart = Please restart your game for the language settings to take effect.
|
||||||
settings = Settings
|
settings = Settings
|
||||||
tutorial = Tutorial
|
tutorial = Tutorial
|
||||||
|
tutorial.retake = Re-Take Tutorial
|
||||||
editor = Editor
|
editor = Editor
|
||||||
mapeditor = Map Editor
|
mapeditor = Map Editor
|
||||||
donate = Donate
|
donate = Donate
|
||||||
@@ -862,20 +863,23 @@ unit.chaos-array.name = Chaos Array
|
|||||||
unit.eradicator.name = Eradicator
|
unit.eradicator.name = Eradicator
|
||||||
unit.lich.name = Lich
|
unit.lich.name = Lich
|
||||||
unit.reaper.name = Reaper
|
unit.reaper.name = Reaper
|
||||||
tutorial.intro = You have entered the[scarlet] Mindustry Tutorial.[]\n\nBegin by[accent] mining copper[]. Tap a copper ore vein near your core to do this.\n\n[accent]{0}/{1} copper
|
tutorial.next = [lightgray]<Tap to continue>
|
||||||
tutorial.drill = Mining manually is inefficient.\n[accent]Drills []can mine automatically.\n\nClick the drill tab in the bottom right, then select the[accent] mechanical drill[]. Place it on a copper vein by clicking.\n[accent]Right-click[] to stop building.
|
tutorial.intro = You have entered the[scarlet] Mindustry Tutorial.[]\nBegin by[accent] mining copper[]. Tap a copper ore vein near your core to do this.\n\n[accent]{0}/{1} copper
|
||||||
tutorial.drill.mobile = Mining manually is inefficient.\n[accent]Drills []can mine automatically.\n\nTap the drill tab in the bottom right, then select the[accent] mechanical drill[]. Place it on a copper vein by tapping, then press the[accent] checkmark[] below to confirm your selection.\nPress the[accent] X button[] to cancel placement.
|
tutorial.drill = Mining manually is inefficient.\n[accent]Drills []can mine automatically.\nClick the drill tab in the bottom right.\nSelect the[accent] mechanical drill[]. Place it on a copper vein by clicking.\n[accent]Right-click[] to stop building.
|
||||||
tutorial.blockinfo = Each block has different stats. Each drill can only mine certain ores.\nTo check a block's info and stats,[accent] tap the "?" button while selecting it in the build menu.[]\n\n[yellow]Access the Mechanical Drill's stats now.[]
|
tutorial.drill.mobile = Mining manually is inefficient.\n[accent]Drills []can mine automatically.\nTap the drill tab in the bottom right.\nSelect the[accent] mechanical drill[].\nPlace it on a copper vein by tapping, then press the[accent] checkmark[] below to confirm your selection.\nPress the[accent] X button[] to cancel placement.
|
||||||
tutorial.conveyor = [accent]Conveyors[] are used to transport items to the core.\nMake a line of conveyors from the drill to the core.\n[yellow]Hold down the mouse to place in a line.[]\nHold[accent] CTRL[] while selecting a line to place diagonally.\n\n[accent]{0}/{1} conveyors\n[accent]0/1 items delivered
|
tutorial.blockinfo = Each block has different stats. Each drill can only mine certain ores.\nTo check a block's info and stats,[accent] tap the "?" button while selecting it in the build menu.[]\n\n[accent]Access the Mechanical Drill's stats now.[]
|
||||||
tutorial.conveyor.mobile = [accent]Conveyors[] are used to transport items to the core.\nMake a line of conveyors from the drill to the core.[yellow] Place in a line by holding down your finger for a few seconds[] and dragging in a direction.\n\n[accent]{0}/{1} conveyors\n[accent]0/1 items delivered
|
tutorial.conveyor = [accent]Conveyors[] are used to transport items to the core.\nMake a line of conveyors from the drill to the core.\n[accent]Hold down the mouse to place in a line.[]\nHold[accent] CTRL[] while selecting a line to place diagonally.\n\n[accent]{0}/{1} conveyors\n[accent]0/1 items delivered
|
||||||
|
tutorial.conveyor.mobile = [accent]Conveyors[] are used to transport items to the core.\nMake a line of conveyors from the drill to the core.\n[accent] Place in a line by holding down your finger for a few seconds[] and dragging in a direction.\n\n[accent]{0}/{1} conveyors\n[accent]0/1 items delivered
|
||||||
tutorial.turret = Defensive structures must be built to repel the[lightgray] enemy[].\nBuild a[accent] duo turret[] near your base.
|
tutorial.turret = Defensive structures must be built to repel the[lightgray] enemy[].\nBuild a[accent] duo turret[] near your base.
|
||||||
tutorial.drillturret = Duo turrets require[accent] copper ammo []to shoot.\nPlace a drill near to the turret. Lead conveyors into the turret to supply it with copper.\n\n[accent]Ammo delivered: 0/1
|
tutorial.drillturret = Duo turrets require[accent] copper ammo []to shoot.\nPlace a drill near to the turret\n Lead conveyors into the turret to supply it with copper.\n\n[accent]Ammo delivered: 0/1
|
||||||
tutorial.pause = During battle, you are able to[accent] pause the game.[]\nYou may queue buildings while paused.\n\n[accent]Press space to pause and unpause.
|
tutorial.pause = During battle, you are able to[accent] pause the game.[]\nYou may queue buildings while paused.\n\n[accent]Press space to pause.
|
||||||
tutorial.pause.mobile = During battle, you are able to[accent] pause the game.[]\nYou may queue buildings while paused.\n\n[accent]Press this button in the top left to pause and unpause.
|
tutorial.pause.mobile = During battle, you are able to[accent] pause the game.[]\nYou may queue buildings while paused.\n\n[accent]Press this button in the top left to pause.
|
||||||
tutorial.breaking = Blocks frequently need to be destroyed.\n[accent]Hold down right-click[] to destroy all blocks in a selection.[]\n\n[yellow]Destroy all the scrap blocks to the right of your core.
|
tutorial.unpause = Now press space again to unpause.
|
||||||
tutorial.breaking.mobile = Blocks frequently need to be destroyed.\n[accent]Select deconstruction mode[], then tap a block to begin breaking it.\nDestroy an area by holding down your finger for a few seconds[] and dragging in a direction.\nPress the checkmark button to confirm breaking.\n\n[yellow]Destroy all the scrap blocks to the right of your core.
|
tutorial.unpause.mobile = Now press it again to unpause.
|
||||||
tutorial.withdraw = In some situations, taking items directly from blocks is necessary.\nTo do this, [accent]tap a block[] with items in it, then [accent]tap the item[] in the inventory. Multiple items can be withdrawn by [accent]tapping and holding[].\n\n[yellow]Withdraw some copper from the core.[]
|
tutorial.breaking = Blocks frequently need to be destroyed.\n[accent]Hold down right-click[] to destroy all blocks in a selection.[]\n\n[accent]Destroy all the scrap blocks to the right of your core.
|
||||||
tutorial.deposit = Deposit items into blocks by dragging from your ship to the destination block.\n\n[yellow]Deposit your copper back into the core.[]
|
tutorial.breaking.mobile = Blocks frequently need to be destroyed.\n[accent]Select deconstruction mode[], then tap a block to begin breaking it.\nDestroy an area by holding down your finger for a few seconds[] and dragging in a direction.\nPress the checkmark button to confirm breaking.\n\n[accent]Destroy all the scrap blocks to the right of your core.
|
||||||
|
tutorial.withdraw = In some situations, taking items directly from blocks is necessary.\nTo do this, [accent]tap a block[] with items in it, then [accent]tap the item[] in the inventory. Multiple items can be withdrawn by [accent]tapping and holding[].\n\n[accent]Withdraw some copper from the core.[]
|
||||||
|
tutorial.deposit = Deposit items into blocks by dragging from your ship to the destination block.\n\n[accent]Deposit your copper back into the core.[]
|
||||||
tutorial.waves = The[lightgray] enemy[] approaches.\n\nDefend the core for 2 waves.[accent] Click[] to shoot.\nBuild more turrets and drills. Mine more copper.
|
tutorial.waves = The[lightgray] enemy[] approaches.\n\nDefend the core for 2 waves.[accent] Click[] to shoot.\nBuild more turrets and drills. Mine more copper.
|
||||||
tutorial.waves.mobile = The[lightgray] enemy[] approaches.\n\nDefend the core for 2 waves. Your ship will automatically fire at enemies.\nBuild more turrets and drills. Mine more copper.
|
tutorial.waves.mobile = The[lightgray] enemy[] approaches.\n\nDefend the core for 2 waves. Your ship will automatically fire at enemies.\nBuild more turrets and drills. Mine more copper.
|
||||||
tutorial.launch = Once you reach a specific wave, you are able to[accent] launch the core[], leaving your defenses behind and[accent] obtaining all the resources in your core.[]\nThese resources can then be used to research new technology.\n\n[accent]Press the launch button.
|
tutorial.launch = Once you reach a specific wave, you are able to[accent] launch the core[], leaving your defenses behind and[accent] obtaining all the resources in your core.[]\nThese resources can then be used to research new technology.\n\n[accent]Press the launch button.
|
||||||
|
|||||||
@@ -52,7 +52,9 @@
|
|||||||
ButtonStyle: {
|
ButtonStyle: {
|
||||||
default: {
|
default: {
|
||||||
down: button-down,
|
down: button-down,
|
||||||
up: button
|
up: button,
|
||||||
|
over: button-over,
|
||||||
|
disabled: button-disabled
|
||||||
},
|
},
|
||||||
square: {
|
square: {
|
||||||
over: button-square-over,
|
over: button-square-over,
|
||||||
|
|||||||
@@ -228,7 +228,8 @@ public class Control implements ApplicationListener{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void playTutorial(Zone zone){
|
public void playTutorial(){
|
||||||
|
Zone zone = Zones.groundZero;
|
||||||
ui.loadAnd(() -> {
|
ui.loadAnd(() -> {
|
||||||
logic.reset();
|
logic.reset();
|
||||||
Net.reset();
|
Net.reset();
|
||||||
@@ -313,7 +314,7 @@ public class Control implements ApplicationListener{
|
|||||||
|
|
||||||
//play tutorial on stop
|
//play tutorial on stop
|
||||||
if(!settings.getBool("tutorial", false)){
|
if(!settings.getBool("tutorial", false)){
|
||||||
Core.app.post(() -> playTutorial(Zones.groundZero));
|
Core.app.post(this::playTutorial);
|
||||||
}
|
}
|
||||||
|
|
||||||
//display UI scale changed dialog
|
//display UI scale changed dialog
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public class Saves{
|
|||||||
lastTimestamp = Time.millis();
|
lastTimestamp = Time.millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!state.is(State.menu) && !state.gameOver && current != null && current.isAutosave()){
|
if(!state.is(State.menu) && !state.gameOver && current != null && current.isAutosave() && !state.rules.tutorial){
|
||||||
time += Time.delta();
|
time += Time.delta();
|
||||||
if(time > Core.settings.getInt("saveinterval") * 60){
|
if(time > Core.settings.getInt("saveinterval") * 60){
|
||||||
saving = true;
|
saving = true;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class Tutorial{
|
|||||||
|
|
||||||
private ObjectSet<String> events = new ObjectSet<>();
|
private ObjectSet<String> events = new ObjectSet<>();
|
||||||
private ObjectIntMap<Block> blocksPlaced = new ObjectIntMap<>();
|
private ObjectIntMap<Block> blocksPlaced = new ObjectIntMap<>();
|
||||||
|
private int sentence;
|
||||||
public TutorialStage stage = TutorialStage.values()[0];
|
public TutorialStage stage = TutorialStage.values()[0];
|
||||||
|
|
||||||
public Tutorial(){
|
public Tutorial(){
|
||||||
@@ -43,7 +44,7 @@ public class Tutorial{
|
|||||||
|
|
||||||
/** update tutorial state, transition if needed */
|
/** update tutorial state, transition if needed */
|
||||||
public void update(){
|
public void update(){
|
||||||
if(stage.done.get()){
|
if(stage.done.get() && !canNext()){
|
||||||
next();
|
next();
|
||||||
}else{
|
}else{
|
||||||
stage.update();
|
stage.update();
|
||||||
@@ -52,15 +53,18 @@ public class Tutorial{
|
|||||||
|
|
||||||
/** draw UI overlay */
|
/** draw UI overlay */
|
||||||
public void draw(){
|
public void draw(){
|
||||||
stage.draw();
|
if(!Core.scene.hasDialog()){
|
||||||
|
stage.draw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resets tutorial state. */
|
/** Resets tutorial state. */
|
||||||
public void reset(){
|
public void reset(){
|
||||||
stage = TutorialStage.values()[4];
|
stage = TutorialStage.values()[0];
|
||||||
stage.begin();
|
stage.begin();
|
||||||
blocksPlaced.clear();
|
blocksPlaced.clear();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
sentence = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Goes on to the next tutorial step. */
|
/** Goes on to the next tutorial step. */
|
||||||
@@ -69,11 +73,32 @@ public class Tutorial{
|
|||||||
stage.begin();
|
stage.begin();
|
||||||
blocksPlaced.clear();
|
blocksPlaced.clear();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
sentence = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canNext(){
|
||||||
|
return sentence + 1 < stage.sentences.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void nextSentence(){
|
||||||
|
if(canNext()){
|
||||||
|
sentence ++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canPrev(){
|
||||||
|
return sentence > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void prevSentence(){
|
||||||
|
if(canPrev()){
|
||||||
|
sentence --;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum TutorialStage{
|
public enum TutorialStage{
|
||||||
intro(
|
intro(
|
||||||
line -> Core.bundle.format(line, item(Items.copper), mineCopper),
|
line -> Strings.format(line, item(Items.copper), mineCopper),
|
||||||
() -> item(Items.copper) >= mineCopper
|
() -> item(Items.copper) >= mineCopper
|
||||||
),
|
),
|
||||||
drill(() -> placed(Blocks.mechanicalDrill, 1)){
|
drill(() -> placed(Blocks.mechanicalDrill, 1)){
|
||||||
@@ -90,7 +115,7 @@ public class Tutorial{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
conveyor(
|
conveyor(
|
||||||
line -> Core.bundle.format(line, Math.min(placed(Blocks.conveyor), 2), 2),
|
line -> Strings.format(line, Math.min(placed(Blocks.conveyor), 2), 2),
|
||||||
() -> placed(Blocks.conveyor, 2) && event("lineconfirm") && event("coreitem")){
|
() -> placed(Blocks.conveyor, 2) && event("lineconfirm") && event("coreitem")){
|
||||||
void draw(){
|
void draw(){
|
||||||
outline("category-distribution");
|
outline("category-distribution");
|
||||||
@@ -111,6 +136,13 @@ public class Tutorial{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
unpause(() -> !state.isPaused()){
|
||||||
|
void draw(){
|
||||||
|
if(mobile){
|
||||||
|
outline("pause");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
breaking(TutorialStage::blocksBroken){
|
breaking(TutorialStage::blocksBroken){
|
||||||
void begin(){
|
void begin(){
|
||||||
placeBlocks();
|
placeBlocks();
|
||||||
@@ -157,21 +189,23 @@ public class Tutorial{
|
|||||||
|
|
||||||
protected final String line = Core.bundle.has("tutorial." + name() + ".mobile") && mobile ? "tutorial." + name() + ".mobile" : "tutorial." + name();
|
protected final String line = Core.bundle.has("tutorial." + name() + ".mobile") && mobile ? "tutorial." + name() + ".mobile" : "tutorial." + name();
|
||||||
protected final Function<String, String> text;
|
protected final Function<String, String> text;
|
||||||
|
protected final Array<String> sentences;
|
||||||
protected final BooleanProvider done;
|
protected final BooleanProvider done;
|
||||||
|
|
||||||
TutorialStage(Function<String, String> text, BooleanProvider done){
|
TutorialStage(Function<String, String> text, BooleanProvider done){
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.done = done;
|
this.done = done;
|
||||||
|
this.sentences = Array.select(Core.bundle.get(line).split("\n"), s -> !s.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
TutorialStage(BooleanProvider done){
|
TutorialStage(BooleanProvider done){
|
||||||
this.text = line -> Core.bundle.get(line);
|
this(line -> line, done);
|
||||||
this.done = done;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** displayed tutorial stage text.*/
|
/** displayed tutorial stage text.*/
|
||||||
public String text(){
|
public String text(){
|
||||||
return text.get(line);
|
String line = sentences.get(control.tutorial.sentence);
|
||||||
|
return line.contains("{") ? text.get(line) : line;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** called every frame when this stage is active.*/
|
/** called every frame when this stage is active.*/
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ public class PausedDialog extends FloatingDialog{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(control.saves.getCurrent() == null || !control.saves.getCurrent().isAutosave()){
|
if(control.saves.getCurrent() == null || !control.saves.getCurrent().isAutosave() || state.rules.tutorial){
|
||||||
state.set(State.menu);
|
state.set(State.menu);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,6 +185,17 @@ public class SettingsMenuDialog extends SettingsDialog{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
game.pref(new Setting(){
|
||||||
|
@Override
|
||||||
|
public void add(SettingsTable table){
|
||||||
|
table.addButton("$tutorial.retake", () -> {
|
||||||
|
control.playTutorial();
|
||||||
|
}).size(220f, 60f).pad(6).left();
|
||||||
|
table.add();
|
||||||
|
table.row();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
graphics.sliderPref("uiscale", 100, 25, 400, 25, s -> {
|
graphics.sliderPref("uiscale", 100, 25, 400, 25, s -> {
|
||||||
if(Core.graphics.getFrameId() > 10){
|
if(Core.graphics.getFrameId() > 10){
|
||||||
Log.info("changed");
|
Log.info("changed");
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import io.anuke.arc.scene.actions.*;
|
|||||||
import io.anuke.arc.scene.event.*;
|
import io.anuke.arc.scene.event.*;
|
||||||
import io.anuke.arc.scene.style.*;
|
import io.anuke.arc.scene.style.*;
|
||||||
import io.anuke.arc.scene.ui.*;
|
import io.anuke.arc.scene.ui.*;
|
||||||
import io.anuke.arc.scene.ui.layout.UnitScl;
|
|
||||||
import io.anuke.arc.scene.ui.layout.*;
|
import io.anuke.arc.scene.ui.layout.*;
|
||||||
import io.anuke.arc.util.*;
|
import io.anuke.arc.util.*;
|
||||||
import io.anuke.mindustry.core.GameState.*;
|
import io.anuke.mindustry.core.GameState.*;
|
||||||
@@ -314,11 +313,20 @@ public class HudFragment extends Fragment{
|
|||||||
|
|
||||||
//tutorial text
|
//tutorial text
|
||||||
parent.fill(t -> {
|
parent.fill(t -> {
|
||||||
t.touchable(Touchable.disabled);
|
|
||||||
Runnable resize = () -> {
|
Runnable resize = () -> {
|
||||||
t.clearChildren();
|
t.clearChildren();
|
||||||
t.top().right().visible(() -> state.rules.tutorial);
|
t.top().right().visible(() -> state.rules.tutorial);
|
||||||
t.table("button-trans", f -> f.labelWrap(() -> control.tutorial.stage.text()).width(!Core.graphics.isPortrait() ? 450f : 180f).pad(3f));
|
t.stack(new Button("default"){{
|
||||||
|
marginLeft(48f);
|
||||||
|
labelWrap(() -> control.tutorial.stage.text() + (control.tutorial.canNext() ? "\n\n" + Core.bundle.get("tutorial.next") : "")).width(!Core.graphics.isPortrait() ? 400f : 160f).pad(2f);
|
||||||
|
clicked(() -> control.tutorial.nextSentence());
|
||||||
|
setDisabled(() -> !control.tutorial.canNext());
|
||||||
|
}},
|
||||||
|
new Table(f -> {
|
||||||
|
f.left().addImageButton("icon-arrow-left-small", "empty", iconsizesmall, () -> {
|
||||||
|
control.tutorial.prevSentence();
|
||||||
|
}).width(44f).growY().visible(() -> control.tutorial.canPrev());
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
resize.run();
|
resize.run();
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package io.anuke.mindustry.world.blocks.production;
|
package io.anuke.mindustry.world.blocks.production;
|
||||||
|
|
||||||
import io.anuke.arc.Core;
|
import io.anuke.arc.*;
|
||||||
import io.anuke.arc.graphics.g2d.Draw;
|
import io.anuke.arc.graphics.g2d.*;
|
||||||
import io.anuke.arc.graphics.g2d.TextureRegion;
|
import io.anuke.mindustry.entities.type.*;
|
||||||
import io.anuke.mindustry.entities.type.TileEntity;
|
import io.anuke.mindustry.world.*;
|
||||||
import io.anuke.mindustry.world.Tile;
|
import io.anuke.mindustry.world.meta.*;
|
||||||
import io.anuke.mindustry.world.meta.BlockStat;
|
|
||||||
import io.anuke.mindustry.world.meta.StatUnit;
|
|
||||||
|
|
||||||
public class Fracker extends SolidPump{
|
public class Fracker extends SolidPump{
|
||||||
protected final float itemUseTime = 100f;
|
protected final float itemUseTime = 100f;
|
||||||
@@ -41,7 +39,7 @@ public class Fracker extends SolidPump{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canProduce(Tile tile){
|
public boolean canProduce(Tile tile){
|
||||||
return tile.entity.liquids.get(result) < liquidCapacity;
|
return tile.entity.liquids.get(result) < liquidCapacity - 0.01f;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user